vault-usage-client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.yardopts +1 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +58 -0
- data/README.md +165 -0
- data/Rakefile +11 -0
- data/bin/vault-usage +37 -0
- data/lib/vault-usage-client/client.rb +205 -0
- data/lib/vault-usage-client/version.rb +8 -0
- data/lib/vault-usage-client.rb +16 -0
- data/test/client_test.rb +338 -0
- data/test/helper.rb +4 -0
- data/test/version_test.rb +9 -0
- data/vault-usage-client.gemspec +21 -0
- metadata +116 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-m markdown
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
GIT
|
2
|
+
remote: https://github.com/heroku/vault-test-tools.git
|
3
|
+
revision: 5eabd67610f279b81cadb803cee8ef704093a05d
|
4
|
+
specs:
|
5
|
+
vault-test-tools (0.2.2)
|
6
|
+
minitest
|
7
|
+
nokogiri
|
8
|
+
rack-perftools_profiler
|
9
|
+
rack-test
|
10
|
+
rdoc
|
11
|
+
redcarpet
|
12
|
+
turn
|
13
|
+
yard
|
14
|
+
|
15
|
+
PATH
|
16
|
+
remote: .
|
17
|
+
specs:
|
18
|
+
vault-usage-client (0.0.1)
|
19
|
+
colorize
|
20
|
+
excon
|
21
|
+
yajl-ruby
|
22
|
+
|
23
|
+
GEM
|
24
|
+
remote: http://rubygems.org/
|
25
|
+
specs:
|
26
|
+
ansi (1.4.3)
|
27
|
+
colorize (0.5.8)
|
28
|
+
excon (0.16.10)
|
29
|
+
json (1.7.6)
|
30
|
+
minitest (4.6.0)
|
31
|
+
nokogiri (1.5.6)
|
32
|
+
open4 (1.3.0)
|
33
|
+
perftools.rb (2.0.0)
|
34
|
+
rack (1.5.2)
|
35
|
+
rack-perftools_profiler (0.6.0)
|
36
|
+
open4 (~> 1.0)
|
37
|
+
perftools.rb (~> 2.0.0)
|
38
|
+
rack (~> 1.0)
|
39
|
+
rack-test (0.6.2)
|
40
|
+
rack (>= 1.0)
|
41
|
+
rake (10.0.3)
|
42
|
+
rdoc (3.12.1)
|
43
|
+
json (~> 1.4)
|
44
|
+
redcarpet (2.2.2)
|
45
|
+
rr (1.0.4)
|
46
|
+
turn (0.9.6)
|
47
|
+
ansi
|
48
|
+
yajl-ruby (1.1.0)
|
49
|
+
yard (0.8.4.1)
|
50
|
+
|
51
|
+
PLATFORMS
|
52
|
+
ruby
|
53
|
+
|
54
|
+
DEPENDENCIES
|
55
|
+
rake
|
56
|
+
rr
|
57
|
+
vault-test-tools (~> 0.2.2)!
|
58
|
+
vault-usage-client!
|
data/README.md
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# Vault::Usage::Client
|
2
|
+
|
3
|
+
Client provides a Ruby API for interacting with the `Vault::Usage`
|
4
|
+
HTTP API.
|
5
|
+
|
6
|
+
## Setting up a development environment
|
7
|
+
|
8
|
+
Install dependencies and run the test suite:
|
9
|
+
|
10
|
+
bundle install --binstubs vendor/bin
|
11
|
+
rbenv rehash
|
12
|
+
rake
|
13
|
+
|
14
|
+
Run tests:
|
15
|
+
|
16
|
+
rake test
|
17
|
+
|
18
|
+
See tasks:
|
19
|
+
|
20
|
+
rake -T
|
21
|
+
|
22
|
+
Generate API documentation:
|
23
|
+
|
24
|
+
rake yard
|
25
|
+
|
26
|
+
## Using the client from the command-line
|
27
|
+
|
28
|
+
The `bin/vault-usage` command-line tool can be used to quickly see
|
29
|
+
usage data for a particular user:
|
30
|
+
|
31
|
+
```bash
|
32
|
+
export VAULT_USAGE_URL=https://username:secret@vault-usage.herokuapp.com
|
33
|
+
bin/vault-usage user123@heroku.com 2013-01-01 2013-02-01
|
34
|
+
```
|
35
|
+
|
36
|
+
## Using the client in Ruby
|
37
|
+
|
38
|
+
The `Vault::Usage` API may only be accessed with valid credentials.
|
39
|
+
You must supply these when you create a new client:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
require 'vault-usage-client'
|
43
|
+
|
44
|
+
client = Vault::Usage::Client::Client.new(
|
45
|
+
'https://username:secret@vault-usage.herokuapp.com')
|
46
|
+
```
|
47
|
+
|
48
|
+
### Usage events
|
49
|
+
|
50
|
+
Usage events represent usage of a product, for a period of time, by an
|
51
|
+
app or user.
|
52
|
+
|
53
|
+
#### Opening a usage event
|
54
|
+
|
55
|
+
Each usage event must have a unique UUID provided by the service
|
56
|
+
reporting it and the start time must be in UTC:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
event_id = SecureRandom.uuid
|
60
|
+
product_name = 'platform:dyno:logical'
|
61
|
+
consumer_hid = 'app1234@heroku.com'
|
62
|
+
start_time = Time.now.getutc
|
63
|
+
client.open_usage_event(event_id, product_name, consumer_hid, start_time)
|
64
|
+
```
|
65
|
+
|
66
|
+
Arbitrary data related to the usage event can optionally be provided
|
67
|
+
by way of a detail object:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
event_id = SecureRandom.uuid
|
71
|
+
product_name = 'platform:dyno:logical'
|
72
|
+
consumer_hid = 'app1234@heroku.com'
|
73
|
+
start_time = Time.now.getutc
|
74
|
+
detail = {type: 'web',
|
75
|
+
description: 'bundle exec bin/web',
|
76
|
+
kernel: 'us-east-1-a'}
|
77
|
+
client.open_usage_event(event_id, product_name, consumer_hid, start_time,
|
78
|
+
detail)
|
79
|
+
```
|
80
|
+
|
81
|
+
Keys in the detail object must be of type `Symbol` and values may only
|
82
|
+
be of type `String`, `Fixnum`, `Bignum`, `Float`, `TrueClass`,
|
83
|
+
`FalseClass` or `NilClass`. In other words, it's not possible to use
|
84
|
+
nested structures in the detail object.
|
85
|
+
|
86
|
+
#### Closing a usage event
|
87
|
+
|
88
|
+
Closing a usage event works the same way as opening one:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
event_id = SecureRandom.uuid
|
92
|
+
product_name = 'platform:dyno:logical'
|
93
|
+
consumer_hid = 'app1234@heroku.com'
|
94
|
+
stop_time = Time.now.getutc
|
95
|
+
client.close_event(event_id, product_name, consumer_hid, stop_time)
|
96
|
+
```
|
97
|
+
|
98
|
+
#### Retrieving usage information
|
99
|
+
|
100
|
+
Usage information for a particular user can be retrieved by the
|
101
|
+
client. The start and stop time must both be specified in UTC:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
user_hid = 'user1234@heroku.com'
|
105
|
+
start_time = Time.utc(2013, 1)
|
106
|
+
stop_time = Time.utc(2013, 2)
|
107
|
+
events = client.usage_for_user(user_hid, start_time, stop_time)
|
108
|
+
```
|
109
|
+
|
110
|
+
The `events` result is an `Array` of objects matching this format:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
[{id: '<event-uuid>',
|
114
|
+
product: '<name>',
|
115
|
+
consumer: '<consumer-hid>',
|
116
|
+
start_time: <Time>,
|
117
|
+
stop_time: <Time>,
|
118
|
+
detail: {<key1>: <value1>,
|
119
|
+
<key2>: <value2>,
|
120
|
+
...}},
|
121
|
+
...]}
|
122
|
+
```
|
123
|
+
|
124
|
+
In some cases it can be useful to exclude event data for certain
|
125
|
+
products:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
user_hid = 'user1234@heroku.com'
|
129
|
+
start_time = Time.utc(2013, 1)
|
130
|
+
stop_time = Time.utc(2013, 2)
|
131
|
+
events = client.usage_for_user(user_hid, start_time, stop_time,
|
132
|
+
['platform:dyno:physical'])
|
133
|
+
```
|
134
|
+
|
135
|
+
You can pass one or more product names to exclude.
|
136
|
+
|
137
|
+
### App ownership events
|
138
|
+
|
139
|
+
App ownership events represent ownership of an app, for a period of
|
140
|
+
time, by a user.
|
141
|
+
|
142
|
+
#### Opening an app ownership event
|
143
|
+
|
144
|
+
Each app ownership event must have a unique UUID provided by the
|
145
|
+
service reporting it and the start time must be in UTC:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
event_id = SecureRandom.uuid
|
149
|
+
user_hid = 'user1234@heroku.com'
|
150
|
+
app_hid = 'app1234@heroku.com'
|
151
|
+
start_time = Time.now.getutc
|
152
|
+
client.open_app_ownership_event(event_id, user_hid, app_hid, start_time)
|
153
|
+
```
|
154
|
+
|
155
|
+
#### Closing an app ownership event
|
156
|
+
|
157
|
+
Closing an app ownership event works the same way as opening one:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
event_id = SecureRandom.uuid
|
161
|
+
user_hid = 'user1234@heroku.com'
|
162
|
+
app_hid = 'app1234@heroku.com'
|
163
|
+
stop_time = Time.now.getutc
|
164
|
+
client.close_app_ownership_event(event_id, user_hid, app_hid, stop_time)
|
165
|
+
```
|
data/Rakefile
ADDED
data/bin/vault-usage
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib = File.expand_path('../../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'vault-usage-client'
|
7
|
+
require 'colorize'
|
8
|
+
|
9
|
+
user_hid = ARGV[0]
|
10
|
+
start_time = Time.utc(*ARGV[1].split('-'))
|
11
|
+
stop_time = Time.utc(*ARGV[2].split('-'))
|
12
|
+
|
13
|
+
client = Vault::Usage::Client.new
|
14
|
+
|
15
|
+
all_events = client.usage_for_user(user_hid, start_time, stop_time, ['platform:dyno:physical'])[:events]
|
16
|
+
|
17
|
+
all_events.group_by { |e| e[:consumer] }.each do |consumer, app_events|
|
18
|
+
puts "=== #{consumer} ===".blue
|
19
|
+
|
20
|
+
app_events.group_by { |e| e[:product] }.each do |product, product_events|
|
21
|
+
|
22
|
+
puts " #{product}"
|
23
|
+
product_events.
|
24
|
+
map { |event| OpenStruct.new(event) }.
|
25
|
+
sort_by(&:start_time).
|
26
|
+
each do |e|
|
27
|
+
puts " #{e.id}".light_green
|
28
|
+
puts " #{e.start_time} -> #{e.stop_time}: #{e.detail}".green
|
29
|
+
if e.start_time > e.stop_time
|
30
|
+
puts " ERROR: start_time is after stop_time".red
|
31
|
+
end
|
32
|
+
puts
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
puts
|
37
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
module Vault::Usage
|
2
|
+
# Client for the `Vault::Usage` HTTP API.
|
3
|
+
class Client
|
4
|
+
attr_reader :url
|
5
|
+
|
6
|
+
# Raised if a non-UTC time is used with the client.
|
7
|
+
class InvalidTimeError < Exception
|
8
|
+
end
|
9
|
+
|
10
|
+
# Instantiate a client.
|
11
|
+
#
|
12
|
+
# @param url [String] The URL to connect to. Include the username and
|
13
|
+
# password to use when connecting.
|
14
|
+
def initialize(url = nil)
|
15
|
+
@url = url || ENV['VAULT_USAGE_URL']
|
16
|
+
end
|
17
|
+
|
18
|
+
# Report that usage of a product, by a user or app, started at a
|
19
|
+
# particular time.
|
20
|
+
#
|
21
|
+
# @param event_id [String] A UUID that uniquely identifies the usage
|
22
|
+
# event.
|
23
|
+
# @param product_name [String] The name of the product that was used, such
|
24
|
+
# as `platform:dyno:logical` or `addon:memcache:100mb`.
|
25
|
+
# @param consumer_hid [String] The Heroku ID, such as `app1234@heroku.com`
|
26
|
+
# or `user1234@heroku.com`, that represents the user or app that used
|
27
|
+
# the specified product.
|
28
|
+
# @param start_time [Time] The beginning of the usage period, always in
|
29
|
+
# UTC.
|
30
|
+
# @param detail [Hash] Optionally, additional details to store with the
|
31
|
+
# event. Keys must be of type `Symbol` and values may only be of type
|
32
|
+
# `String`, `Fixnum`, `Bignum`, `Float`, `TrueClass`, `FalseClass` or
|
33
|
+
# `NilClass`.
|
34
|
+
# @raise [InvalidTimeError] Raised if a non-UTC start time is provided.
|
35
|
+
# @raise [Excon::Errors::HTTPStatusError] Raised if the server returns an
|
36
|
+
# unsuccessful HTTP status code.
|
37
|
+
def open_usage_event(event_id, product_name, consumer_hid, start_time,
|
38
|
+
detail=nil)
|
39
|
+
unless start_time.zone.eql?('UTC')
|
40
|
+
raise InvalidTimeError.new('Start time must be in UTC.')
|
41
|
+
end
|
42
|
+
path = "/products/#{product_name}/usage/#{consumer_hid}" +
|
43
|
+
"/events/#{event_id}/open/#{iso_format(start_time)}"
|
44
|
+
unless detail.nil?
|
45
|
+
headers = {'Content-Type' => 'application/json'}
|
46
|
+
body = JSON.generate(detail)
|
47
|
+
end
|
48
|
+
connection = Excon.new(@url)
|
49
|
+
connection.put(path: path, headers: headers, body: body,
|
50
|
+
expects: [201])
|
51
|
+
end
|
52
|
+
|
53
|
+
# Report that usage of a product, by a user or app, stopped at a
|
54
|
+
# particular time.
|
55
|
+
#
|
56
|
+
# @param event_id [String] A UUID that uniquely identifies the usage
|
57
|
+
# event.
|
58
|
+
# @param product_name [String] The name of the product that was used, such
|
59
|
+
# as `platform:dyno:logical` or `addon:memcache:100mb`.
|
60
|
+
# @param consumer_hid [String] The Heroku ID, such as `app1234@heroku.com`
|
61
|
+
# or `user1234@heroku.com`, that represents the user or app that used
|
62
|
+
# the specified product.
|
63
|
+
# @param stop_time [Time] The end of the usage period, always in UTC.
|
64
|
+
# @raise [InvalidTimeError] Raised if a non-UTC stop time is provided.
|
65
|
+
# @raise [Excon::Errors::HTTPStatusError] Raised if the server returns an
|
66
|
+
# unsuccessful HTTP status code.
|
67
|
+
def close_usage_event(event_id, product_name, consumer_hid, stop_time)
|
68
|
+
unless stop_time.zone.eql?('UTC')
|
69
|
+
raise InvalidTimeError.new('Stop time must be in UTC.')
|
70
|
+
end
|
71
|
+
path = "/products/#{product_name}/usage/#{consumer_hid}" +
|
72
|
+
"/events/#{event_id}/close/#{iso_format(stop_time)}"
|
73
|
+
connection = Excon.new(@url)
|
74
|
+
connection.put(path: path, expects: [201])
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get the usage events for the apps owned by the specified user during the
|
78
|
+
# specified period.
|
79
|
+
#
|
80
|
+
# @param user_hid [String] The user HID, such as `user1234@heroku.com`, to
|
81
|
+
# fetch usage data for.
|
82
|
+
# @param start_time [Time] The beginning of the usage period, always in
|
83
|
+
# UTC, within which events must overlap to be included in usage data.
|
84
|
+
# @param stop_time [Time] The end of the usage period, always in UTC,
|
85
|
+
# within which events must overlap to be included in usage data.
|
86
|
+
# @param exclude [Array] Optionally, a list of product names, such as
|
87
|
+
# `['platform:dyno:physical', 'addon:memcache:100mb']`, to be excluded
|
88
|
+
# from usage data.
|
89
|
+
# @raise [InvalidTimeError] Raised if a non-UTC start or stop time is
|
90
|
+
# provided.
|
91
|
+
# @raise [Excon::Errors::HTTPStatusError] Raised if the server returns an
|
92
|
+
# unsuccessful HTTP status code.
|
93
|
+
# @return [Array] A list of usage events for the specified user, matching
|
94
|
+
# the following format:
|
95
|
+
#
|
96
|
+
# ```
|
97
|
+
# [{id: '<event-uuid>',
|
98
|
+
# product: '<name>',
|
99
|
+
# consumer: '<heroku-id>',
|
100
|
+
# start_time: <Time>,
|
101
|
+
# stop_time: <Time>,
|
102
|
+
# detail: {<key1>: <value1>,
|
103
|
+
# <key2>: <value2>,
|
104
|
+
# ...}},
|
105
|
+
# ...]}
|
106
|
+
# ```
|
107
|
+
def usage_for_user(user_hid, start_time, stop_time, exclude=nil)
|
108
|
+
unless start_time.zone.eql?('UTC')
|
109
|
+
raise InvalidTimeError.new('Start time must be in UTC.')
|
110
|
+
end
|
111
|
+
unless stop_time.zone.eql?('UTC')
|
112
|
+
raise InvalidTimeError.new('Stop time must be in UTC.')
|
113
|
+
end
|
114
|
+
path = "/users/#{user_hid}/usage/#{iso_format(start_time)}/" +
|
115
|
+
"#{iso_format(stop_time)}"
|
116
|
+
unless exclude.nil? || exclude.empty?
|
117
|
+
query = {exclude: exclude.join(',')}
|
118
|
+
end
|
119
|
+
connection = Excon.new(@url)
|
120
|
+
response = connection.get(path: path, expects: [200], query: query)
|
121
|
+
events = JSON.parse(response.body, {symbolize_keys: true})
|
122
|
+
events.each do |event|
|
123
|
+
event.each do |key, value|
|
124
|
+
event[key] = parse_date(value) if date?(value)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Report that ownership of an app by a user started at a particular time.
|
130
|
+
#
|
131
|
+
# @param event_id [String] A UUID that uniquely identifies the ownership
|
132
|
+
# event.
|
133
|
+
# @param user_hid [String] The user HID, such as `user1234@heroku.com`,
|
134
|
+
# that owns the specified app.
|
135
|
+
# @param app_hid [String] The app HID, such as `app1234@heroku.com`, that
|
136
|
+
# is being transferred to the specified user.
|
137
|
+
# @param start_time [Time] The beginning of the ownership period, always
|
138
|
+
# in UTC.
|
139
|
+
# @raise [InvalidTimeError] Raised if a non-UTC start time is provided.
|
140
|
+
# @raise [Excon::Errors::HTTPStatusError] Raised if the server returns an
|
141
|
+
# unsuccessful HTTP status code.
|
142
|
+
def open_app_ownership_event(event_id, user_hid, app_hid, start_time)
|
143
|
+
unless start_time.zone.eql?('UTC')
|
144
|
+
raise InvalidTimeError.new('Start time must be in UTC.')
|
145
|
+
end
|
146
|
+
path = "/users/#{user_hid}/apps/#{app_hid}/open/#{event_id}" +
|
147
|
+
"/#{iso_format(start_time)}"
|
148
|
+
connection = Excon.new(@url)
|
149
|
+
connection.put(path: path, expects: [201])
|
150
|
+
end
|
151
|
+
|
152
|
+
# Report that ownership of an app by a user stopped at a particular time.
|
153
|
+
#
|
154
|
+
# @param event_id [String] A UUID that uniquely identifies the ownership
|
155
|
+
# event.
|
156
|
+
# @param user_hid [String] The user HID, such as `user1234@heroku.com`,
|
157
|
+
# that owned the specified app.
|
158
|
+
# @param app_hid [String] The app HID, such as `app1234@heroku.com`, that
|
159
|
+
# is being transferred away from the specified user.
|
160
|
+
# @param stop_time [Time] The end of the ownership period, always in UTC.
|
161
|
+
# @raise [InvalidTimeError] Raised if a non-UTC stop time is provided.
|
162
|
+
# @raise [Excon::Errors::HTTPStatusError] Raised if the server returns an
|
163
|
+
# unsuccessful HTTP status code.
|
164
|
+
def close_app_ownership_event(event_id, user_hid, app_hid, stop_time)
|
165
|
+
unless stop_time.zone.eql?('UTC')
|
166
|
+
raise InvalidTimeError.new('Stop time must be in UTC.')
|
167
|
+
end
|
168
|
+
path = "/users/#{user_hid}/apps/#{app_hid}/close/#{event_id}" +
|
169
|
+
"/#{iso_format(stop_time)}"
|
170
|
+
connection = Excon.new(@url)
|
171
|
+
connection.put(path: path, expects: [201])
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
# Convert a time to an ISO 8601 combined data and time format.
|
177
|
+
#
|
178
|
+
# @param time [Time] The time to convert to ISO 8601 format.
|
179
|
+
# @return [String] An ISO 8601 date in `YYYY-MM-DDTHH:MM:SSZ` format.
|
180
|
+
def iso_format(time)
|
181
|
+
time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
182
|
+
end
|
183
|
+
|
184
|
+
# Determine if the value is an ISO 8601 date in `YYYY-MM-DDTHH:MM:SSZ`
|
185
|
+
# format.
|
186
|
+
#
|
187
|
+
# @param value [String] The value to check for date-ness.
|
188
|
+
# @return [TrueClass,FalseClass] True if the value resembles a date,
|
189
|
+
# otherwise false.
|
190
|
+
def date?(value)
|
191
|
+
value =~ /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/
|
192
|
+
end
|
193
|
+
|
194
|
+
# Parse an ISO 8601 date in `YYYY-MM-DDTHH:MM:SSZ` format into a Time
|
195
|
+
# instance.
|
196
|
+
#
|
197
|
+
# @param value [String] The value to parse.
|
198
|
+
# @raise [ArgumentError] Raised if the value doesn't represent a valid
|
199
|
+
# date.
|
200
|
+
# @return [Time] The Time instance created from the date string in UTC.
|
201
|
+
def parse_date(value)
|
202
|
+
DateTime.parse(value).to_time.getutc
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.require :default, ENV['RACK_ENV'].to_sym
|
3
|
+
|
4
|
+
require 'excon'
|
5
|
+
require 'yajl/json_gem'
|
6
|
+
|
7
|
+
module Vault
|
8
|
+
module Usage
|
9
|
+
# Client provides a Ruby API to access the Vault::Usage HTTP API.
|
10
|
+
class Client
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'vault-usage-client/client'
|
16
|
+
require 'vault-usage-client/version'
|
data/test/client_test.rb
ADDED
@@ -0,0 +1,338 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class ClientTest < Vault::TestCase
|
4
|
+
include Vault::Test::EnvironmentHelpers
|
5
|
+
|
6
|
+
def setup
|
7
|
+
super
|
8
|
+
Excon.stubs.clear
|
9
|
+
Excon.defaults[:mock] = true
|
10
|
+
@client = Vault::Usage::Client.new(
|
11
|
+
'https://username:secret@vault-usage.herokuapp.com')
|
12
|
+
@event_id = 'd8bb95d1-6279-4488-961d-133514b772fa'
|
13
|
+
@product_name = 'platform:dyno:logical'
|
14
|
+
@app_hid = 'app123@heroku.com'
|
15
|
+
@user_hid = 'user456@heroku.com'
|
16
|
+
@start_time = Time.utc(2013, 1)
|
17
|
+
@stop_time = Time.utc(2013, 2)
|
18
|
+
end
|
19
|
+
|
20
|
+
def teardown
|
21
|
+
# FIXME This is a bit ugly, but Excon doesn't provide a builtin way to
|
22
|
+
# ensure that a request was invoked, so we have to do it ourselves.
|
23
|
+
# Without this, and the Excon.stubs.pop calls in the tests below, tests
|
24
|
+
# will pass if request logic is completely removed from application
|
25
|
+
# code. -jkakar
|
26
|
+
assert(Excon.stubs.empty?, 'Expected HTTP requests were not made.')
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
# Convert a time to an ISO 8601 combined data and time format.
|
31
|
+
def iso_format(time)
|
32
|
+
time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
33
|
+
end
|
34
|
+
|
35
|
+
# Client.new looks for VAULT_USAGE_URL if none is passed in to the constructor
|
36
|
+
def test_uses_env_for_url_if_not_given
|
37
|
+
url = 'http://foo:bar@example.com'
|
38
|
+
set_env 'VAULT_USAGE_URL', url
|
39
|
+
@client = Vault::Usage::Client.new
|
40
|
+
assert_equal(url, @client.url)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Client.open_usage_event makes a PUT request to the Vault::Usage HTTP API,
|
44
|
+
# passing the supplied credentials using HTTP basic auth, to report that
|
45
|
+
# usage of a product began at a particular time.
|
46
|
+
def test_open_usage_event
|
47
|
+
Excon.stub(method: :put) do |request|
|
48
|
+
assert_equal('Basic dXNlcm5hbWU6c2VjcmV0',
|
49
|
+
request[:headers]['Authorization'])
|
50
|
+
assert_equal('vault-usage.herokuapp.com:443', request[:host_port])
|
51
|
+
assert_equal("/products/#{@product_name}/usage/#{@app_hid}" +
|
52
|
+
"/events/#{@event_id}/open/#{iso_format(@start_time)}",
|
53
|
+
request[:path])
|
54
|
+
Excon.stubs.pop
|
55
|
+
{status: 201}
|
56
|
+
end
|
57
|
+
@client.open_usage_event(@event_id, @product_name, @app_hid, @start_time)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Client.open_usage_event optionally accepts a detail hash which is sent as
|
61
|
+
# a JSON payload in the request body when provided.
|
62
|
+
def test_open_usage_event_with_detail
|
63
|
+
detail = {type: 'web',
|
64
|
+
description: 'bundle exec bin/web',
|
65
|
+
kernel: 'us-east-1-a'}
|
66
|
+
Excon.stub(method: :put) do |request|
|
67
|
+
assert_equal('application/json', request[:headers]['Content-Type'])
|
68
|
+
assert_equal(detail, JSON.parse(request[:body], {symbolize_keys: true}))
|
69
|
+
Excon.stubs.pop
|
70
|
+
{status: 201}
|
71
|
+
end
|
72
|
+
@client.open_usage_event(@event_id, @product_name, @app_hid, @start_time,
|
73
|
+
detail)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Client.open_usage_event raises an InvalidTimeError if the start time is
|
77
|
+
# not in UTC.
|
78
|
+
def test_open_usage_event_with_non_utc_start_time
|
79
|
+
start_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
|
80
|
+
error = assert_raises Vault::Usage::Client::InvalidTimeError do
|
81
|
+
@client.open_usage_event(@event_id, @product_name, @app_hid, start_time)
|
82
|
+
end
|
83
|
+
assert_equal('Start time must be in UTC.', error.message)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Client.open_usage_event raises the appropriate
|
87
|
+
# Excon::Errors::HTTPStatusError if an unsuccessful HTTP status code is
|
88
|
+
# returned by the server.
|
89
|
+
def test_open_usage_event_with_unsuccessful_response
|
90
|
+
Excon.stub(method: :put) do |request|
|
91
|
+
Excon.stubs.pop
|
92
|
+
{status: 400, body: 'Bad inputs provided.'}
|
93
|
+
end
|
94
|
+
assert_raises Excon::Errors::BadRequest do
|
95
|
+
@client.open_usage_event(@event_id, @product_name, @app_hid, @start_time)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Client.close_usage_event makes a PUT request to the Vault::Usage HTTP
|
100
|
+
# API, passing the supplied credentials using HTTP basic auth, to report
|
101
|
+
# that usage of a product ended at a particular time.
|
102
|
+
def test_close_usage_event
|
103
|
+
Excon.stub(method: :put) do |request|
|
104
|
+
assert_equal('Basic dXNlcm5hbWU6c2VjcmV0',
|
105
|
+
request[:headers]['Authorization'])
|
106
|
+
assert_equal('vault-usage.herokuapp.com:443', request[:host_port])
|
107
|
+
assert_equal("/products/#{@product_name}/usage/#{@app_hid}" +
|
108
|
+
"/events/#{@event_id}/close/#{iso_format(@stop_time)}",
|
109
|
+
request[:path])
|
110
|
+
Excon.stubs.pop
|
111
|
+
{status: 201}
|
112
|
+
end
|
113
|
+
@client.close_usage_event(@event_id, @product_name, @app_hid, @stop_time)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Client.close_usage_event raises an InvalidTimeError if the stop time is
|
117
|
+
# not in UTC.
|
118
|
+
def test_close_usage_event_with_non_utc_stop_time
|
119
|
+
stop_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
|
120
|
+
error = assert_raises Vault::Usage::Client::InvalidTimeError do
|
121
|
+
@client.close_usage_event(@event_id, @product_name, @app_hid, stop_time)
|
122
|
+
end
|
123
|
+
assert_equal('Stop time must be in UTC.', error.message)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Client.close_usage_event raises the appropriate
|
127
|
+
# Excon::Errors::HTTPStatusError if an unsuccessful HTTP status code is
|
128
|
+
# returned by the server.
|
129
|
+
def test_close_usage_event_with_unsuccessful_response
|
130
|
+
Excon.stub(method: :put) do |request|
|
131
|
+
Excon.stubs.pop
|
132
|
+
{status: 400, body: 'Bad inputs provided.'}
|
133
|
+
end
|
134
|
+
assert_raises Excon::Errors::BadRequest do
|
135
|
+
@client.close_usage_event(@event_id, @product_name, @app_hid, @stop_time)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Client.usage_for_user makes a GET request to the Vault::Usage HTTP API,
|
140
|
+
# passing the supplied credentials using HTTP basic auth, to retrieve the
|
141
|
+
# usage events for a particular user that occurred during the specified
|
142
|
+
# period of time.
|
143
|
+
def test_usage_for_user
|
144
|
+
Excon.stub(method: :get) do |request|
|
145
|
+
assert_equal('Basic dXNlcm5hbWU6c2VjcmV0',
|
146
|
+
request[:headers]['Authorization'])
|
147
|
+
assert_equal('vault-usage.herokuapp.com:443', request[:host_port])
|
148
|
+
assert_equal("/users/#{@user_hid}/usage/#{iso_format(@start_time)}/" +
|
149
|
+
"#{iso_format(@stop_time)}",
|
150
|
+
request[:path])
|
151
|
+
Excon.stubs.pop
|
152
|
+
{status: 200, body: JSON.generate([])}
|
153
|
+
end
|
154
|
+
assert_equal([], @client.usage_for_user(@user_hid, @start_time,
|
155
|
+
@stop_time))
|
156
|
+
end
|
157
|
+
|
158
|
+
# Client.usage_for_user optionally accepts a list of products to exclude.
|
159
|
+
# They're passed to the server using query string arguments.
|
160
|
+
def test_usage_for_user_with_exclude
|
161
|
+
Excon.stub(method: :get) do |request|
|
162
|
+
assert_equal({exclude: 'platform:dyno:physical'}, request[:query])
|
163
|
+
Excon.stubs.pop
|
164
|
+
{status: 200, body: JSON.generate([])}
|
165
|
+
end
|
166
|
+
assert_equal([], @client.usage_for_user(@user_hid, @start_time, @stop_time,
|
167
|
+
['platform:dyno:physical']))
|
168
|
+
end
|
169
|
+
|
170
|
+
# Client.usage_for_user comma-separates product names, when many are
|
171
|
+
# provided in the exclusion list, and passes them to the server using a
|
172
|
+
# single query argument.
|
173
|
+
def test_usage_for_user_with_many_excludes
|
174
|
+
Excon.stub(method: :get) do |request|
|
175
|
+
assert_equal({exclude: 'platform:dyno:physical,addons:memcache:100mb'},
|
176
|
+
request[:query])
|
177
|
+
Excon.stubs.pop
|
178
|
+
{status: 200, body: JSON.generate([])}
|
179
|
+
end
|
180
|
+
assert_equal([], @client.usage_for_user(@user_hid, @start_time, @stop_time,
|
181
|
+
['platform:dyno:physical',
|
182
|
+
'addons:memcache:100mb']))
|
183
|
+
end
|
184
|
+
|
185
|
+
# Client.usage_for_user with an empty product exclusion list is the same as
|
186
|
+
# not passing one at all.
|
187
|
+
def test_usage_for_user_with_empty_exclude
|
188
|
+
Excon.stub(method: :get) do |request|
|
189
|
+
assert_equal(nil, request[:query])
|
190
|
+
Excon.stubs.pop
|
191
|
+
{status: 200, body: JSON.generate([])}
|
192
|
+
end
|
193
|
+
assert_equal([], @client.usage_for_user(@user_hid, @start_time, @stop_time,
|
194
|
+
[]))
|
195
|
+
end
|
196
|
+
|
197
|
+
# Client.usage_for_user converts event start and stop times to Time
|
198
|
+
# instances.
|
199
|
+
def test_usage_for_user_converts_times
|
200
|
+
Excon.stub(method: :get) do |request|
|
201
|
+
Excon.stubs.pop
|
202
|
+
events = [{id: @event_id,
|
203
|
+
product: @product_name,
|
204
|
+
consumer: @app_hid,
|
205
|
+
start_time: iso_format(@start_time),
|
206
|
+
stop_time: iso_format(@stop_time),
|
207
|
+
detail: {}}]
|
208
|
+
{status: 200, body: JSON.generate(events)}
|
209
|
+
end
|
210
|
+
assert_equal([{id: @event_id,
|
211
|
+
product: @product_name,
|
212
|
+
consumer: @app_hid,
|
213
|
+
start_time: @start_time,
|
214
|
+
stop_time: @stop_time,
|
215
|
+
detail: {}}],
|
216
|
+
@client.usage_for_user(@user_hid, @start_time, @stop_time))
|
217
|
+
end
|
218
|
+
|
219
|
+
# Client.usage_for_user raises an InvalidTimeError if the start time is not
|
220
|
+
# in UTC.
|
221
|
+
def test_usage_for_user_with_non_utc_start_time
|
222
|
+
start_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
|
223
|
+
error = assert_raises Vault::Usage::Client::InvalidTimeError do
|
224
|
+
@client.usage_for_user(@user_hid, start_time, @stop_time)
|
225
|
+
end
|
226
|
+
assert_equal('Start time must be in UTC.', error.message)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Client.usage_for_user raises an InvalidTimeError if the stop time is not
|
230
|
+
# in UTC.
|
231
|
+
def test_usage_for_user_with_non_utc_stop_time
|
232
|
+
stop_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
|
233
|
+
error = assert_raises Vault::Usage::Client::InvalidTimeError do
|
234
|
+
@client.usage_for_user(@user_hid, @start_time, stop_time)
|
235
|
+
end
|
236
|
+
assert_equal('Stop time must be in UTC.', error.message)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Client.usage_for_user raises the appropriate Excon::Errors::HTTPStatusError
|
240
|
+
# if an unsuccessful HTTP status code is returned by the server.
|
241
|
+
def test_usage_for_user_with_unsuccessful_response
|
242
|
+
Excon.stub(method: :get) do |request|
|
243
|
+
Excon.stubs.pop
|
244
|
+
{status: 400, body: 'Bad inputs provided.'}
|
245
|
+
end
|
246
|
+
assert_raises Excon::Errors::BadRequest do
|
247
|
+
@client.usage_for_user(@user_hid, @start_time, @stop_time)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Client.open_app_ownership_event makes a PUT request to the Vault::Usage
|
252
|
+
# HTTP API, passing the supplied credentials using HTTP basic auth, to
|
253
|
+
# report that ownership of an app, by a particular user, began at a
|
254
|
+
# particular time.
|
255
|
+
def test_open_app_ownership_event
|
256
|
+
Excon.stub(method: :put) do |request|
|
257
|
+
assert_equal('Basic dXNlcm5hbWU6c2VjcmV0',
|
258
|
+
request[:headers]['Authorization'])
|
259
|
+
assert_equal('vault-usage.herokuapp.com:443', request[:host_port])
|
260
|
+
assert_equal("/users/#{@user_hid}/apps/#{@app_hid}/open/#{@event_id}" +
|
261
|
+
"/#{iso_format(@start_time)}",
|
262
|
+
request[:path])
|
263
|
+
Excon.stubs.pop
|
264
|
+
{status: 201}
|
265
|
+
end
|
266
|
+
@client.open_app_ownership_event(@event_id, @user_hid, @app_hid,
|
267
|
+
@start_time)
|
268
|
+
end
|
269
|
+
|
270
|
+
# Client.open_app_ownership_event raises an InvalidTimeError if the start
|
271
|
+
# time is not in UTC.
|
272
|
+
def test_open_app_ownership_event_with_non_utc_start_time
|
273
|
+
start_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
|
274
|
+
error = assert_raises Vault::Usage::Client::InvalidTimeError do
|
275
|
+
@client.open_app_ownership_event(@event_id, @user_hid, @app_hid,
|
276
|
+
start_time)
|
277
|
+
end
|
278
|
+
assert_equal('Start time must be in UTC.', error.message)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Client.open_app_ownership_event raises the appropriate
|
282
|
+
# Excon::Errors::HTTPStatusError if an unsuccessful HTTP status code is
|
283
|
+
# returned by the server.
|
284
|
+
def test_open_app_ownership_event_with_unsuccessful_response
|
285
|
+
Excon.stub(method: :put) do |request|
|
286
|
+
Excon.stubs.pop
|
287
|
+
{status: 400, body: 'Bad inputs provided.'}
|
288
|
+
end
|
289
|
+
assert_raises Excon::Errors::BadRequest do
|
290
|
+
@client.open_app_ownership_event(@event_id, @user_hid, @app_hid,
|
291
|
+
@start_time)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Client.close_app_ownership_event makes a PUT request to the Vault::Usage
|
296
|
+
# HTTP API, passing the supplied credentials using HTTP basic auth, to
|
297
|
+
# report that ownership of an app, by a particular user, began at a
|
298
|
+
# particular time.
|
299
|
+
def test_close_app_ownership_event
|
300
|
+
Excon.stub(method: :put) do |request|
|
301
|
+
assert_equal('Basic dXNlcm5hbWU6c2VjcmV0',
|
302
|
+
request[:headers]['Authorization'])
|
303
|
+
assert_equal('vault-usage.herokuapp.com:443', request[:host_port])
|
304
|
+
assert_equal("/users/#{@user_hid}/apps/#{@app_hid}/close/#{@event_id}" +
|
305
|
+
"/#{iso_format(@stop_time)}",
|
306
|
+
request[:path])
|
307
|
+
Excon.stubs.pop
|
308
|
+
{status: 201}
|
309
|
+
end
|
310
|
+
@client.close_app_ownership_event(@event_id, @user_hid, @app_hid,
|
311
|
+
@stop_time)
|
312
|
+
end
|
313
|
+
|
314
|
+
# Client.close_app_ownership_event raises an InvalidTimeError if the stop
|
315
|
+
# time is not in UTC.
|
316
|
+
def test_close_app_ownership_event_with_non_utc_stop_time
|
317
|
+
stop_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
|
318
|
+
error = assert_raises Vault::Usage::Client::InvalidTimeError do
|
319
|
+
@client.close_app_ownership_event(@event_id, @user_hid, @app_hid,
|
320
|
+
stop_time)
|
321
|
+
end
|
322
|
+
assert_equal('Stop time must be in UTC.', error.message)
|
323
|
+
end
|
324
|
+
|
325
|
+
# Client.close_app_ownership_event raises the appropriate
|
326
|
+
# Excon::Errors::HTTPStatusError if an unsuccessful HTTP status code is
|
327
|
+
# returned by the server.
|
328
|
+
def test_close_app_ownership_event_with_unsuccessful_response
|
329
|
+
Excon.stub(method: :put) do |request|
|
330
|
+
Excon.stubs.pop
|
331
|
+
{status: 400, body: 'Bad inputs provided.'}
|
332
|
+
end
|
333
|
+
assert_raises Excon::Errors::BadRequest do
|
334
|
+
@client.close_app_ownership_event(@event_id, @user_hid, @app_hid,
|
335
|
+
@stop_time)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'vault-usage-client/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = "vault-usage-client"
|
7
|
+
gem.version = Vault::Usage::Client::VERSION
|
8
|
+
gem.authors = ["Chris Continanza", "Jamu Kakar"]
|
9
|
+
gem.email = ["csquared@heroku.com", "jkakar@heroku.com"]
|
10
|
+
gem.description = "Client for Vault::Usage"
|
11
|
+
gem.summary = "A simple wrapper around the Vault::Usage HTTP API"
|
12
|
+
gem.homepage = "https://github.com/heroku/vault-usage-client"
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($/)
|
15
|
+
gem.test_files = gem.files.grep('^(test|spec|features)/')
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
|
18
|
+
gem.add_dependency 'excon'
|
19
|
+
gem.add_dependency 'yajl-ruby'
|
20
|
+
gem.add_dependency 'colorize'
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vault-usage-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chris Continanza
|
9
|
+
- Jamu Kakar
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-02-21 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: excon
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: yajl-ruby
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: colorize
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
description: Client for Vault::Usage
|
64
|
+
email:
|
65
|
+
- csquared@heroku.com
|
66
|
+
- jkakar@heroku.com
|
67
|
+
executables: []
|
68
|
+
extensions: []
|
69
|
+
extra_rdoc_files: []
|
70
|
+
files:
|
71
|
+
- .gitignore
|
72
|
+
- .yardopts
|
73
|
+
- Gemfile
|
74
|
+
- Gemfile.lock
|
75
|
+
- README.md
|
76
|
+
- Rakefile
|
77
|
+
- bin/vault-usage
|
78
|
+
- lib/vault-usage-client.rb
|
79
|
+
- lib/vault-usage-client/client.rb
|
80
|
+
- lib/vault-usage-client/version.rb
|
81
|
+
- test/client_test.rb
|
82
|
+
- test/helper.rb
|
83
|
+
- test/version_test.rb
|
84
|
+
- vault-usage-client.gemspec
|
85
|
+
homepage: https://github.com/heroku/vault-usage-client
|
86
|
+
licenses: []
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
hash: 3440025206376780543
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ! '>='
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
hash: 3440025206376780543
|
109
|
+
requirements: []
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 1.8.23
|
112
|
+
signing_key:
|
113
|
+
specification_version: 3
|
114
|
+
summary: A simple wrapper around the Vault::Usage HTTP API
|
115
|
+
test_files: []
|
116
|
+
has_rdoc:
|