vault 0.12.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.circleci/config.yml +85 -0
- data/CHANGELOG.md +32 -0
- data/README.md +11 -3
- data/lib/vault/api.rb +2 -0
- data/lib/vault/api/kv.rb +207 -0
- data/lib/vault/api/secret.rb +12 -0
- data/lib/vault/api/sys.rb +2 -0
- data/lib/vault/api/sys/mount.rb +7 -2
- data/lib/vault/api/sys/namespace.rb +83 -0
- data/lib/vault/api/sys/quota.rb +107 -0
- data/lib/vault/api/transform.rb +29 -0
- data/lib/vault/api/transform/alphabet.rb +43 -0
- data/lib/vault/api/transform/role.rb +42 -0
- data/lib/vault/api/transform/template.rb +54 -0
- data/lib/vault/api/transform/transformation.rb +61 -0
- data/lib/vault/client.rb +9 -0
- data/lib/vault/configurable.rb +1 -0
- data/lib/vault/defaults.rb +7 -0
- data/lib/vault/request.rb +1 -0
- data/lib/vault/version.rb +1 -1
- data/vault.gemspec +4 -4
- metadata +26 -19
- data/.travis.yml +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 267c85a379172af5c24fd3c3d4e14b9f07991e058f64933b3c56cc07036b053e
|
4
|
+
data.tar.gz: 847ead8ea9965e449dfbf11e3447b240e5c016fbe90e177f1ba6adca0615ee18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 744df9d7282b0f873f008667fbd9c5bd943eea362535cdf872598c5eb5cb9fa36bb91d182a2b3ad0a2877294c16e345d371b33998a83645ba4e6880b13980e0e
|
7
|
+
data.tar.gz: 64ac03ddf3a2c5609e2224548353be0d9e640bfba1ec0ec0e215f541f2802db830238373a5b38fb15f77675d75717847468b8e5c2a1331cf609e87ca497e812a
|
@@ -0,0 +1,85 @@
|
|
1
|
+
version: 2.1
|
2
|
+
|
3
|
+
orbs:
|
4
|
+
gem: zfhui/ruby-gem@0.2.1
|
5
|
+
|
6
|
+
references:
|
7
|
+
images:
|
8
|
+
ubuntu: &UBUNTU_IMAGE ubuntu-1604:201903-01
|
9
|
+
|
10
|
+
jobs:
|
11
|
+
test:
|
12
|
+
machine:
|
13
|
+
image: *UBUNTU_IMAGE
|
14
|
+
parameters:
|
15
|
+
ruby-version:
|
16
|
+
type: string
|
17
|
+
vault-version:
|
18
|
+
type: string
|
19
|
+
steps:
|
20
|
+
- checkout
|
21
|
+
# Restore bundle cache
|
22
|
+
- restore_cache:
|
23
|
+
keys:
|
24
|
+
- v1-dependencies-bundler-<< parameters.ruby-version >>-{{ checksum "vault.gemspec" }}
|
25
|
+
# fallback to using the latest cache if no exact match is found
|
26
|
+
- v1-dependencies-bundler-
|
27
|
+
- run:
|
28
|
+
name: Install vault
|
29
|
+
command: |
|
30
|
+
curl -sLo vault.zip https://releases.hashicorp.com/vault/<< parameters.vault-version >>/vault_<< parameters.vault-version >>_linux_amd64.zip
|
31
|
+
unzip vault.zip
|
32
|
+
mkdir -p ~/bin
|
33
|
+
mv vault ~/bin
|
34
|
+
export PATH="~/bin:$PATH"
|
35
|
+
- run:
|
36
|
+
name: Set ruby version
|
37
|
+
command: |
|
38
|
+
rvm install << parameters.ruby-version >>
|
39
|
+
echo . $(rvm << parameters.ruby-version >> do rvm env --path) >> $BASH_ENV
|
40
|
+
- run:
|
41
|
+
name: Run tests
|
42
|
+
command: |
|
43
|
+
export VAULT_VERSION=<< parameters.vault-version >>
|
44
|
+
ruby --version
|
45
|
+
gem install bundler
|
46
|
+
bundle -v
|
47
|
+
bundle install --jobs=3 --retry=3 --path=vendor/bundle
|
48
|
+
bundle exec rake
|
49
|
+
# Store bundle cache
|
50
|
+
- save_cache:
|
51
|
+
key: v1-dependencies-bundler-<< parameters.ruby-version >>-{{ checksum "vault.gemspec" }}
|
52
|
+
paths:
|
53
|
+
- vendor/bundle
|
54
|
+
|
55
|
+
build-release:
|
56
|
+
working_directory: ~/repo
|
57
|
+
executor: gem/default
|
58
|
+
steps:
|
59
|
+
- gem/build:
|
60
|
+
gem-name: vault
|
61
|
+
- gem/release:
|
62
|
+
gem-name: vault
|
63
|
+
gem-credentials-env-name: $RUBYGEMS_API_KEY
|
64
|
+
|
65
|
+
workflows:
|
66
|
+
run-tests:
|
67
|
+
jobs:
|
68
|
+
- test:
|
69
|
+
filters:
|
70
|
+
tags:
|
71
|
+
only: /^v[0-9]+\.[0-9]+\.[0-9]+.*/
|
72
|
+
matrix:
|
73
|
+
parameters:
|
74
|
+
ruby-version: ["2.7.1", "2.6", "2.5"]
|
75
|
+
vault-version: ["1.5.0", "1.4.2", "1.4.1", "1.4.0", "1.3.6"]
|
76
|
+
name: test-ruby-<< matrix.ruby-version >>-vault-<< matrix.vault-version >>
|
77
|
+
- build-release:
|
78
|
+
requires:
|
79
|
+
- test
|
80
|
+
context: vault-gem-release
|
81
|
+
filters:
|
82
|
+
tags:
|
83
|
+
only: /^v[0-9]+\.[0-9]+\.[0-9]+.*/
|
84
|
+
branches:
|
85
|
+
ignore: /.*/
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,37 @@
|
|
1
1
|
# Vault Ruby Changelog
|
2
2
|
|
3
|
+
## v0.15.0 (July 29, 2020)
|
4
|
+
|
5
|
+
IMPROVEMENTS
|
6
|
+
|
7
|
+
- Added support for Resource Quotas
|
8
|
+
|
9
|
+
## v0.14.0 (May 28, 2020)
|
10
|
+
|
11
|
+
IMPROVEMENTS
|
12
|
+
|
13
|
+
- Added support for the Transform Secrets Engine
|
14
|
+
|
15
|
+
## v0.13.2 (May 7, 2020)
|
16
|
+
|
17
|
+
BUG FIXES
|
18
|
+
|
19
|
+
- Fixed the ability to use namespace as an option for each request. Previously, that option was ignored.
|
20
|
+
- aws-sigv4 gem was unlocked after a bug in 1.1.2 broke CI
|
21
|
+
|
22
|
+
## v0.13.1 (April 28, 2020)
|
23
|
+
|
24
|
+
IMPROVEMENTS
|
25
|
+
|
26
|
+
- Added support for defining a namespace when initializing the client, as well as options for changing the namespace via method.
|
27
|
+
- Added support for sys/namespaces API. Ability to Get, Create, Delete, and List namespaces has been provided.
|
28
|
+
|
29
|
+
## v0.13.0 (October 1, 2019)
|
30
|
+
|
31
|
+
IMPROVEMENTS
|
32
|
+
|
33
|
+
- Add support for versioned KV secrets in the client
|
34
|
+
|
3
35
|
## v0.12.0 (August 14, 2018)
|
4
36
|
|
5
37
|
IMPROVEMENTS
|
data/README.md
CHANGED
@@ -28,6 +28,8 @@ Start a Vault client:
|
|
28
28
|
```ruby
|
29
29
|
Vault.address = "http://127.0.0.1:8200" # Also reads from ENV["VAULT_ADDR"]
|
30
30
|
Vault.token = "abcd-1234" # Also reads from ENV["VAULT_TOKEN"]
|
31
|
+
# Optional - if using the Namespace enterprise feature
|
32
|
+
# Vault.namespace = "my-namespace" # Also reads from ENV["VAULT_NAMESPACE"]
|
31
33
|
|
32
34
|
Vault.sys.mounts #=> { :secret => #<struct Vault::Mount type="generic", description="generic secret storage"> }
|
33
35
|
```
|
@@ -43,6 +45,8 @@ Vault.configure do |config|
|
|
43
45
|
|
44
46
|
# The token to authenticate with Vault, also read as ENV["VAULT_TOKEN"]
|
45
47
|
config.token = "abcd-1234"
|
48
|
+
# Optional - if using the Namespace enterprise feature
|
49
|
+
# config.namespace = "my-namespace" # Also reads from ENV["VAULT_NAMESPACE"]
|
46
50
|
|
47
51
|
# Proxy connection information, also read as ENV["VAULT_PROXY_(thing)"]
|
48
52
|
config.proxy_address = "..."
|
@@ -85,7 +89,8 @@ And if you want to authenticate with a `AWS EC2` :
|
|
85
89
|
# Export VAULT_ADDR to ENV then
|
86
90
|
# Get the pkcs7 value from AWS
|
87
91
|
signature = `curl http://169.254.169.254/latest/dynamic/instance-identity/pkcs7`
|
88
|
-
|
92
|
+
iam_role = `curl http://169.254.169.254/latest/meta-data/iam/security-credentials/`
|
93
|
+
vault_token = Vault.auth.aws_ec2(iam_role, signature, nil)
|
89
94
|
vault_client = Vault::Client.new(address: ENV["VAULT_ADDR"], token: vault_token.auth.client_token)
|
90
95
|
```
|
91
96
|
|
@@ -117,7 +122,9 @@ For advanced users, the first argument of the block is the attempt number and th
|
|
117
122
|
|
118
123
|
```ruby
|
119
124
|
Vault.with_retries(Vault::HTTPConnectionError, Vault::HTTPError) do |attempt, e|
|
120
|
-
|
125
|
+
if e
|
126
|
+
log "Received exception #{e} from Vault - attempt #{attempt}"
|
127
|
+
end
|
121
128
|
Vault.logical.read("secret/bacon")
|
122
129
|
end
|
123
130
|
```
|
@@ -206,7 +213,8 @@ Development
|
|
206
213
|
|
207
214
|
Important Notes:
|
208
215
|
|
209
|
-
- **All new features must include test coverage.** At a bare minimum, Unit tests are required. It is preferred if you include
|
216
|
+
- **All new features must include test coverage.** At a bare minimum, Unit tests are required. It is preferred if you include integration tests as well.
|
210
217
|
- **The tests must be be idempotent.** The HTTP calls made during a test should be able to be run over and over.
|
211
218
|
- **Tests are order independent.** The default RSpec configuration randomizes the test order, so this should not be a problem.
|
212
219
|
- **Integration tests require Vault** Vault must be available in the path for the integration tests to pass.
|
220
|
+
- **In order to be considered an integration test:** The test MUST use the `vault_test_client` or `vault_redirect_test_client` as the client. This spawns a process, or uses an already existing process from another test, to run against.
|
data/lib/vault/api.rb
CHANGED
@@ -5,8 +5,10 @@ module Vault
|
|
5
5
|
require_relative "api/auth_tls"
|
6
6
|
require_relative "api/auth"
|
7
7
|
require_relative "api/help"
|
8
|
+
require_relative "api/kv"
|
8
9
|
require_relative "api/logical"
|
9
10
|
require_relative "api/secret"
|
10
11
|
require_relative "api/sys"
|
12
|
+
require_relative "api/transform"
|
11
13
|
end
|
12
14
|
end
|
data/lib/vault/api/kv.rb
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
require_relative "secret"
|
2
|
+
require_relative "../client"
|
3
|
+
require_relative "../request"
|
4
|
+
require_relative "../response"
|
5
|
+
|
6
|
+
module Vault
|
7
|
+
class Client
|
8
|
+
# A proxy to the {KV} methods.
|
9
|
+
# @return [KV]
|
10
|
+
def kv(mount)
|
11
|
+
KV.new(self, mount)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class KV < Request
|
16
|
+
attr_reader :mount
|
17
|
+
|
18
|
+
def initialize(client, mount)
|
19
|
+
super client
|
20
|
+
|
21
|
+
@mount = mount
|
22
|
+
end
|
23
|
+
|
24
|
+
# List the names of secrets at the given path, if the path supports
|
25
|
+
# listing. If the the path does not exist, an empty array will be returned.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# Vault.kv("secret").list("foo") #=> ["bar", "baz"]
|
29
|
+
#
|
30
|
+
# @param [String] path
|
31
|
+
# the path to list
|
32
|
+
#
|
33
|
+
# @return [Array<String>]
|
34
|
+
def list(path = "", options = {})
|
35
|
+
headers = extract_headers!(options)
|
36
|
+
json = client.list("/v1/#{mount}/metadata/#{encode_path(path)}", {}, headers)
|
37
|
+
json[:data][:keys] || []
|
38
|
+
rescue HTTPError => e
|
39
|
+
return [] if e.code == 404
|
40
|
+
raise
|
41
|
+
end
|
42
|
+
|
43
|
+
# Read the secret at the given path. If the secret does not exist, +nil+
|
44
|
+
# will be returned. The latest version is returned by default, but you
|
45
|
+
# can request a specific version.
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# Vault.kv("secret").read("password") #=> #<Vault::Secret lease_id="">
|
49
|
+
#
|
50
|
+
# @param [String] path
|
51
|
+
# the path to read
|
52
|
+
# @param [Integer] version
|
53
|
+
# the version of the secret
|
54
|
+
#
|
55
|
+
# @return [Secret, nil]
|
56
|
+
def read(path, version = nil, options = {})
|
57
|
+
headers = extract_headers!(options)
|
58
|
+
params = {}
|
59
|
+
params[:version] = version unless version.nil?
|
60
|
+
|
61
|
+
json = client.get("/v1/#{mount}/data/#{encode_path(path)}", params, headers)
|
62
|
+
return Secret.decode(json[:data])
|
63
|
+
rescue HTTPError => e
|
64
|
+
return nil if e.code == 404
|
65
|
+
raise
|
66
|
+
end
|
67
|
+
|
68
|
+
# Read the metadata of a secret at the given path. If the secret does not
|
69
|
+
# exist, nil will be returned.
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# Vault.kv("secret").read_metadata("password") => {...}
|
73
|
+
#
|
74
|
+
# @param [String] path
|
75
|
+
# the path to read
|
76
|
+
#
|
77
|
+
# @return [Hash, nil]
|
78
|
+
def read_metadata(path)
|
79
|
+
client.get("/v1/#{mount}/metadata/#{encode_path(path)}")[:data]
|
80
|
+
rescue HTTPError => e
|
81
|
+
return nil if e.code == 404
|
82
|
+
raise
|
83
|
+
end
|
84
|
+
|
85
|
+
# Write the secret at the given path with the given data. Note that the
|
86
|
+
# data must be a {Hash}!
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
# Vault.logical.write("secret/password", value: "secret") #=> #<Vault::Secret lease_id="">
|
90
|
+
#
|
91
|
+
# @param [String] path
|
92
|
+
# the path to write
|
93
|
+
# @param [Hash] data
|
94
|
+
# the data to write
|
95
|
+
#
|
96
|
+
# @return [Secret]
|
97
|
+
def write(path, data = {}, options = {})
|
98
|
+
headers = extract_headers!(options)
|
99
|
+
json = client.post("/v1/#{mount}/data/#{encode_path(path)}", JSON.fast_generate(:data => data), headers)
|
100
|
+
if json.nil?
|
101
|
+
return true
|
102
|
+
else
|
103
|
+
return Secret.decode(json)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Write the metadata of a secret at the given path. Note that the data must
|
108
|
+
# be a {Hash}.
|
109
|
+
#
|
110
|
+
# @example
|
111
|
+
# Vault.kv("secret").write_metadata("password", max_versions => 3)
|
112
|
+
#
|
113
|
+
# @param [String] path
|
114
|
+
# the path to write
|
115
|
+
# @param [Hash] metadata
|
116
|
+
# the metadata to write
|
117
|
+
#
|
118
|
+
# @return [true]
|
119
|
+
def write_metadata(path, metadata = {})
|
120
|
+
client.post("/v1/#{mount}/metadata/#{encode_path(path)}", JSON.fast_generate(metadata))
|
121
|
+
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
# Delete the secret at the given path. If the secret does not exist, vault
|
126
|
+
# will still return true.
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# Vault.logical.delete("secret/password") #=> true
|
130
|
+
#
|
131
|
+
# @param [String] path
|
132
|
+
# the path to delete
|
133
|
+
#
|
134
|
+
# @return [true]
|
135
|
+
def delete(path)
|
136
|
+
client.delete("/v1/#{mount}/data/#{encode_path(path)}")
|
137
|
+
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
# Mark specific versions of a secret as deleted.
|
142
|
+
#
|
143
|
+
# @example
|
144
|
+
# Vault.kv("secret").delete_versions("password", [1, 2])
|
145
|
+
#
|
146
|
+
# @param [String] path
|
147
|
+
# the path to remove versions from
|
148
|
+
# @param [Array<Integer>] versions
|
149
|
+
# an array of versions to remove
|
150
|
+
#
|
151
|
+
# @return [true]
|
152
|
+
def delete_versions(path, versions)
|
153
|
+
client.post("/v1/#{mount}/delete/#{encode_path(path)}", JSON.fast_generate(versions: versions))
|
154
|
+
|
155
|
+
true
|
156
|
+
end
|
157
|
+
|
158
|
+
# Mark specific versions of a secret as active.
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# Vault.kv("secret").undelete_versions("password", [1, 2])
|
162
|
+
#
|
163
|
+
# @param [String] path
|
164
|
+
# the path to enable versions for
|
165
|
+
# @param [Array<Integer>] versions
|
166
|
+
# an array of versions to mark as undeleted
|
167
|
+
#
|
168
|
+
# @return [true]
|
169
|
+
def undelete_versions(path, versions)
|
170
|
+
client.post("/v1/#{mount}/undelete/#{encode_path(path)}", JSON.fast_generate(versions: versions))
|
171
|
+
|
172
|
+
true
|
173
|
+
end
|
174
|
+
|
175
|
+
# Completely remove a secret and its metadata.
|
176
|
+
#
|
177
|
+
# @example
|
178
|
+
# Vault.kv("secret").destroy("password")
|
179
|
+
#
|
180
|
+
# @param [String] path
|
181
|
+
# the path to remove
|
182
|
+
#
|
183
|
+
# @return [true]
|
184
|
+
def destroy(path)
|
185
|
+
client.delete("/v1/#{mount}/metadata/#{encode_path(path)}")
|
186
|
+
|
187
|
+
true
|
188
|
+
end
|
189
|
+
|
190
|
+
# Completely remove specific versions of a secret.
|
191
|
+
#
|
192
|
+
# @example
|
193
|
+
# Vault.kv("secret").destroy_versions("password", [1, 2])
|
194
|
+
#
|
195
|
+
# @param [String] path
|
196
|
+
# the path to remove versions from
|
197
|
+
# @param [Array<Integer>] versions
|
198
|
+
# an array of versions to destroy
|
199
|
+
#
|
200
|
+
# @return [true]
|
201
|
+
def destroy_versions(path, versions)
|
202
|
+
client.post("/v1/#{mount}/destroy/#{encode_path(path)}", JSON.fast_generate(versions: versions))
|
203
|
+
|
204
|
+
true
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
data/lib/vault/api/secret.rb
CHANGED
@@ -32,6 +32,18 @@ module Vault
|
|
32
32
|
# @return [Hash<Symbol, Object>]
|
33
33
|
field :data, freeze: true
|
34
34
|
|
35
|
+
# @!attribute [r] metadata
|
36
|
+
# Read-only metadata information related to the secret.
|
37
|
+
#
|
38
|
+
# @example Reading metadata
|
39
|
+
# secret = Vault.logical(:versioned).read("secret", "foo")
|
40
|
+
# secret.metadata[:created_time] #=> "2018-12-08T04:22:54.168065Z"
|
41
|
+
# secret.metadata[:version] #=> 1
|
42
|
+
# secret.metadata[:destroyed] #=> false
|
43
|
+
#
|
44
|
+
# @return [Hash<Symbol, Object>]
|
45
|
+
field :metadata, freeze: true
|
46
|
+
|
35
47
|
# @!attribute [r] lease_duration
|
36
48
|
# The number of seconds this lease is valid. If this number is 0 or nil,
|
37
49
|
# the secret does not expire.
|
data/lib/vault/api/sys.rb
CHANGED
data/lib/vault/api/sys/mount.rb
CHANGED
@@ -16,6 +16,11 @@ module Vault
|
|
16
16
|
# Type of the mount.
|
17
17
|
# @return [String]
|
18
18
|
field :type
|
19
|
+
|
20
|
+
# @!attribute [r] type
|
21
|
+
# Options given to the mount.
|
22
|
+
# @return [Hash<Symbol, Object>]
|
23
|
+
field :options
|
19
24
|
end
|
20
25
|
|
21
26
|
class Sys < Request
|
@@ -44,8 +49,8 @@ module Vault
|
|
44
49
|
# the type of mount
|
45
50
|
# @param [String] description
|
46
51
|
# a human-friendly description (optional)
|
47
|
-
def mount(path, type, description = nil)
|
48
|
-
payload =
|
52
|
+
def mount(path, type, description = nil, options = {})
|
53
|
+
payload = options.merge type: type
|
49
54
|
payload[:description] = description if !description.nil?
|
50
55
|
|
51
56
|
client.post("/v1/sys/mounts/#{encode_path(path)}", JSON.fast_generate(payload))
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Vault
|
2
|
+
class Namespace < Response
|
3
|
+
# @!attribute [r] id
|
4
|
+
# ID of the namespace
|
5
|
+
# @return [String]
|
6
|
+
field :id
|
7
|
+
|
8
|
+
# @!attribute [r] path
|
9
|
+
# Path of the namespace, includes parent paths if nested.
|
10
|
+
# @return [String]
|
11
|
+
field :path
|
12
|
+
end
|
13
|
+
|
14
|
+
class Sys
|
15
|
+
# List all namespaces in a given scope. Ignores nested namespaces.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# Vault.sys.namespaces #=> { :foo => #<struct Vault::Namespace id="xxxx1", path="foo/" }
|
19
|
+
#
|
20
|
+
# @return [Hash<Symbol, Namespace>]
|
21
|
+
def namespaces(scoped=nil)
|
22
|
+
path = ["v1", scoped, "sys", "namespaces"].compact
|
23
|
+
json = client.list(path.join("/"))
|
24
|
+
json = json[:data] if json[:data]
|
25
|
+
if json[:key_info]
|
26
|
+
json = json[:key_info]
|
27
|
+
hash = {}
|
28
|
+
json.each do |k,v|
|
29
|
+
hash[k.to_s.chomp("/").to_sym] = Namespace.decode(v)
|
30
|
+
end
|
31
|
+
hash
|
32
|
+
else
|
33
|
+
json
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create a namespace. Nests the namespace if a namespace header is provided.
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# Vault.sys.create_namespace("foo")
|
41
|
+
#
|
42
|
+
# @param [String] namespace
|
43
|
+
# the potential path of the namespace, without any parent path provided
|
44
|
+
#
|
45
|
+
# @return [true]
|
46
|
+
def create_namespace(namespace)
|
47
|
+
client.put("/v1/sys/namespaces/#{namespace}", {})
|
48
|
+
return true
|
49
|
+
end
|
50
|
+
|
51
|
+
# Delete a namespace. Raises an error if the namespace provided is not empty.
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# Vault.sys.delete_namespace("foo")
|
55
|
+
#
|
56
|
+
# @param [String] namespace
|
57
|
+
# the path of the namespace to be deleted
|
58
|
+
#
|
59
|
+
# @return [true]
|
60
|
+
def delete_namespace(namespace)
|
61
|
+
client.delete("/v1/sys/namespaces/#{namespace}")
|
62
|
+
return true
|
63
|
+
end
|
64
|
+
|
65
|
+
# Retrieve a namespace by path.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# Vault.sys.get_namespace("foo")
|
69
|
+
#
|
70
|
+
# @param [String] namespace
|
71
|
+
# the path of the namespace ot be retrieved
|
72
|
+
#
|
73
|
+
# @return [Namespace]
|
74
|
+
def get_namespace(namespace)
|
75
|
+
json = client.get("/v1/sys/namespaces/#{namespace}")
|
76
|
+
if data = json.dig(:data)
|
77
|
+
Namespace.decode(data)
|
78
|
+
else
|
79
|
+
json
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Vault
|
2
|
+
class Quota < Response
|
3
|
+
# @!attribute [r] name
|
4
|
+
# Name of the quota rule.
|
5
|
+
# @return [String]
|
6
|
+
field :name
|
7
|
+
|
8
|
+
# @!attribute [r] path
|
9
|
+
# Namespace/Path combination the quota applies to.
|
10
|
+
# @return [String]
|
11
|
+
field :path
|
12
|
+
|
13
|
+
# @!attribute [r] type
|
14
|
+
# Type of the quota rule, must be one of "lease-count" or "rate-limit"
|
15
|
+
# @return [String]
|
16
|
+
field :type
|
17
|
+
end
|
18
|
+
|
19
|
+
class RateLimitQuota < Quota
|
20
|
+
# @!attribute [r] rate
|
21
|
+
# The rate at which allowed requests are refilled per second by the quota
|
22
|
+
# rule.
|
23
|
+
# @return [Float]
|
24
|
+
field :rate
|
25
|
+
|
26
|
+
# @!attribute [r] burst
|
27
|
+
# The maximum number of requests at any given second allowed by the quota
|
28
|
+
# rule.
|
29
|
+
# @return [Int]
|
30
|
+
field :burst
|
31
|
+
end
|
32
|
+
|
33
|
+
class LeaseCountQuota < Quota
|
34
|
+
# @!attribute [r] counter
|
35
|
+
# Number of currently active leases for the quota.
|
36
|
+
# @return [Int]
|
37
|
+
field :counter
|
38
|
+
|
39
|
+
# @!attribute [r] max_leases
|
40
|
+
# The maximum number of allowed leases for this quota.
|
41
|
+
# @return [Int]
|
42
|
+
field :max_leases
|
43
|
+
end
|
44
|
+
|
45
|
+
class Sys
|
46
|
+
def quotas(type)
|
47
|
+
path = generate_path(type)
|
48
|
+
json = client.list(path)
|
49
|
+
if data = json.dig(:data, :key_info)
|
50
|
+
data.map do |item|
|
51
|
+
type_class(type).decode(item)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
json
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_quota(type, name, opts={})
|
59
|
+
path = generate_path(type, name)
|
60
|
+
client.post(path, JSON.fast_generate(opts))
|
61
|
+
return true
|
62
|
+
end
|
63
|
+
|
64
|
+
def delete_quota(type, name)
|
65
|
+
path = generate_path(type, name)
|
66
|
+
client.delete(path)
|
67
|
+
return true
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_quota(type, name)
|
71
|
+
path = generate_path(type, name)
|
72
|
+
response = client.get(path)
|
73
|
+
if data = response[:data]
|
74
|
+
type_class(type).decode(data)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_quota_config
|
79
|
+
client.get("v1/sys/quotas/config")
|
80
|
+
end
|
81
|
+
|
82
|
+
def update_quota_config(opts={})
|
83
|
+
client.post("v1/sys/quotas/config", JSON.fast_generate(opts))
|
84
|
+
return true
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def generate_path(type, name=nil)
|
90
|
+
verify_type(type)
|
91
|
+
path = ["v1", "sys", "quotas", type, name].compact
|
92
|
+
path.join("/")
|
93
|
+
end
|
94
|
+
|
95
|
+
def verify_type(type)
|
96
|
+
return if ["rate-limit", "lease-count"].include?(type)
|
97
|
+
raise ArgumentError, "type must be one of \"rate-limit\" or \"lease-count\""
|
98
|
+
end
|
99
|
+
|
100
|
+
def type_class(type)
|
101
|
+
case type
|
102
|
+
when "lease-count" then LeaseCountQuota
|
103
|
+
when "rate-limit" then RateLimitQuota
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative '../client'
|
2
|
+
require_relative '../request'
|
3
|
+
|
4
|
+
module Vault
|
5
|
+
class Client
|
6
|
+
# A proxy to the {Transform} methods.
|
7
|
+
# @return [Transform]
|
8
|
+
def transform
|
9
|
+
@transform ||= Transform.new(self)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Transform < Request
|
14
|
+
def encode(role_name:, **opts)
|
15
|
+
opts ||= {}
|
16
|
+
client.post("/v1/transform/encode/#{encode_path(role_name)}", JSON.fast_generate(opts))
|
17
|
+
end
|
18
|
+
|
19
|
+
def decode(role_name:, **opts)
|
20
|
+
opts ||= {}
|
21
|
+
client.post("/v1/transform/decode/#{encode_path(role_name)}", JSON.fast_generate(opts))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require_relative 'transform/alphabet'
|
27
|
+
require_relative 'transform/role'
|
28
|
+
require_relative 'transform/template'
|
29
|
+
require_relative 'transform/transformation'
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative '../../request'
|
2
|
+
require_relative '../../response'
|
3
|
+
|
4
|
+
module Vault
|
5
|
+
class Transform < Request
|
6
|
+
class Alphabet < Response
|
7
|
+
# @!attribute [r] id
|
8
|
+
# String listing all possible characters of the alphabet
|
9
|
+
# @return [String]
|
10
|
+
field :alphabet
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_alphabet(name, alphabet:, **opts)
|
14
|
+
opts ||= {}
|
15
|
+
opts[:alphabet] = alphabet
|
16
|
+
client.post("/v1/transform/alphabet/#{encode_path(name)}", JSON.fast_generate(opts))
|
17
|
+
return true
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_alphabet(name)
|
21
|
+
json = client.get("/v1/transform/alphabet/#{encode_path(name)}")
|
22
|
+
if data = json.dig(:data)
|
23
|
+
Alphabet.decode(data)
|
24
|
+
else
|
25
|
+
json
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def delete_alphabet(name)
|
30
|
+
client.delete("/v1/transform/alphabet/#{encode_path(name)}")
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def alphabets
|
35
|
+
json = client.list("/v1/transform/alphabet")
|
36
|
+
if keys = json.dig(:data, :keys)
|
37
|
+
keys
|
38
|
+
else
|
39
|
+
json
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative '../../request'
|
2
|
+
require_relative '../../response'
|
3
|
+
|
4
|
+
module Vault
|
5
|
+
class Transform < Request
|
6
|
+
class Role < Response
|
7
|
+
# @!attribute [r] transformations
|
8
|
+
# Array of all transformations the role has access to
|
9
|
+
# @return [Array<String>]
|
10
|
+
field :transformations
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_role(name, **opts)
|
14
|
+
opts ||= {}
|
15
|
+
client.post("/v1/transform/role/#{encode_path(name)}", JSON.fast_generate(opts))
|
16
|
+
return true
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_role(name)
|
20
|
+
json = client.get("/v1/transform/role/#{encode_path(name)}")
|
21
|
+
if data = json.dig(:data)
|
22
|
+
Role.decode(data)
|
23
|
+
else
|
24
|
+
json
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete_role(name)
|
29
|
+
client.delete("/v1/transform/role/#{encode_path(name)}")
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def roles
|
34
|
+
json = client.list("/v1/transform/role")
|
35
|
+
if keys = json.dig(:data, :keys)
|
36
|
+
keys
|
37
|
+
else
|
38
|
+
json
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative '../../request'
|
2
|
+
require_relative '../../response'
|
3
|
+
|
4
|
+
module Vault
|
5
|
+
class Transform < Request
|
6
|
+
class Template < Response
|
7
|
+
# @!attribute [r] alphabet
|
8
|
+
# Name of the alphabet to be used in the template
|
9
|
+
# @return [String]
|
10
|
+
field :alphabet
|
11
|
+
|
12
|
+
# @!attribute [r] pattern
|
13
|
+
# Regex string to detect and match for the template
|
14
|
+
# @return [String]
|
15
|
+
field :pattern
|
16
|
+
|
17
|
+
# @!attribute [r] type
|
18
|
+
# Type of the template, currently, only "regex" is supported
|
19
|
+
# @return [String]
|
20
|
+
field :type
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_template(name, type:, pattern:, **opts)
|
24
|
+
opts ||= {}
|
25
|
+
opts[:type] = type
|
26
|
+
opts[:pattern] = pattern
|
27
|
+
client.post("/v1/transform/template/#{encode_path(name)}", JSON.fast_generate(opts))
|
28
|
+
return true
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_template(name)
|
32
|
+
json = client.get("/v1/transform/template/#{encode_path(name)}")
|
33
|
+
if data = json.dig(:data)
|
34
|
+
Template.decode(data)
|
35
|
+
else
|
36
|
+
json
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete_template(name)
|
41
|
+
client.delete("/v1/transform/template/#{encode_path(name)}")
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def templates
|
46
|
+
json = client.list("/v1/transform/template")
|
47
|
+
if keys = json.dig(:data, :keys)
|
48
|
+
keys
|
49
|
+
else
|
50
|
+
json
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative '../../request'
|
2
|
+
require_relative '../../response'
|
3
|
+
|
4
|
+
module Vault
|
5
|
+
class Transform < Request
|
6
|
+
class Transformation < Response
|
7
|
+
# @!attribute [r] allowed_roles
|
8
|
+
# Array of role names that are allowed to use this transformation
|
9
|
+
# @return [Array<String>]
|
10
|
+
field :allowed_roles
|
11
|
+
|
12
|
+
# @!attribute [r] templates
|
13
|
+
# Array of template names accessible to this transformation
|
14
|
+
# @return [Array<String>]
|
15
|
+
field :templates
|
16
|
+
|
17
|
+
# @!attribute [r] tweak_source
|
18
|
+
# String representing how a tweak is provided for this transformation.
|
19
|
+
# Available tweaks are "supplied", "generated", and "internal"
|
20
|
+
# @return [String]
|
21
|
+
field :tweak_source
|
22
|
+
|
23
|
+
# @!attribute [r] type
|
24
|
+
# String representing the type of transformation this is.
|
25
|
+
# Available types are "fpe", and "masking"
|
26
|
+
# @return [String]
|
27
|
+
field :type
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_transformation(name, type:, template:, **opts)
|
31
|
+
opts ||= {}
|
32
|
+
opts[:type] = type
|
33
|
+
opts[:template] = template
|
34
|
+
client.post("/v1/transform/transformation/#{encode_path(name)}", JSON.fast_generate(opts))
|
35
|
+
return true
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_transformation(name)
|
39
|
+
json = client.get("/v1/transform/transformation/#{encode_path(name)}")
|
40
|
+
if data = json.dig(:data)
|
41
|
+
Transformation.decode(data)
|
42
|
+
else
|
43
|
+
json
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete_transformation(name)
|
48
|
+
client.delete("/v1/transform/transformation/#{encode_path(name)}")
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def transformations
|
53
|
+
json = client.list("/v1/transform/transformation")
|
54
|
+
if keys = json.dig(:data, :keys)
|
55
|
+
keys
|
56
|
+
else
|
57
|
+
json
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/vault/client.rb
CHANGED
@@ -16,6 +16,9 @@ module Vault
|
|
16
16
|
# The name of the header used to hold the Vault token.
|
17
17
|
TOKEN_HEADER = "X-Vault-Token".freeze
|
18
18
|
|
19
|
+
# The name of the header used to hold the Namespace.
|
20
|
+
NAMESPACE_HEADER = "X-Vault-Namespace".freeze
|
21
|
+
|
19
22
|
# The name of the header used to hold the wrapped request ttl.
|
20
23
|
WRAP_TTL_HEADER = "X-Vault-Wrap-TTL".freeze
|
21
24
|
|
@@ -255,6 +258,12 @@ module Vault
|
|
255
258
|
headers[TOKEN_HEADER] ||= token
|
256
259
|
end
|
257
260
|
|
261
|
+
# Add the Vault Namespace header - users could still override this on a
|
262
|
+
# per-request basis
|
263
|
+
if !namespace.nil?
|
264
|
+
headers[NAMESPACE_HEADER] ||= namespace
|
265
|
+
end
|
266
|
+
|
258
267
|
# Add headers
|
259
268
|
headers.each do |key, value|
|
260
269
|
request.add_field(key, value)
|
data/lib/vault/configurable.rb
CHANGED
data/lib/vault/defaults.rb
CHANGED
data/lib/vault/request.rb
CHANGED
data/lib/vault/version.rb
CHANGED
data/vault.gemspec
CHANGED
@@ -21,10 +21,10 @@ Gem::Specification.new do |spec|
|
|
21
21
|
|
22
22
|
spec.add_runtime_dependency "aws-sigv4"
|
23
23
|
|
24
|
-
spec.add_development_dependency "bundler"
|
25
|
-
spec.add_development_dependency "pry"
|
24
|
+
spec.add_development_dependency "bundler", "~> 2"
|
25
|
+
spec.add_development_dependency "pry", "~> 0.13.1"
|
26
26
|
spec.add_development_dependency "rake", "~> 12.0"
|
27
27
|
spec.add_development_dependency "rspec", "~> 3.5"
|
28
|
-
spec.add_development_dependency "yard"
|
29
|
-
spec.add_development_dependency "webmock", "~>
|
28
|
+
spec.add_development_dependency "yard", "~> 0.9.24"
|
29
|
+
spec.add_development_dependency "webmock", "~> 3.8.3"
|
30
30
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vault
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Seth Vargo
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-08-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sigv4
|
@@ -28,30 +28,30 @@ dependencies:
|
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '2'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '2'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: pry
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 0.13.1
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 0.13.1
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -84,30 +84,30 @@ dependencies:
|
|
84
84
|
name: yard
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
89
|
+
version: 0.9.24
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
96
|
+
version: 0.9.24
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: webmock
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
103
|
+
version: 3.8.3
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
110
|
+
version: 3.8.3
|
111
111
|
description: Vault is a Ruby API client for interacting with a Vault server.
|
112
112
|
email:
|
113
113
|
- sethvargo@gmail.com
|
@@ -115,9 +115,9 @@ executables: []
|
|
115
115
|
extensions: []
|
116
116
|
extra_rdoc_files: []
|
117
117
|
files:
|
118
|
+
- ".circleci/config.yml"
|
118
119
|
- ".gitignore"
|
119
120
|
- ".rspec"
|
120
|
-
- ".travis.yml"
|
121
121
|
- CHANGELOG.md
|
122
122
|
- Gemfile
|
123
123
|
- LICENSE
|
@@ -130,6 +130,7 @@ files:
|
|
130
130
|
- lib/vault/api/auth_tls.rb
|
131
131
|
- lib/vault/api/auth_token.rb
|
132
132
|
- lib/vault/api/help.rb
|
133
|
+
- lib/vault/api/kv.rb
|
133
134
|
- lib/vault/api/logical.rb
|
134
135
|
- lib/vault/api/secret.rb
|
135
136
|
- lib/vault/api/sys.rb
|
@@ -140,8 +141,15 @@ files:
|
|
140
141
|
- lib/vault/api/sys/leader.rb
|
141
142
|
- lib/vault/api/sys/lease.rb
|
142
143
|
- lib/vault/api/sys/mount.rb
|
144
|
+
- lib/vault/api/sys/namespace.rb
|
143
145
|
- lib/vault/api/sys/policy.rb
|
146
|
+
- lib/vault/api/sys/quota.rb
|
144
147
|
- lib/vault/api/sys/seal.rb
|
148
|
+
- lib/vault/api/transform.rb
|
149
|
+
- lib/vault/api/transform/alphabet.rb
|
150
|
+
- lib/vault/api/transform/role.rb
|
151
|
+
- lib/vault/api/transform/template.rb
|
152
|
+
- lib/vault/api/transform/transformation.rb
|
145
153
|
- lib/vault/client.rb
|
146
154
|
- lib/vault/configurable.rb
|
147
155
|
- lib/vault/defaults.rb
|
@@ -177,8 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
177
185
|
- !ruby/object:Gem::Version
|
178
186
|
version: '0'
|
179
187
|
requirements: []
|
180
|
-
|
181
|
-
rubygems_version: 2.6.14
|
188
|
+
rubygems_version: 3.1.2
|
182
189
|
signing_key:
|
183
190
|
specification_version: 4
|
184
191
|
summary: Vault is a Ruby API client for interacting with a Vault server.
|
data/.travis.yml
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
dist: trusty
|
2
|
-
sudo: false
|
3
|
-
language: ruby
|
4
|
-
cache: bundler
|
5
|
-
|
6
|
-
env:
|
7
|
-
- VAULT_VERSION=0.8.3
|
8
|
-
- VAULT_VERSION=0.7.3
|
9
|
-
- VAULT_VERSION=0.6.5
|
10
|
-
- VAULT_VERSION=0.5.3
|
11
|
-
|
12
|
-
before_install:
|
13
|
-
- curl -sLo vault.zip https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip
|
14
|
-
- unzip vault.zip
|
15
|
-
- mkdir -p ~/bin
|
16
|
-
- mv vault ~/bin
|
17
|
-
- export PATH="~/bin:$PATH"
|
18
|
-
|
19
|
-
branches:
|
20
|
-
only:
|
21
|
-
- master
|
22
|
-
|
23
|
-
rvm:
|
24
|
-
- 2.2
|
25
|
-
- 2.3
|
26
|
-
- 2.4
|