From fbab64df77eb3b39d3b7aa41d47b1a4d2d27ed58 Mon Sep 17 00:00:00 2001
From: Caleb Lemoine <21261388+circa10a@users.noreply.github.com>
Date: Mon, 27 Dec 2021 14:49:17 -0600
Subject: [PATCH] initial commit (#1)
---
.gitattributes | 4 +
.github/workflows/comment.yaml | 11 +
.github/workflows/release.yaml | 32 +++
.github/workflows/tag.yaml | 22 ++
.github/workflows/test.yaml | 28 +++
.gitignore | 21 ++
.golangci.yaml | 4 +
.goreleaser.yaml | 34 +++
Dockerfile.jenkins | 4 +
Dockerfile.vault | 8 +
Makefile | 61 +++++
README.md | 303 +++++++++++++++++++++++
configs/jenkins/jenkins.yaml | 11 +
configs/jenkins/plugins.txt | 1 +
docker-compose.yaml | 23 ++
go.mod | 15 ++
go.sum | 434 +++++++++++++++++++++++++++++++++
images/jenkins-vault.png | 3 +
images/permissions.png | 3 +
main.go | 30 +++
plugin/backend.go | 116 +++++++++
plugin/backend_test.go | 76 ++++++
plugin/client.go | 36 +++
plugin/jenkins_token.go | 121 +++++++++
plugin/jenkins_user.go | 158 ++++++++++++
plugin/path_config.go | 215 ++++++++++++++++
plugin/path_config_test.go | 145 +++++++++++
plugin/path_tokens.go | 120 +++++++++
plugin/path_tokens_test.go | 35 +++
plugin/path_users.go | 250 +++++++++++++++++++
plugin/path_users_test.go | 141 +++++++++++
31 files changed, 2465 insertions(+)
create mode 100644 .gitattributes
create mode 100644 .github/workflows/comment.yaml
create mode 100644 .github/workflows/release.yaml
create mode 100644 .github/workflows/tag.yaml
create mode 100644 .github/workflows/test.yaml
create mode 100644 .gitignore
create mode 100644 .golangci.yaml
create mode 100644 .goreleaser.yaml
create mode 100644 Dockerfile.jenkins
create mode 100644 Dockerfile.vault
create mode 100644 Makefile
create mode 100644 README.md
create mode 100644 configs/jenkins/jenkins.yaml
create mode 100644 configs/jenkins/plugins.txt
create mode 100644 docker-compose.yaml
create mode 100644 go.mod
create mode 100644 go.sum
create mode 100644 images/jenkins-vault.png
create mode 100644 images/permissions.png
create mode 100644 main.go
create mode 100644 plugin/backend.go
create mode 100644 plugin/backend_test.go
create mode 100644 plugin/client.go
create mode 100644 plugin/jenkins_token.go
create mode 100644 plugin/jenkins_user.go
create mode 100644 plugin/path_config.go
create mode 100644 plugin/path_config_test.go
create mode 100644 plugin/path_tokens.go
create mode 100644 plugin/path_tokens_test.go
create mode 100644 plugin/path_users.go
create mode 100644 plugin/path_users_test.go
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..580abec
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+# Images
+*.jpg filter=lfs diff=lfs merge=lfs -text
+*.jpeg filter=lfs diff=lfs merge=lfs -text
+*.png filter=lfs diff=lfs merge=lfs -text
\ No newline at end of file
diff --git a/.github/workflows/comment.yaml b/.github/workflows/comment.yaml
new file mode 100644
index 0000000..97a6c39
--- /dev/null
+++ b/.github/workflows/comment.yaml
@@ -0,0 +1,11 @@
+name: comment
+on:
+ pull_request:
+ types: [opened]
+jobs:
+ comment:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: circa10a/animal-action@main
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000..c680324
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,32 @@
+
+name: release
+on:
+ workflow_run:
+ workflows: ["Bump Git Version"]
+ branches: [main]
+ types:
+ - completed
+jobs:
+ goreleaser:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Unshallow
+ run: git fetch --prune --unshallow
+ - id: vars
+ run: |
+ echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
+ echo "Using Go version ${{ steps.vars.outputs.go_version }}"
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: ${{ steps.vars.outputs.go_version }}
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v2
+ with:
+ version: latest
+ args: release --rm-dist
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml
new file mode 100644
index 0000000..1782703
--- /dev/null
+++ b/.github/workflows/tag.yaml
@@ -0,0 +1,22 @@
+name: Bump Git Version
+on:
+ push:
+ branches:
+ - main
+jobs:
+ semver:
+ runs-on: ubuntu-latest
+ outputs:
+ tag: ${{ steps.tagging.outputs.new_tag }}
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: '0'
+ - name: Bump version and push tag
+ uses: anothrNick/github-tag-action@1.26.0
+ id: tagging
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ WITH_V: true
+ RELEASE_BRANCHES: main
+ DEFAULT_BUMP: minor
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000..c01448d
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,28 @@
+name: test
+on: [
+ push,
+ pull_request
+]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: '^1.16.6'
+ - name: Checkout code
+ uses: actions/checkout@v2
+ - name: Test
+ run: make test
+ golangci-lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: '^1.16.6'
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@v2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..65da982
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Remove any built plugins
+vault/plugins
+
+
+!.vscode/launch.json
\ No newline at end of file
diff --git a/.golangci.yaml b/.golangci.yaml
new file mode 100644
index 0000000..f894c30
--- /dev/null
+++ b/.golangci.yaml
@@ -0,0 +1,4 @@
+linters-settings:
+ govet:
+ enable:
+ - fieldalignment
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
new file mode 100644
index 0000000..be9143c
--- /dev/null
+++ b/.goreleaser.yaml
@@ -0,0 +1,34 @@
+# Visit https://goreleaser.com for documentation on how to customize this
+# behavior.
+before:
+ hooks:
+ - go mod tidy
+builds:
+- env:
+ # goreleaser does not work with CGO, it could also complicate
+ # usage by users in CI/CD systems like Terraform Cloud where
+ # they are unable to install libraries.
+ - CGO_ENABLED=0
+ mod_timestamp: '{{ .CommitTimestamp }}'
+ flags:
+ - -trimpath
+ ldflags:
+ - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}'
+ goos:
+ - freebsd
+ - windows
+ - linux
+ - darwin
+ goarch:
+ - amd64
+ - arm64
+ binary: '{{ .ProjectName }}_v{{ .Version }}'
+archives:
+- format: zip
+ name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
+checksum:
+ name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
+ algorithm: sha256
+changelog:
+ skip: false
+ sort: asc
diff --git a/Dockerfile.jenkins b/Dockerfile.jenkins
new file mode 100644
index 0000000..4c3f1c6
--- /dev/null
+++ b/Dockerfile.jenkins
@@ -0,0 +1,4 @@
+FROM jenkins/jenkins:lts
+COPY configs/jenkins/plugins.txt /usr/share/jenkins/ref/plugins.txt
+COPY configs/jenkins/jenkins.yaml $JENKINS_HOME/jenkins.yaml
+RUN /usr/local/bin/install-plugins.sh < /usr/share/jenkins/ref/plugins.txt
diff --git a/Dockerfile.vault b/Dockerfile.vault
new file mode 100644
index 0000000..4b8e3aa
--- /dev/null
+++ b/Dockerfile.vault
@@ -0,0 +1,8 @@
+FROM golang
+WORKDIR /tmp/build
+COPY . .
+RUN GOOS=linux; go mod tidy && \
+ go build -ldflags="-s -w" -o vault-plugin-secrets-jenkins
+
+FROM vault
+COPY --from=0 --chown=vault /tmp/build/vault-plugin-secrets-jenkins /vault/plugins/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6bd3fc3
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,61 @@
+GOARCH = amd64
+
+UNAME = $(shell uname -s)
+
+ifndef OS
+ ifeq ($(UNAME), Linux)
+ OS = linux
+ else ifeq ($(UNAME), Darwin)
+ OS = darwin
+ endif
+endif
+
+.DEFAULT_GOAL := all
+
+all: fmt build start
+
+build:
+ GOOS="$(OS)" GOARCH="$(GOARCH)" go build -o vault/plugins/vault-plugin-secrets-jenkins
+ chmod 755 vault/plugins/*
+
+start:
+ vault server -dev -dev-root-token-id=root -dev-plugin-dir=./vault/plugins
+
+enable:
+ vault secrets enable -path=jenkins vault-plugin-secrets-jenkins
+
+clean:
+ rm -f ./vault/plugins/vault-plugin-secrets-jenkins
+
+fmt:
+ go fmt $$(go list ./...)
+
+lint:
+ golangci-lint run -v
+
+jenkins:
+ docker rm -f vault-jenkins
+ docker build -t vault-jenkins -f Dockerfile.jenkins .
+ docker run --name vault-jenkins -d --rm -p 8080:8080 vault-jenkins
+
+test: jenkins
+ sleep 15
+ go test -v ./...
+
+set-vault-var:
+ export VAULT_ADDR="http://localhost:8200"
+
+enable-plugin: build
+ vault secrets enable vault-plugin-secrets-jenkins || exit 0
+ vault write sys/plugins/catalog/jenkins \
+ sha_256="$$(shasum -a 256 ./vault/plugins/vault-plugin-secrets-jenkins | cut -d " " -f1)" \
+ command="vault-plugin-secrets-jenkins"
+ vault write vault-plugin-secrets-jenkins/config url=http://localhost:8080 username=admin password=admin
+
+token: set-vault-var enable-plugin
+ vault read vault-plugin-secrets-jenkins/tokens/mytoken ttl=30
+
+user: set-vault-var enable-plugin
+ vault write vault-plugin-secrets-jenkins/users/myuser ttl=45 password=testpass fullname=fullname email=email@email.com
+
+.PHONY: build clean fmt start enable
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..dc2539d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,303 @@
+# vault-plugin-secrets-jenkins
+
+![Build Status](https://github.com/circa10a/vault-plugin-secrets-jenkins/workflows/release/badge.svg)
+[![PkgGoDev](https://pkg.go.dev/badge/github.com/circa10a/vault-plugin-secrets-jenkins)](https://pkg.go.dev/github.com/circa10a/vault-plugin-secrets-jenkins?tab=overview)
+[![Go Report Card](https://goreportcard.com/badge/github.com/circa10a/vault-plugin-secrets-jenkins)](https://goreportcard.com/report/github.com/circa10a/vault-plugin-secrets-jenkins)
+
+
+
+This is a backend plugin to be used with [Hashicorp Vault](https://www.github.com/hashicorp/vault).
+This plugin generates ephemeral Jenkins Users and API tokens.
+
+- [vault-plugin-secrets-jenkins](#vault-plugin-secrets-jenkins)
+ - [Quick Links](#quick-links)
+ - [Usage](#usage)
+ - [Enable plugin](#enable-plugin)
+ - [Configure Plugin](#configure-plugin)
+ - [Root User Validation](#root-user-validation)
+ - [Creating API tokens for configured user](#creating-api-tokens-for-configured-user)
+ - [Set default token TTL](#set-default-token-ttl)
+ - [Create a token](#create-a-token)
+ - [Specifiying a TTL per token](#specifiying-a-ttl-per-token)
+ - [Parsing a token value from Vault response](#parsing-a-token-value-from-vault-response)
+ - [List all active token leases](#list-all-active-token-leases)
+ - [Revoking all tokens for configured user](#revoking-all-tokens-for-configured-user)
+ - [Managing ephemeral users](#managing-ephemeral-users)
+ - [Create a user](#create-a-user)
+ - [Specifiying a TTL per user](#specifiying-a-ttl-per-user)
+ - [List all active users](#list-all-active-users)
+ - [Revoking a User](#revoking-a-user)
+ - [Revoking all users](#revoking-all-users)
+ - [Developing](#developing)
+ - [Get Plugin](#get-plugin)
+ - [Build plugin and start Vault](#build-plugin-and-start-vault)
+ - [Enable and configure plugin](#enable-and-configure-plugin)
+ - [Start Jenkins + Vault](#start-jenkins---vault)
+ - [Tests](#tests)
+ - [Create example credentials](#create-example-credentials)
+
+## Quick Links
+
+- [Vault Website](https://www.vaultproject.io)
+- [Jenkins API Tokens](https://www.jenkins.io/doc/book/system-administration/authenticating-scripted-clients/)
+
+## Usage
+
+This is a [Vault plugin](https://www.vaultproject.io/docs/internals/plugins.html)
+and is meant to work with Vault. This guide assumes you have already installed Vault
+and have a basic understanding of how Vault works. Otherwise, first read this guide on
+how to [get started with Vault](https://www.vaultproject.io/intro/getting-started/install.html).
+
+If you are just interested in using this plugin with Vault, you will need to install it by downloading the appropriate architecture from the [releases page](https://github.com/circa10a/vault-plugin-secrets-jenkins/releases) and placing it in the plugins directory. [Hashicorp Vault plugin documentation can be found here](https://www.vaultproject.io/docs/internals/plugins#plugin-directory).
+
+## Enable plugin
+
+```shell
+vault secrets enable -path=jenkins vault-plugin-secrets-jenkins
+Success! Enabled the vault-plugin-secrets-jenkins secrets engine at: jenkins/
+```
+
+## Configure Plugin
+
+The plugin expects a minimum configuration of a "root" user to create users and API tokens with. You can configure the plugin by writing the `/config` endpoint like so:
+
+```shell
+vault write jenkins/config url=http://localhost:8080 username=admin password=admin
+Success! Data written to: jenkins/config
+```
+
+### Root User Validation
+
+By default, the plugin will attempt to connect to the configured jenkins instance to ensure connectivity and authentication is working properly. To disable this functionality, simply pass the `validate=false` parameter like so:
+
+```shell
+vault write jenkins/config url=http://localhost:8080 username=admin password=fake validate=false
+Success! Data written to: jenkins/config
+```
+
+## Creating API tokens for configured user
+
+### Set default token TTL
+
+You may first want to setup a default TTL on all tokens created, you can do so by writing to the `/tokens/tune` endpoint of the plugin like so, otherwise the system default of `768h` (32 days) is used:
+
+```shell
+vault write sys/mounts/jenkins/tokenstune default_lease_ttl=5m
+Success! Data written to: sys/mounts/jenkins/tokens/tune
+```
+
+Tokens will automatically be revoked and deleted from Jenkins after the TTL has expired.
+
+### Create a token
+
+A token with a lease is generated by using a `read` operation on the `tokens/` endpoint:
+
+```shell
+vault read jenkins/tokens/mytoken
+Key Value
+--- -----
+lease_id jenkins/tokens/mytoken/fJ57afQZMyXDcJnm74BgLLt8
+lease_duration 5m
+lease_renewable true
+token 1184cb7b22c404efa1c293e9841b66f345
+token_id 1c2864f3-4108-4417-807a-358357bc8432
+token_name mytoken
+```
+
+The `token` value is what is to be used to authenticate with Jenkins as a subsitution for the user's password.
+
+:warning: **The token is not stored in Vault and will only be viewable from the first response. The token will not be able to be accessed again.** :warning:
+
+#### Specifiying a TTL per token
+
+You can specify the TTL for an individual token by supplying the `ttl=` parameter like so:
+
+```shell
+vault read jenkins/tokens/mytoken ttl=2m
+Key Value
+--- -----
+lease_id jenkins/tokens/mytoken/i81VB5RmXJCQdMdUUuwngTJI
+lease_duration 2m
+lease_renewable true
+token 1185adcc9c996fd9b394a520ca8e0c6024
+token_id a7ffa97f-032a-40c1-b9d9-e14c7a7dbc12
+token_name mytoken
+```
+
+#### Parsing a token value from Vault response
+
+**HTTP**:
+
+```shell
+curl -s -H "X-Vault-Token: " http://localhost:8200/v1/jenkins/tokens/mytoken | jq '.data.token'
+"119630cd3df88834e6b8000983529afcde"
+```
+
+**CLI**:
+
+```shell
+vault read jenkins/tokens/mytoken -format=json | jq -r '.data.token'
+119e4f728a738a1ca4e2c65329a5ebdba9
+```
+
+### List all active token leases
+
+You can view all of the all active Jenkins API token leases that Vault is managing:
+
+```shell
+vault list sys/leases/lookup/jenkins/tokens/mytoken
+Keys
+----
+Yvk37n1SCCfovcvk9YswCm7m
+rFbQIvo7mGUMbumYIplTewbX
+xlY32KoMTuS54rgAPnK2QvjR
+```
+
+### Revoking all tokens for configured user
+
+You can revoke all Vault managed tokens by revoking all leases under the `/jenkins/tokens` mount:
+
+```shell
+vault lease revoke -prefix=true jenkins/tokens/
+```
+
+## Managing ephemeral users
+
+This plugin allows you to create local Jenkins users with leases. The recommended method for controlling the permissions for these users is to use the [matrix authorization strategy plugin](https://plugins.jenkins.io/matrix-auth/) and have a default permission set for authenticated users:
+
+![alt text](/images/permissions.png)
+
+### Create a user
+
+A user with a lease is generated by using a `write` operation on the `/users/` endpoint:
+
+```shell
+vault write jenkins/users/myuser password=password fullname="Jenkins the Butler" email=email@example.com
+Key Value
+--- -----
+lease_id jenkins/users/myuser/hTGbJhDFbAQpALv1FjJyJ4vz
+lease_duration 5m
+lease_renewable true
+email email@example.com
+fullname Jenkins the Butler
+username myuser
+```
+
+Once the user is created, you can follow the same steps above to create API tokens for the new user if you prefer.
+
+Once a user is created and exists in Vault, writes to the same user endpoint will fail since it already exists. Once the lease has expired, the same username endpoint can be written to.
+
+:warning: **The password is not stored in Vault and will not accessible within Vault itself.** :warning:
+
+#### Specifiying a TTL per user
+
+You can specify the TTL for an individual supplying by supplying the `ttl=` parameter like so:
+
+```shell
+vault write jenkins/users/myuser password=password fullname="Jenkins the Butler" email=email@example.com ttl=1m
+Key Value
+--- -----
+lease_id jenkins/users/myuser/hBUoPCrwAySlmQuMoGEiZtUF
+lease_duration 1m
+lease_renewable true
+email email@example.com
+fullname Jenkins the Butler
+username myuser
+```
+
+### List all active users
+
+You can view all of the all active Jenkins Users that Vault is managing by listing the `/users/` endpoint:
+
+```shell
+❯ vault list jenkins/users/
+Keys
+----
+myuser
+```
+
+### Revoking a User
+
+You can revoke an individual Jenkins user by revoking the user name inder the `/users/` endpoint:
+
+```shell
+vault lease revoke jenkins/users/myuser
+All revocation operations queued successfully!
+```
+
+### Revoking all users
+
+You can revoke all Vault managed Jenkins users by revoking all users under the `/jenkins/users` mount:
+
+```shell
+vault lease revoke -prefix=true jenkins/users/
+```
+
+## Developing
+
+If you wish to work on this plugin, you'll first need [Go](https://www.golang.org)
+installed on your machine (whichever version is required by Vault) as well as [docker](https://docs.docker.com/get-docker/) to run Jenkins.
+
+### Get Plugin
+
+Clone this repository:
+
+```shell
+git clone https://github.com/circa10a/vault-plugin-secrets-jenkins.git
+```
+
+### Build plugin and start Vault
+
+Once the server is started, register the plugin in the Vault server's [plugin catalog](https://www.vaultproject.io/docs/internals/plugins.html#plugin-catalog):
+
+```sh
+make all
+```
+
+### Enable and configure plugin
+
+For configuration to work, jenkins will need to be running (via docker):
+
+```shell
+make jenkins
+```
+
+Then configure the plugin:
+
+```shell
+make enable-plugin
+```
+
+### Start Jenkins + Vault
+
+```shell
+docker-compose up
+```
+
+### Tests
+
+Jenkins needs to be running for the tests to execute successfully:
+
+```shell
+make jenkins
+```
+
+To run the integration tests:
+
+```shell
+make test
+```
+
+#### Create example credentials
+
+Create a token:
+
+```shell
+make token
+```
+
+Create a user:
+
+```shell
+make user
+```
diff --git a/configs/jenkins/jenkins.yaml b/configs/jenkins/jenkins.yaml
new file mode 100644
index 0000000..e52e16d
--- /dev/null
+++ b/configs/jenkins/jenkins.yaml
@@ -0,0 +1,11 @@
+jenkins:
+ authorizationStrategy:
+ loggedInUsersCanDoAnything:
+ allowAnonymousRead: false
+ securityRealm:
+ local:
+ allowsSignup: false
+ users:
+ - id: admin
+ name: admin
+ password: admin
diff --git a/configs/jenkins/plugins.txt b/configs/jenkins/plugins.txt
new file mode 100644
index 0000000..5f358ad
--- /dev/null
+++ b/configs/jenkins/plugins.txt
@@ -0,0 +1 @@
+configuration-as-code
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..b8e6bad
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,23 @@
+version: '3'
+
+services:
+ jenkins:
+ container_name: jenkins
+ build:
+ context: .
+ dockerfile: Dockerfile.jenkins
+ ports:
+ - 8080:8080
+
+ vault:
+ container_name: vault
+ build:
+ context: .
+ dockerfile: Dockerfile.vault
+ ports:
+ - 8200:8200
+ command: server -dev -dev-root-token-id=root -dev-plugin-dir=/vault/plugins
+ depends_on:
+ - jenkins
+ volumes:
+ - ./vault/plugins:/vault/plugins
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..ad44bda
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,15 @@
+module github.com/circa10a/vault-plugin-secrets-jenkins
+
+go 1.16
+
+require (
+ github.com/circa10a/gojenkins v1.2.0 // indirect
+ github.com/google/uuid v1.3.0
+ github.com/hashicorp-demoapp/hashicups-client-go v0.0.0-20210721190446-1df90c457bd4
+ github.com/hashicorp/go-hclog v0.16.2
+ github.com/hashicorp/vault-testing-stepwise v0.1.1
+ github.com/hashicorp/vault/api v1.1.1
+ github.com/hashicorp/vault/sdk v0.2.1
+ github.com/stretchr/testify v1.7.0
+ golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..f8d7e36
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,434 @@
+bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
+github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
+github.com/Microsoft/hcsshim v0.8.9 h1:VrfodqvztU8YSOvygU+DN1BGaSGxmrNfqOv5oOuX2Bk=
+github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs=
+github.com/armon/go-metrics v0.3.3 h1:a9F4rlj7EWWrbj7BYw8J8+x+ZZkJeqzNyRk8hdPF+ro=
+github.com/armon/go-metrics v0.3.3/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
+github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
+github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/circa10a/gojenkins v1.2.0 h1:rPIm6QAQSNbogTu1XvUdR04guKljLI58NJ06I5Nzbi4=
+github.com/circa10a/gojenkins v1.2.0/go.mod h1:/CWLrIDKkcPZupw9Im0H4K68rg8ErTUT1eALO55o0D8=
+github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
+github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
+github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.4 h1:3o0smo5SKY7H6AJCmJhsnCjR2/V2T8VmiHt7seN2/kI=
+github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20200709052629-daa8e1ccc0bc h1:lDK/G7OlwUnJW3O6nv/8M89bMupV6FuLK6FXmC3ueWc=
+github.com/containerd/continuity v0.0.0-20200709052629-daa8e1ccc0bc/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
+github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
+github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
+github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
+github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
+github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v1.4.2-0.20200319182547-c7ad2b866182 h1:Caj/qGJ9KyulC1WSksyPgp7r8+DKgTGfU39lmb2C5MQ=
+github.com/docker/docker v1.4.2-0.20200319182547-c7ad2b866182/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
+github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE=
+github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
+github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
+github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
+github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/hashicorp-demoapp/hashicups-client-go v0.0.0-20210721190446-1df90c457bd4 h1:Rt8GW22FYXFLaY9cnfBfPSg9kAngQbagHm31wNoSbBU=
+github.com/hashicorp-demoapp/hashicups-client-go v0.0.0-20210721190446-1df90c457bd4/go.mod h1:fJF8CZhWlImByx49t7RZvuoxskStDwqIWi5/GOSJqGI=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
+github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs=
+github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc=
+github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-kms-wrapping/entropy v0.1.0 h1:xuTi5ZwjimfpvpL09jDE71smCBRpnF5xfo871BSX4gs=
+github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE=
+github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
+github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-retryablehttp v0.6.2/go.mod h1:gEx6HMUGxYYhJScX7W1Il64m6cc2C1mDaW3NQ9sY1FY=
+github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM=
+github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
+github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
+github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
+github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
+github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk=
+github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/vault-testing-stepwise v0.1.1 h1:jByWPXZATbuI8+zWWI0T32jdlkJ1V7uHcrDC2GWNC40=
+github.com/hashicorp/vault-testing-stepwise v0.1.1/go.mod h1:3vUYn6D0ZadvstNO3YQQlIcp7u1a19MdoOC0NQ0yaOE=
+github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f/go.mod h1:euTFbi2YJgwcju3imEt919lhJKF68nN1cQPq3aA+kBE=
+github.com/hashicorp/vault/api v1.1.1 h1:907ld+Z9cALyvbZK2qUX9cLwvSaEQsMVQB3x2KE8+AI=
+github.com/hashicorp/vault/api v1.1.1/go.mod h1:29UXcn/1cLOPHQNMWA7bCz2By4PSd0VKPAydKXS5yN0=
+github.com/hashicorp/vault/sdk v0.1.14-0.20200519221530-14615acda45f/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
+github.com/hashicorp/vault/sdk v0.2.1 h1:S4O6Iv/dyKlE9AUTXGa7VOvZmsCvg36toPKgV4f2P4M=
+github.com/hashicorp/vault/sdk v0.2.1/go.mod h1:WfUiO1vYzfBkz1TmoE4ZGU7HD0T0Cl/rZwaxjBkgN4U=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
+github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
+github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
+github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
+github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
+github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
+github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
+github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
+github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
+github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
+golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
+golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210924151903-3ad01bbaa167/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
+golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
+golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c h1:IGkKhmfzcztjm6gYkykvu/NiS8kaqbCWAEWWAyf8J5U=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
+gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
+gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/images/jenkins-vault.png b/images/jenkins-vault.png
new file mode 100644
index 0000000..018c041
--- /dev/null
+++ b/images/jenkins-vault.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ddea7583cdd63b451185b15498638914cb9c8ba30877feae4047c25edb0f8268
+size 190139
diff --git a/images/permissions.png b/images/permissions.png
new file mode 100644
index 0000000..96b63dc
--- /dev/null
+++ b/images/permissions.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:26053f3e0870575553683dc6f2c4d060ad9eab109fd97c8bd2e2682d3ba9538b
+size 148135
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..315195d
--- /dev/null
+++ b/main.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+ "os"
+
+ jenkinssecretsengine "github.com/circa10a/vault-plugin-secrets-jenkins/plugin"
+ "github.com/hashicorp/go-hclog"
+ "github.com/hashicorp/vault/api"
+ "github.com/hashicorp/vault/sdk/plugin"
+)
+
+func main() {
+ apiClientMeta := &api.PluginAPIClientMeta{}
+ flags := apiClientMeta.FlagSet()
+ // nolint
+ flags.Parse(os.Args[1:])
+
+ tlsConfig := apiClientMeta.GetTLSConfig()
+ tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig)
+ logger := hclog.New(&hclog.LoggerOptions{})
+
+ err := plugin.Serve(&plugin.ServeOpts{
+ BackendFactoryFunc: jenkinssecretsengine.Factory,
+ TLSProviderFunc: tlsProviderFunc,
+ })
+ if err != nil {
+ logger.Error("plugin shutting down", "error", err)
+ os.Exit(1)
+ }
+}
diff --git a/plugin/backend.go b/plugin/backend.go
new file mode 100644
index 0000000..e9b9410
--- /dev/null
+++ b/plugin/backend.go
@@ -0,0 +1,116 @@
+package jenkinssecretsengine
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "sync"
+
+ "github.com/hashicorp/vault/sdk/framework"
+ "github.com/hashicorp/vault/sdk/logical"
+)
+
+func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
+ b := backend()
+ if err := b.Setup(ctx, conf); err != nil {
+ return nil, err
+ }
+ return b, nil
+}
+
+// jenkinsBackend defines an object that
+// extends the Vault backend and stores the
+// target API's client.
+type jenkinsBackend struct {
+ *framework.Backend
+ client *jenkinsClient
+ lock sync.RWMutex
+}
+
+// backend defines the target API backend
+// for Vault. It must include each path
+// and the secrets it will store.
+func backend() *jenkinsBackend {
+ var b = jenkinsBackend{}
+
+ b.Backend = &framework.Backend{
+ Help: strings.TrimSpace(backendHelp),
+ PathsSpecial: &logical.Paths{
+ LocalStorage: []string{},
+ SealWrapStorage: []string{
+ configPrefix,
+ fmt.Sprintf("%s/*", usersPrefix),
+ fmt.Sprintf("%s/*", tokensPrefix),
+ },
+ },
+ Paths: framework.PathAppend(
+ []*framework.Path{
+ pathConfig(&b),
+ },
+ pathTokens(&b),
+ pathUsers(&b),
+ ),
+ Secrets: []*framework.Secret{
+ b.jenkinsUser(),
+ b.jenkinsToken(),
+ },
+ BackendType: logical.TypeLogical,
+ Invalidate: b.invalidate,
+ }
+ return &b
+}
+
+// reset clears any client configuration for a new
+// backend to be configured
+func (b *jenkinsBackend) reset() {
+ b.lock.Lock()
+ defer b.lock.Unlock()
+ b.client = nil
+}
+
+// invalidate clears an existing client configuration in
+// the backend
+func (b *jenkinsBackend) invalidate(ctx context.Context, key string) {
+ if key == configPrefix {
+ b.reset()
+ }
+}
+
+// getClient locks the backend as it configures and creates a
+// a new client for the Jenkins API
+func (b *jenkinsBackend) getClient(ctx context.Context, s logical.Storage) (*jenkinsClient, error) {
+ b.lock.RLock()
+ unlockFunc := b.lock.RUnlock
+ defer func() { unlockFunc() }()
+
+ if b.client != nil {
+ return b.client, nil
+ }
+
+ b.lock.RUnlock()
+ b.lock.Lock()
+ unlockFunc = b.lock.Unlock
+
+ config, err := getConfig(ctx, s)
+ if err != nil {
+ return nil, err
+ }
+
+ if config == nil {
+ config = new(jenkinsConfig)
+ }
+
+ b.client, err = newClient(config)
+ if err != nil {
+ return nil, err
+ }
+
+ return b.client, nil
+}
+
+// backendHelp should contain help information for the backend
+const backendHelp = `
+The Jenkins secrets backend dynamically generates user tokens.
+After mounting this backend, credentials to manage Jenkins user tokens
+must be configured with the "config/" endpoints.
+`
diff --git a/plugin/backend_test.go b/plugin/backend_test.go
new file mode 100644
index 0000000..824939f
--- /dev/null
+++ b/plugin/backend_test.go
@@ -0,0 +1,76 @@
+package jenkinssecretsengine
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/hashicorp/go-hclog"
+ "github.com/hashicorp/vault/sdk/logical"
+ "github.com/stretchr/testify/require"
+)
+
+const (
+ envVarJenkinsUsername = "TEST_JENKINS_USERNAME"
+ envVarJenkinsPassword = "TEST_JENKINS_PASSWORD"
+ envVarJenkinsURL = "TEST_JENKINS_URL"
+ testTokenName = "test-user-token"
+)
+
+var testUsername, testPassword, testURL string
+
+func setTestVars() {
+ if val, ok := os.LookupEnv(envVarJenkinsUsername); ok {
+ testUsername = val
+ } else {
+ testUsername = "admin"
+ }
+ if val, ok := os.LookupEnv(envVarJenkinsPassword); ok {
+ testPassword = val
+ } else {
+ testPassword = "admin"
+ }
+ if val, ok := os.LookupEnv(envVarJenkinsURL); ok {
+ testURL = val
+ } else {
+ testURL = "http://localhost:8080"
+ }
+}
+
+// getTestBackend will help you construct a test backend object.
+func getTestBackend(tb testing.TB) (*jenkinsBackend, logical.Storage) {
+ // Have ability to override test vars while also having defaults
+ setTestVars()
+ tb.Helper()
+
+ config := logical.TestBackendConfig()
+ config.StorageView = new(logical.InmemStorage)
+ config.Logger = hclog.NewNullLogger()
+ config.System = logical.TestSystemView()
+
+ b, err := Factory(context.Background(), config)
+ if err != nil {
+ tb.Fatal(err)
+ }
+
+ return b.(*jenkinsBackend), config.StorageView
+}
+
+// AddConfig adds the configuration to the test backend.
+// Make sure data includes all of the configuration
+// attributes you need and the `config` path!
+func AddTestConfig(t *testing.T, b logical.Backend, s logical.Storage) {
+ req := &logical.Request{
+ Operation: logical.CreateOperation,
+ Path: configPrefix,
+ Storage: s,
+ Data: map[string]interface{}{
+ "username": testUsername,
+ "password": testPassword,
+ "url": testURL,
+ },
+ }
+ resp, err := b.HandleRequest(context.Background(), req)
+ require.Nil(t, resp)
+ require.Nil(t, err)
+}
diff --git a/plugin/client.go b/plugin/client.go
new file mode 100644
index 0000000..aa38855
--- /dev/null
+++ b/plugin/client.go
@@ -0,0 +1,36 @@
+package jenkinssecretsengine
+
+import (
+ "errors"
+
+ "github.com/circa10a/gojenkins"
+)
+
+// jenkinsClient creates an object storing
+// the client.
+type jenkinsClient struct {
+ *gojenkins.Jenkins
+}
+
+// newClient creates a new client to access Jenkins
+func newClient(config *jenkinsConfig) (*jenkinsClient, error) {
+ if config == nil {
+ return nil, errors.New("jenkins configuration was nil in /config")
+ }
+
+ if config.Username == "" {
+ return nil, errors.New("jenkins username was not defined in /config")
+ }
+
+ if config.Password == "" {
+ return nil, errors.New("jenkins password was not defined in /config")
+ }
+
+ if config.URL == "" {
+ return nil, errors.New("jenkins URL was not defined in /config")
+ }
+
+ jenkins := gojenkins.CreateJenkins(nil, config.URL, config.Username, config.Password)
+
+ return &jenkinsClient{jenkins}, nil
+}
diff --git a/plugin/jenkins_token.go b/plugin/jenkins_token.go
new file mode 100644
index 0000000..2cf2edc
--- /dev/null
+++ b/plugin/jenkins_token.go
@@ -0,0 +1,121 @@
+package jenkinssecretsengine
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/hashicorp/vault/sdk/framework"
+ "github.com/hashicorp/vault/sdk/logical"
+)
+
+const (
+ jenkinsTokenType = "jenkins_token"
+)
+
+// jenkinsToken defines a secret for the Jenkins token
+type jenkinsToken struct {
+ Token string `json:"token"`
+ TokenID string `json:"token_id"`
+ Name string `json:"token_name"`
+ TTL time.Duration `json:"ttl"`
+ MaxTTL time.Duration `json:"max_ttl"`
+}
+
+// toResponseData returns response data for a token
+func (token *jenkinsToken) toResponseData() map[string]interface{} {
+ respData := map[string]interface{}{
+ "token": token.Token,
+ "token_name": token.Name,
+ "token_id": token.TokenID,
+ }
+ return respData
+}
+
+// jenkinsToken defines an api token to store for a given user
+// and how it should be revoked or renewed.
+func (b *jenkinsBackend) jenkinsToken() *framework.Secret {
+ return &framework.Secret{
+ Type: jenkinsTokenType,
+ Fields: map[string]*framework.FieldSchema{
+ "token": {
+ Type: framework.TypeString,
+ Description: "Jenkins Token",
+ },
+ },
+ Revoke: b.tokenRevoke,
+ Renew: b.tokenRenew,
+ }
+}
+
+// tokenRevoke removes the token from the Vault storage API and calls the client to revoke the token
+func (b *jenkinsBackend) tokenRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ client, err := b.getClient(ctx, req.Storage)
+ if err != nil {
+ return nil, fmt.Errorf("error getting client: %w", err)
+ }
+
+ tokenID := ""
+ tokenIDRaw, ok := req.Secret.InternalData["token_id"]
+ if ok {
+ tokenID, ok = tokenIDRaw.(string)
+ if !ok {
+ return nil, fmt.Errorf("invalid value for token_id in secret internal data")
+ }
+ }
+
+ // Delete from Jenkins
+ if err := deleteToken(ctx, client, tokenID); err != nil {
+ return nil, fmt.Errorf("error revoking user token: %w", err)
+ }
+
+ return nil, nil
+}
+
+// tokenRenew renews the ttl time in vault
+func (b *jenkinsBackend) tokenRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ ttlRaw, ok := req.Secret.InternalData["ttl"]
+ if !ok {
+ return nil, fmt.Errorf("secret is missing ttl internal data")
+ }
+ maxTtlRaw, ok := req.Secret.InternalData["max_ttl"]
+ if !ok {
+ return nil, fmt.Errorf("secret is missing max_ttl internal data")
+ }
+
+ resp := &logical.Response{Secret: req.Secret}
+ ttl := time.Duration(ttlRaw.(float64)) * time.Second
+ maxTtl := time.Duration(maxTtlRaw.(float64)) * time.Second
+
+ if ttl > 0 {
+ resp.Secret.TTL = ttl
+ }
+ if maxTtl > 0 {
+ resp.Secret.MaxTTL = maxTtl
+ }
+
+ return resp, nil
+}
+
+// createToken calls the jenkins client to generate and return a new token
+func createToken(ctx context.Context, j *jenkinsClient, tokenName string) (*jenkinsToken, error) {
+ token, err := j.GenerateAPIToken(ctx, tokenName)
+ if err != nil {
+ return nil, fmt.Errorf("error creating jenkins token: %w", err)
+ }
+
+ return &jenkinsToken{
+ Token: token.Value,
+ TokenID: token.UUID,
+ }, nil
+}
+
+// deleteToken revokes the token
+func deleteToken(ctx context.Context, j *jenkinsClient, tokenID string) error {
+ err := j.RevokeAPIToken(ctx, tokenID)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/plugin/jenkins_user.go b/plugin/jenkins_user.go
new file mode 100644
index 0000000..638f9a5
--- /dev/null
+++ b/plugin/jenkins_user.go
@@ -0,0 +1,158 @@
+package jenkinssecretsengine
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/hashicorp/vault/sdk/framework"
+ "github.com/hashicorp/vault/sdk/logical"
+)
+
+const (
+ jenkinsUserType = "jenkins_user"
+)
+
+// jenkinsUser defines a user as secret
+type jenkinsUser struct {
+ Username string `json:"username"`
+ Password string `json:"password,omitempty"`
+ Fullname string `json:"fullname"`
+ Email string `json:"email"`
+ TTL time.Duration `json:"ttl"`
+ MaxTTL time.Duration `json:"max_ttl"`
+}
+
+// toResponseData returns response data for a user
+func (user *jenkinsUser) toResponseData() map[string]interface{} {
+ respData := map[string]interface{}{
+ "username": user.Username,
+ "fullname": user.Fullname,
+ "email": user.Email,
+ }
+ return respData
+}
+
+// jenkinsUser defines an a user in jenkins
+// and how it should be revoked or renewed.
+func (b *jenkinsBackend) jenkinsUser() *framework.Secret {
+ return &framework.Secret{
+ Type: jenkinsUserType,
+ Fields: map[string]*framework.FieldSchema{
+ "username": {
+ Type: framework.TypeString,
+ Description: "Jenkins User",
+ },
+ },
+ Revoke: b.userRevoke,
+ Renew: b.userRenew,
+ }
+}
+
+// userRevoke removes the user from the Vault storage API and calls the client to revoke the user
+func (b *jenkinsBackend) userRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ client, err := b.getClient(ctx, req.Storage)
+ if err != nil {
+ return nil, fmt.Errorf("error getting client: %w", err)
+ }
+
+ username := ""
+ usernameRaw, ok := req.Secret.InternalData["username"]
+ if ok {
+ username, ok = usernameRaw.(string)
+ if !ok {
+ return nil, fmt.Errorf("invalid value for username in secret internal data")
+ }
+ }
+
+ // Delete from Jenkins
+ if err := deleteUser(ctx, client, username); err != nil {
+ return nil, fmt.Errorf("error revoking user: %w", err)
+ }
+
+ // Delete from store
+ err = req.Storage.Delete(ctx, b.getUserPath(username))
+ if err != nil {
+ return nil, fmt.Errorf("error remove user from storage: %w", err)
+ }
+
+ return nil, nil
+}
+
+// userRenew renews the ttl time in vault
+func (b *jenkinsBackend) userRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ ttlRaw, ok := req.Secret.InternalData["ttl"]
+ if !ok {
+ return nil, fmt.Errorf("secret is missing ttl internal data")
+ }
+ maxTtlRaw, ok := req.Secret.InternalData["max_ttl"]
+ if !ok {
+ return nil, fmt.Errorf("secret is missing max_ttl internal data")
+ }
+
+ resp := &logical.Response{Secret: req.Secret}
+ ttl := time.Duration(ttlRaw.(float64)) * time.Second
+ maxTtl := time.Duration(maxTtlRaw.(float64)) * time.Second
+
+ if ttl > 0 {
+ resp.Secret.TTL = ttl
+ }
+ if maxTtl > 0 {
+ resp.Secret.MaxTTL = maxTtl
+ }
+
+ return resp, nil
+}
+
+// createUser calls the jenkins client to create and return a new user
+func createUser(ctx context.Context, j *jenkinsClient, username, password, fullname, email string) (*jenkinsUser, error) {
+ user, err := j.CreateUser(ctx, username, password, fullname, email)
+ if err != nil {
+ return nil, fmt.Errorf("error creating jenkins user: %w", err)
+ }
+
+ return &jenkinsUser{
+ Username: user.UserName,
+ Password: password,
+ Fullname: user.FullName,
+ Email: user.Email,
+ }, nil
+}
+
+// deleteUser revokes the user
+func deleteUser(ctx context.Context, j *jenkinsClient, username string) error {
+ err := j.DeleteUser(ctx, username)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// getUser gets the user from the Vault storage API
+func (b *jenkinsBackend) getUserFromStorage(ctx context.Context, s logical.Storage, username string) (*jenkinsUser, error) {
+ if username == "" {
+ return nil, fmt.Errorf("missing username")
+ }
+
+ entry, err := s.Get(ctx, b.getUserPath(username))
+ if err != nil {
+ return nil, err
+ }
+
+ if entry == nil {
+ return nil, nil
+ }
+
+ var user jenkinsUser
+
+ if err := entry.DecodeJSON(&user); err != nil {
+ return nil, err
+ }
+ return &user, nil
+}
+
+// getUserPath returns the user storage path such as /users/user
+func (b *jenkinsBackend) getUserPath(username string) string {
+ return fmt.Sprintf("%s/%s", usersPrefix, username)
+}
diff --git a/plugin/path_config.go b/plugin/path_config.go
new file mode 100644
index 0000000..6d08e2e
--- /dev/null
+++ b/plugin/path_config.go
@@ -0,0 +1,215 @@
+package jenkinssecretsengine
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/hashicorp/vault/sdk/framework"
+ "github.com/hashicorp/vault/sdk/logical"
+)
+
+const (
+ configPrefix = "config"
+)
+
+// jenkinsConfig includes the minimum configuration
+// required to instantiate a new jenkins client.
+type jenkinsConfig struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ URL string `json:"url"`
+ ValidateClient bool `json:"validate,omitempty"`
+}
+
+// pathConfig extends the Vault API with a `/config`
+// endpoint for the backend. You can choose whether
+// or not certain attributes should be displayed,
+// required, and named. For example, password
+// is marked as sensitive and will not be output
+// when you read the configuration.
+func pathConfig(b *jenkinsBackend) *framework.Path {
+ return &framework.Path{
+ Pattern: configPrefix,
+ Fields: map[string]*framework.FieldSchema{
+ "username": {
+ Type: framework.TypeString,
+ Description: "The username to access Jenkins",
+ Required: true,
+ DisplayAttrs: &framework.DisplayAttributes{
+ Name: "Username",
+ Sensitive: false,
+ },
+ },
+ "password": {
+ Type: framework.TypeString,
+ Description: "The user's password to access Jenkins",
+ Required: true,
+ DisplayAttrs: &framework.DisplayAttributes{
+ Name: "Password",
+ Sensitive: true,
+ },
+ },
+ "url": {
+ Type: framework.TypeString,
+ Description: "The Jenkins URL",
+ Required: true,
+ DisplayAttrs: &framework.DisplayAttributes{
+ Name: "URL",
+ Sensitive: false,
+ },
+ },
+ "validate": {
+ Type: framework.TypeBool,
+ Description: fmt.Sprintf("The ensure jenkins client can connect and authenticate on init when writing to /%s mount", configPrefix),
+ Required: false,
+ Default: true,
+ },
+ },
+ Operations: map[logical.Operation]framework.OperationHandler{
+ logical.ReadOperation: &framework.PathOperation{
+ Callback: b.pathConfigRead,
+ },
+ logical.CreateOperation: &framework.PathOperation{
+ Callback: b.pathConfigWrite,
+ },
+ logical.UpdateOperation: &framework.PathOperation{
+ Callback: b.pathConfigWrite,
+ },
+ logical.DeleteOperation: &framework.PathOperation{
+ Callback: b.pathConfigDelete,
+ },
+ },
+ ExistenceCheck: b.pathConfigExistenceCheck,
+ HelpSynopsis: pathConfigHelpSyn,
+ HelpDescription: pathConfigHelpDescription,
+ }
+}
+
+// pathConfigExistenceCheck verifies if the configuration exists.
+func (b *jenkinsBackend) pathConfigExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
+ out, err := req.Storage.Get(ctx, req.Path)
+ if err != nil {
+ return false, fmt.Errorf("existence check failed: %w", err)
+ }
+
+ return out != nil, nil
+}
+
+// pathConfigRead reads the configuration and outputs non-sensitive information.
+func (b *jenkinsBackend) pathConfigRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
+ config, err := getConfig(ctx, req.Storage)
+ if err != nil {
+ return nil, err
+ }
+
+ return &logical.Response{
+ Data: map[string]interface{}{
+ "username": config.Username,
+ "url": config.URL,
+ },
+ }, nil
+}
+
+// pathConfigWrite updates the configuration for the backend
+func (b *jenkinsBackend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
+ config, err := getConfig(ctx, req.Storage)
+ if err != nil {
+ return nil, err
+ }
+
+ createOperation := (req.Operation == logical.CreateOperation)
+
+ if config == nil {
+ if !createOperation {
+ return nil, errors.New("config not found during update operation")
+ }
+ config = new(jenkinsConfig)
+ }
+
+ if username, ok := data.GetOk("username"); ok {
+ config.Username = username.(string)
+ } else if !ok && createOperation {
+ return nil, fmt.Errorf("missing username in configuration")
+ }
+
+ if url, ok := data.GetOk("url"); ok {
+ config.URL = url.(string)
+ } else if !ok && createOperation {
+ return nil, fmt.Errorf("missing url in configuration")
+ }
+
+ if password, ok := data.GetOk("password"); ok {
+ config.Password = password.(string)
+ } else if !ok && createOperation {
+ return nil, fmt.Errorf("missing password in configuration")
+ }
+
+ entry, err := logical.StorageEntryJSON(configPrefix, config)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := req.Storage.Put(ctx, entry); err != nil {
+ return nil, err
+ }
+
+ // If parameters is set (true by default), ensure jenkins client config works
+ validate := data.Get("validate").(bool)
+ if validate {
+ client, err := b.getClient(ctx, req.Storage)
+ if err != nil {
+ return logical.ErrorResponse(err.Error()), err
+ }
+ _, err = client.Init(ctx)
+ if err != nil {
+ // reset the client so the next invocation will pick up the new configuration
+ b.reset()
+ return logical.ErrorResponse(err.Error()), err
+ }
+ }
+
+ // reset the client so the next invocation will pick up the new configuration
+ b.reset()
+
+ return nil, nil
+}
+
+// pathConfigDelete removes the configuration for the backend
+func (b *jenkinsBackend) pathConfigDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
+ err := req.Storage.Delete(ctx, configPrefix)
+
+ if err == nil {
+ b.reset()
+ }
+
+ return nil, err
+}
+
+func getConfig(ctx context.Context, s logical.Storage) (*jenkinsConfig, error) {
+ entry, err := s.Get(ctx, configPrefix)
+ if err != nil {
+ return nil, err
+ }
+
+ if entry == nil {
+ return nil, nil
+ }
+
+ config := new(jenkinsConfig)
+ if err := entry.DecodeJSON(&config); err != nil {
+ return nil, fmt.Errorf("error reading root configuration: %w", err)
+ }
+
+ // return the config, we are done
+ return config, nil
+}
+
+// pathConfigHelpSynopsis summarizes the help text for the configuration
+const pathConfigHelpSyn = `Configure the Jenkins backend.`
+
+// pathConfigHelpDescription describes the help text for the configuration
+const pathConfigHelpDescription = `
+The Jenkins secret backend requires credentials for managing
+ephemeral users and API tokens for the configured user.
+`
diff --git a/plugin/path_config_test.go b/plugin/path_config_test.go
new file mode 100644
index 0000000..98afffa
--- /dev/null
+++ b/plugin/path_config_test.go
@@ -0,0 +1,145 @@
+package jenkinssecretsengine
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/hashicorp/vault/sdk/logical"
+ "github.com/stretchr/testify/assert"
+)
+
+// TestConfig mocks the creation, read, update, and delete
+// of the backend configuration for Jenkins.
+func TestConfig(t *testing.T) {
+ b, reqStorage := getTestBackend(t)
+
+ t.Run("Test Configuration", func(t *testing.T) {
+ err := testConfigCreate(t, b, reqStorage, map[string]interface{}{
+ "username": testUsername,
+ "password": testPassword,
+ "url": testURL,
+ })
+ assert.NoError(t, err)
+
+ err = testConfigRead(t, b, reqStorage, map[string]interface{}{
+ "username": testUsername,
+ "url": testURL,
+ })
+ assert.NoError(t, err)
+
+ // Ensure we can update
+ err = testConfigUpdate(t, b, reqStorage, map[string]interface{}{
+ "username": testUsername,
+ "url": "http://localhost:8081",
+ "validate": false,
+ })
+ assert.NoError(t, err)
+
+ err = testConfigRead(t, b, reqStorage, map[string]interface{}{
+ "username": testUsername,
+ "url": "http://localhost:8081",
+ })
+ assert.NoError(t, err)
+
+ // Ensure we can update and validation works
+ err = testConfigUpdate(t, b, reqStorage, map[string]interface{}{
+ "username": testUsername,
+ "url": "http://localhost:8081",
+ "validate": true,
+ })
+ assert.Error(t, err)
+
+ err = testConfigDelete(t, b, reqStorage)
+ assert.NoError(t, err)
+ })
+}
+
+func testConfigDelete(t *testing.T, b logical.Backend, s logical.Storage) error {
+ resp, err := b.HandleRequest(context.Background(), &logical.Request{
+ Operation: logical.DeleteOperation,
+ Path: configPrefix,
+ Storage: s,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if resp != nil && resp.IsError() {
+ return resp.Error()
+ }
+ return nil
+}
+
+func testConfigCreate(t *testing.T, b logical.Backend, s logical.Storage, d map[string]interface{}) error {
+ resp, err := b.HandleRequest(context.Background(), &logical.Request{
+ Operation: logical.CreateOperation,
+ Path: configPrefix,
+ Data: d,
+ Storage: s,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if resp != nil && resp.IsError() {
+ return resp.Error()
+ }
+ return nil
+}
+
+func testConfigUpdate(t *testing.T, b logical.Backend, s logical.Storage, d map[string]interface{}) error {
+ resp, err := b.HandleRequest(context.Background(), &logical.Request{
+ Operation: logical.UpdateOperation,
+ Path: configPrefix,
+ Data: d,
+ Storage: s,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if resp != nil && resp.IsError() {
+ return resp.Error()
+ }
+ return nil
+}
+
+func testConfigRead(t *testing.T, b logical.Backend, s logical.Storage, expected map[string]interface{}) error {
+ resp, err := b.HandleRequest(context.Background(), &logical.Request{
+ Operation: logical.ReadOperation,
+ Path: configPrefix,
+ Storage: s,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if resp == nil && expected == nil {
+ return nil
+ }
+
+ if resp.IsError() {
+ return resp.Error()
+ }
+
+ if len(expected) != len(resp.Data) {
+ return fmt.Errorf("read data mismatch (expected %d values, got %d)", len(expected), len(resp.Data))
+ }
+
+ for k, expectedV := range expected {
+ actualV, ok := resp.Data[k]
+
+ if !ok {
+ return fmt.Errorf(`expected data["%s"] = %v but was not included in read output"`, k, expectedV)
+ } else if expectedV != actualV {
+ return fmt.Errorf(`expected data["%s"] = %v, instead got %v"`, k, expectedV, actualV)
+ }
+ }
+
+ return nil
+}
diff --git a/plugin/path_tokens.go b/plugin/path_tokens.go
new file mode 100644
index 0000000..dd35835
--- /dev/null
+++ b/plugin/path_tokens.go
@@ -0,0 +1,120 @@
+package jenkinssecretsengine
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/hashicorp/vault/sdk/framework"
+ "github.com/hashicorp/vault/sdk/logical"
+)
+
+const tokensPrefix = "tokens"
+
+// pathTokens extends the Vault API with a `/tokens`
+// endpoint for an api token.
+func pathTokens(b *jenkinsBackend) []*framework.Path {
+ return []*framework.Path{
+ {
+ Pattern: fmt.Sprintf("%s/%s", tokensPrefix, framework.GenericNameRegex("name")),
+ Fields: map[string]*framework.FieldSchema{
+ "ttl": {
+ Type: framework.TypeDurationSecond,
+ Description: "Default lease for generated token. If not set or set to 0, will use system default.",
+ Required: false,
+ },
+ "max_ttl": {
+ Type: framework.TypeDurationSecond,
+ Description: "Maximum time for token. If not set or set to 0, will use system default.",
+ Required: false,
+ },
+ },
+ Callbacks: map[logical.Operation]framework.OperationFunc{
+ logical.ReadOperation: b.pathTokensRead,
+ logical.UpdateOperation: b.pathTokensRead,
+ },
+ HelpSynopsis: pathTokensHelpSyn,
+ HelpDescription: pathTokensHelpDesc,
+ },
+ }
+}
+
+// pathTokensRead creates a new Jenkins token each time it is called if a user exists.
+func (b *jenkinsBackend) pathTokensRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ ttl := time.Duration(d.Get("ttl").(int)) * time.Second
+ maxTtl := time.Duration(d.Get("max_ttl").(int)) * time.Second
+ jenkinsTokenConfig := &jenkinsToken{
+ TTL: ttl,
+ MaxTTL: maxTtl,
+ }
+
+ return b.createUserToken(ctx, req, *jenkinsTokenConfig)
+}
+
+// createUserToken creates a new Jenkins token to store into the Vault backend, generates
+// a response with the secrets information, and checks the TTL and MaxTTL attributes.
+func (b *jenkinsBackend) createUserToken(ctx context.Context, req *logical.Request, jenkinsToken jenkinsToken) (*logical.Response, error) {
+ tokenName := strings.TrimPrefix(req.Path, fmt.Sprintf("%s/", tokensPrefix))
+ token, err := b.createToken(ctx, req.Storage, tokenName)
+ if err != nil {
+ return nil, err
+ }
+
+ // We won't store the token
+ // It's only available in the initial read response
+ token.Name = tokenName
+
+ // Need to store token ID to revoke later
+ internalData := map[string]interface{}{
+ "token_id": token.TokenID,
+ "token_name": tokenName,
+ "ttl": token.TTL,
+ "max_ttl": token.MaxTTL,
+ }
+
+ // Create secret with lease
+ resp := b.Secret(jenkinsTokenType).Response(token.toResponseData(), internalData)
+
+ if jenkinsToken.TTL > 0 {
+ resp.Secret.TTL = jenkinsToken.TTL
+ }
+ if jenkinsToken.MaxTTL > 0 {
+ resp.Secret.MaxTTL = jenkinsToken.MaxTTL
+ }
+
+ return resp, nil
+}
+
+// createToken uses the Jenkins client create a new token
+func (b *jenkinsBackend) createToken(ctx context.Context, s logical.Storage, tokenName string) (*jenkinsToken, error) {
+ client, err := b.getClient(ctx, s)
+ if err != nil {
+ return nil, err
+ }
+
+ var token *jenkinsToken
+
+ token, err = createToken(ctx, client, tokenName)
+ if err != nil {
+ return nil, fmt.Errorf("error creating Jenkins token: %w", err)
+ }
+
+ if token == nil {
+ return nil, errors.New("error creating Jenkins token")
+ }
+
+ return token, nil
+}
+
+const (
+ pathTokensHelpSyn = `
+Generate a Jenkins API token for the configured user.
+`
+
+ pathTokensHelpDesc = `
+This path generates a Jenkins API tokens
+for the user configured under the /config mount.
+`
+)
diff --git a/plugin/path_tokens_test.go b/plugin/path_tokens_test.go
new file mode 100644
index 0000000..62b1072
--- /dev/null
+++ b/plugin/path_tokens_test.go
@@ -0,0 +1,35 @@
+package jenkinssecretsengine
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/hashicorp/vault/sdk/logical"
+ "github.com/stretchr/testify/require"
+)
+
+// Test uses a mock backend to test creation of tokens
+func TestToken(t *testing.T) {
+ b, s := getTestBackend(t)
+ AddTestConfig(t, b, s)
+
+ t.Run("Create Token", func(t *testing.T) {
+ resp, err := testTokenRead(t, b, s)
+
+ require.Nil(t, err)
+ require.Nil(t, resp.Error())
+ require.NotNil(t, resp)
+ require.Equal(t, resp.Data["token_name"], testTokenName)
+ })
+}
+
+// Utility function to create a token by reading and return any errors
+func testTokenRead(t *testing.T, b *jenkinsBackend, s logical.Storage) (*logical.Response, error) {
+ t.Helper()
+ return b.HandleRequest(context.Background(), &logical.Request{
+ Operation: logical.ReadOperation,
+ Path: fmt.Sprintf("%s/%s", tokensPrefix, testTokenName),
+ Storage: s,
+ })
+}
diff --git a/plugin/path_users.go b/plugin/path_users.go
new file mode 100644
index 0000000..c229695
--- /dev/null
+++ b/plugin/path_users.go
@@ -0,0 +1,250 @@
+package jenkinssecretsengine
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/hashicorp/vault/sdk/framework"
+ "github.com/hashicorp/vault/sdk/logical"
+)
+
+const usersPrefix = "users"
+
+// pathUsers extends the Vault API with a `/user`
+// endpoint for a user.
+func pathUsers(b *jenkinsBackend) []*framework.Path {
+ return []*framework.Path{
+ {
+ Pattern: fmt.Sprintf("%s/%s", usersPrefix, framework.GenericNameRegex("name")),
+ Fields: map[string]*framework.FieldSchema{
+ "password": {
+ Type: framework.TypeString,
+ Description: "Password for the Jenkins user",
+ Required: true,
+ DisplayAttrs: &framework.DisplayAttributes{
+ Sensitive: true,
+ },
+ },
+ "fullname": {
+ Type: framework.TypeString,
+ Description: "Fullname for the Jenkins user",
+ Required: true,
+ },
+ "email": {
+ Type: framework.TypeString,
+ Description: "Email for the Jenkins user",
+ Required: true,
+ },
+ "ttl": {
+ Type: framework.TypeDurationSecond,
+ Description: "Default lease for a user. If not set or set to 0, will use system default.",
+ Required: false,
+ },
+ "max_ttl": {
+ Type: framework.TypeDurationSecond,
+ Description: "Maximum time for a user. If not set or set to 0, will use system default.",
+ Required: false,
+ },
+ },
+ Callbacks: map[logical.Operation]framework.OperationFunc{
+ logical.ReadOperation: b.pathUsersRead,
+ logical.CreateOperation: b.pathUsersWrite,
+ logical.UpdateOperation: b.pathUsersWrite,
+ logical.DeleteOperation: b.pathUsersDelete,
+ },
+ ExistenceCheck: b.pathUsersExistenceCheck,
+ HelpSynopsis: pathUsersHelpSyn,
+ HelpDescription: pathUsersHelpDesc,
+ },
+ {
+ Pattern: fmt.Sprintf("%s/?$", usersPrefix),
+ Operations: map[logical.Operation]framework.OperationHandler{
+ logical.ListOperation: &framework.PathOperation{
+ Callback: b.pathUsersList,
+ },
+ },
+ HelpSynopsis: pathUsersListHelpSyn,
+ HelpDescription: pathUsersListHelpDescription,
+ },
+ }
+}
+
+// pathUsersExistenceCheck verifies if a user exists.
+func (b *jenkinsBackend) pathUsersExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
+ out, err := req.Storage.Get(ctx, req.Path)
+ if err != nil {
+ return false, fmt.Errorf("existence check failed: %w", err)
+ }
+
+ return out != nil, nil
+}
+
+// pathUserList makes a request to Vault storage to retrieve a list of roles for the backend
+func (b *jenkinsBackend) pathUsersList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ entries, err := req.Storage.List(ctx, fmt.Sprintf("%s/", usersPrefix))
+ if err != nil {
+ return nil, err
+ }
+
+ return logical.ListResponse(entries), nil
+}
+
+// pathUsersRead returns a Jenkins user object in storage
+func (b *jenkinsBackend) pathUsersRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ username := b.parseUsernameFromPath(req.Path)
+ entry, err := b.getUserFromStorage(ctx, req.Storage, username)
+ if err != nil {
+ return nil, err
+ }
+
+ if entry == nil {
+ return nil, nil
+ }
+
+ return &logical.Response{
+ Data: entry.toResponseData(),
+ }, nil
+}
+
+// pathUsersDelete deletes a Jenkins user
+func (b *jenkinsBackend) pathUsersDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ username := b.parseUsernameFromPath(req.Path)
+
+ client, err := b.getClient(ctx, req.Storage)
+ if err != nil {
+ return nil, err
+ }
+
+ err = deleteUser(ctx, client, username)
+ if err != nil {
+ return logical.ErrorResponse(err.Error()), err
+ }
+
+ err = req.Storage.Delete(ctx, b.getUserPath(username))
+ if err != nil {
+ return logical.ErrorResponse(err.Error()), err
+ }
+
+ return nil, nil
+}
+
+// pathUsersWrite creates a new Jenkins user each time it is called if a user doesn't exist.
+func (b *jenkinsBackend) pathUsersWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ exists, err := b.pathUsersExistenceCheck(ctx, req, d)
+ if err != nil {
+ return logical.ErrorResponse(err.Error()), err
+ }
+
+ if exists {
+ return logical.ErrorResponse("user already exists"), nil
+ }
+
+ username := b.parseUsernameFromPath(req.Path)
+ password := d.Get("password").(string)
+ fullname := d.Get("fullname").(string)
+ email := d.Get("email").(string)
+ ttl := time.Duration(d.Get("ttl").(int)) * time.Second
+ maxTtl := time.Duration(d.Get("max_ttl").(int)) * time.Second
+ jenkinsUserConfig := &jenkinsUser{
+ Username: username,
+ Password: password,
+ Fullname: fullname,
+ Email: email,
+ TTL: ttl,
+ MaxTTL: maxTtl,
+ }
+
+ return b.createJenkinsUser(ctx, req, *jenkinsUserConfig)
+}
+
+// createJenkinsUser creates a new Jenkins user to store into the Vault backend, generates
+// a response with the user information, and checks the TTL and MaxTTL attributes.
+func (b *jenkinsBackend) createJenkinsUser(ctx context.Context, req *logical.Request, jenkinsUser jenkinsUser) (*logical.Response, error) {
+ user, err := b.createUser(ctx, req.Storage, jenkinsUser)
+ if err != nil {
+ return nil, err
+ }
+
+ // We won't store the password
+ // Need to store username to revoke later, ttl to renew later
+ internalData := map[string]interface{}{
+ "username": user.Username,
+ "fullname": user.Fullname,
+ "email": user.Email,
+ "ttl": user.TTL,
+ "max_ttl": user.MaxTTL,
+ }
+
+ // Create secret with lease
+ resp := b.Secret(jenkinsUserType).Response(user.toResponseData(), internalData)
+
+ // Create thing to store
+ entry, err := logical.StorageEntryJSON(b.getUserPath(jenkinsUser.Username), internalData)
+ if err != nil {
+ return logical.ErrorResponse("error creating user storage entry"), err
+ }
+
+ // Write to storage to view user inventory
+ err = req.Storage.Put(ctx, entry)
+ if err != nil {
+ return logical.ErrorResponse("error writing user to internal storage"), err
+ }
+
+ // Set TTL
+ if jenkinsUser.TTL > 0 {
+ resp.Secret.TTL = jenkinsUser.TTL
+ }
+ if jenkinsUser.MaxTTL > 0 {
+ resp.Secret.MaxTTL = jenkinsUser.MaxTTL
+ }
+
+ return resp, nil
+}
+
+// createUser uses the Jenkins client create a new user
+func (b *jenkinsBackend) createUser(ctx context.Context, s logical.Storage, userConfig jenkinsUser) (*jenkinsUser, error) {
+ client, err := b.getClient(ctx, s)
+ if err != nil {
+ return nil, err
+ }
+
+ var user *jenkinsUser
+
+ user, err = createUser(ctx, client, userConfig.Username, userConfig.Password, userConfig.Fullname, userConfig.Email)
+ if err != nil {
+ return nil, fmt.Errorf("error creating Jenkins user: %w", err)
+ }
+
+ if user == nil {
+ return nil, errors.New("error creating Jenkins user")
+ }
+
+ return user, nil
+}
+
+// parseUsername gets Jenkins username from /users request path
+func (b *jenkinsBackend) parseUsernameFromPath(path string) string {
+ return strings.TrimPrefix(path, fmt.Sprintf("%s/", usersPrefix))
+}
+
+const (
+ pathUsersHelpSyn = `
+Create a Jenkins User.
+`
+
+ pathUsersHelpDesc = `
+This path generates a Jenkins user
+using the root user configured under the /config mount.
+`
+
+ pathUsersListHelpSyn = `
+List Jenkins users.
+
+`
+ pathUsersListHelpDescription = `
+List all Jenkins users created under /users mount.
+`
+)
diff --git a/plugin/path_users_test.go b/plugin/path_users_test.go
new file mode 100644
index 0000000..430c66a
--- /dev/null
+++ b/plugin/path_users_test.go
@@ -0,0 +1,141 @@
+package jenkinssecretsengine
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/hashicorp/vault/sdk/logical"
+ "github.com/stretchr/testify/assert"
+)
+
+const (
+ testUserUsername = "testUsername"
+ testUserPassword = "testPassword"
+ testUserFullname = "testFullname"
+ testUserEmail = "testEmail@testemail.com"
+)
+
+// TestUser mocks the creation, read operations for a Jenkins user
+func TestUser(t *testing.T) {
+ b, s := getTestBackend(t)
+ AddTestConfig(t, b, s)
+
+ userPath := fmt.Sprintf("%s/%s", usersPrefix, testUserUsername)
+
+ t.Run("Test User", func(t *testing.T) {
+ err := testUserCreate(t, b, s, userPath, map[string]interface{}{
+ "password": testUserPassword,
+ "fullname": testUserFullname,
+ "email": testUserEmail,
+ })
+ assert.NoError(t, err)
+
+ // Users are ephemeral, will error if exists
+ err = testUserUpdate(t, b, s, userPath, map[string]interface{}{
+ "password": testUserPassword,
+ "fullname": testUserFullname,
+ "email": testUserEmail,
+ })
+ assert.Error(t, err)
+
+ err = testUserRead(t, b, s, userPath, map[string]interface{}{
+ "username": testUserUsername,
+ "fullname": testUserFullname,
+ "email": testUserEmail,
+ })
+ assert.NoError(t, err)
+
+ err = testUserDelete(t, b, s, userPath)
+ assert.NoError(t, err)
+ })
+}
+
+func testUserDelete(t *testing.T, b logical.Backend, s logical.Storage, path string) error {
+ resp, err := b.HandleRequest(context.Background(), &logical.Request{
+ Operation: logical.DeleteOperation,
+ Path: path,
+ Storage: s,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if resp != nil && resp.IsError() {
+ return resp.Error()
+ }
+ return nil
+}
+
+func testUserCreate(t *testing.T, b logical.Backend, s logical.Storage, path string, d map[string]interface{}) error {
+ resp, err := b.HandleRequest(context.Background(), &logical.Request{
+ Operation: logical.CreateOperation,
+ Path: path,
+ Data: d,
+ Storage: s,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if resp != nil && resp.IsError() {
+ return resp.Error()
+ }
+ return nil
+}
+
+func testUserUpdate(t *testing.T, b logical.Backend, s logical.Storage, path string, d map[string]interface{}) error {
+ resp, err := b.HandleRequest(context.Background(), &logical.Request{
+ Operation: logical.UpdateOperation,
+ Path: path,
+ Data: d,
+ Storage: s,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if resp != nil && resp.IsError() {
+ return resp.Error()
+ }
+ return nil
+}
+
+func testUserRead(t *testing.T, b logical.Backend, s logical.Storage, path string, expected map[string]interface{}) error {
+ resp, err := b.HandleRequest(context.Background(), &logical.Request{
+ Operation: logical.ReadOperation,
+ Path: path,
+ Storage: s,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if resp == nil && expected == nil {
+ return nil
+ }
+
+ if resp.IsError() {
+ return resp.Error()
+ }
+
+ if len(expected) != len(resp.Data) {
+ return fmt.Errorf("read data mismatch (expected %d values, got %d)", len(expected), len(resp.Data))
+ }
+
+ for k, expectedV := range expected {
+ actualV, ok := resp.Data[k]
+
+ if !ok {
+ return fmt.Errorf(`expected data["%s"] = %v but was not included in read output"`, k, expectedV)
+ } else if expectedV != actualV {
+ return fmt.Errorf(`expected data["%s"] = %v, instead got %v"`, k, expectedV, actualV)
+ }
+ }
+
+ return nil
+}