miasma-google 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e2d2cab6950ca96ad4fb8c6a3d190bbb3a43dab3
4
+ data.tar.gz: 25e2c4132fc3a15959b1ea3fc154c0bbe84f8dc2
5
+ SHA512:
6
+ metadata.gz: 80b7edcbb1da6434ed0fc57b35c16d9b6b5b75066255c4142a2f2582939bfc4522bbc218a7a0595e842b07f296c5fb2529372cd49da76c744d84b43758ff37dd
7
+ data.tar.gz: e87bde17346316b5e81f2dcf2b952f899a58eb31f21746dd71918971d3a151b74f51937ee46f28494c12472d9d600d46535bf60dee33bc26a7b7eb33005ff48d
@@ -0,0 +1,2 @@
1
+ # v0.1.0
2
+ * Initial release with Orchestration support
data/LICENSE ADDED
File without changes
@@ -0,0 +1,46 @@
1
+ # Miasma Google
2
+
3
+ Google API plugin for the miasma cloud library
4
+
5
+ ## Supported credential attributes:
6
+
7
+ Supported attributes used in the credentials section of API
8
+ configurations:
9
+
10
+ ```ruby
11
+ Miasma.api(
12
+ :type => :orchestration,
13
+ :provider => :google,
14
+ :credentials => {
15
+ ...
16
+ }
17
+ )
18
+ ```
19
+
20
+ ### Credential attributes
21
+
22
+ ` `google_project` - ID of the google project to use
23
+ * `google_service_account_email` - Email address for service account
24
+ * `google_service_account_private_key` - Path to private key for service account
25
+ * `google_auth_scope` - Scope requested for user (default: `'cloud-platform'`)
26
+ * `google_auth_base` - URL endpoint for authorization (default: `'https://www.googleapis.com/auth'`)
27
+ * `google_assertion_target` - URL for permission assertion (default: `'https://www.googleapis.com/oauth2/v4/token'`)
28
+ * `google_assertion_expiry` - Number of seconds token is valid (default: `120`)
29
+ * `google_api_base_endpoint` - URL for requests (default: `'https://www.googleapis.com'`)
30
+
31
+ ## Current support matrix
32
+
33
+ |Model |Create|Read|Update|Delete|
34
+ |--------------|------|----|------|------|
35
+ |AutoScale | | | | |
36
+ |BlockStorage | | | | |
37
+ |Compute | | | | |
38
+ |DNS | | | | |
39
+ |LoadBalancer | | | | |
40
+ |Network | | | | |
41
+ |Orchestration | X | X | X | X |
42
+ |Queues | | | | |
43
+ |Storage | | | | |
44
+
45
+ ## Info
46
+ * Repository: https://github.com/miasma-rb/miasma-google
@@ -0,0 +1,3 @@
1
+ module MiasmaGoogle
2
+ VERSION = Gem::Version.new('0.1.0')
3
+ end
@@ -0,0 +1,261 @@
1
+ require 'miasma'
2
+ require 'base64'
3
+ require 'digest/sha2'
4
+ require 'openssl'
5
+
6
+ module Miasma
7
+
8
+ module Contrib
9
+
10
+ module Google
11
+
12
+ # Base signature class
13
+ class Signature
14
+
15
+ # @return [String] algorithm of signature
16
+ attr_reader :algorithm
17
+ # @return [String] format of signature
18
+ attr_reader :format
19
+ # @return [Smash] signature claims
20
+ attr_reader :claims
21
+
22
+ # Create a new signature
23
+ #
24
+ # @param [String, Symbol] algorithm used for signature
25
+ # @param [String, Symbol] format of signature
26
+ # @param claims [Hash] request claims
27
+ # @return [self]
28
+ def initialize(algo, fmt, clms)
29
+ @algorithm = algo
30
+ @format = fmt
31
+ @claims = clms.to_smash
32
+ end
33
+
34
+ # Generate signature
35
+ #
36
+ # @return [String]
37
+ def generate
38
+ raise NotImplementedError
39
+ end
40
+
41
+ # JSON Web Token signature
42
+ class Jwt < Signature
43
+
44
+ # Required items within claims
45
+ REQUIRED_CLAIMS = [
46
+ :iss, # email address of service account
47
+ :scope, # space-delimited list of permissions requested
48
+ :aud, # intended target of assertion
49
+ :exp, # expiration time of assertion
50
+ :iat # time assertion was issued
51
+ ]
52
+
53
+ # Create a new JWT signature instance
54
+ #
55
+ # @param private_key_path [String] private signing key path
56
+ # @param claims [Hash] request claims
57
+ # @return [self]
58
+ def initialize(private_key_path, i_claims)
59
+ super('RS256', 'JWT', i_claims)
60
+ claims[:iat] ||= Time.now.to_i
61
+ claims[:exp] ||= Time.now.to_i + 120
62
+ @private_key = private_key_path
63
+ validate_claims!
64
+ validate_key!
65
+ end
66
+
67
+ # Generate signature
68
+ #
69
+ # @return [String]
70
+ def generate
71
+ "#{encoded_header}.#{encoded_claims}.#{encoded_signature}"
72
+ end
73
+
74
+ # @return [String] encoded header
75
+ def encoded_header
76
+ Base64.urlsafe_encode64(header.to_json)
77
+ end
78
+
79
+ # @return [String] header
80
+ def header
81
+ Smash.new(
82
+ :alg => algorithm,
83
+ :typ => format
84
+ )
85
+ end
86
+
87
+ # @return [String] encoded claims set
88
+ def encoded_claims
89
+ t_claims = claims.to_smash
90
+ if(t_claims.key?(:scope))
91
+ t_claims[:scope] = [t_claims[:scope]].flatten.compact.join(' ')
92
+ end
93
+ Base64.urlsafe_encode64(t_claims.to_json)
94
+ end
95
+
96
+ # @return [String] encoded signature
97
+ def encoded_signature
98
+ Base64.urlsafe_encode64(signature)
99
+ end
100
+
101
+ # @return [String] JWT signature
102
+ def signature
103
+ token = "#{encoded_header}.#{encoded_claims}"
104
+ hasher = OpenSSL::Digest::SHA256.new
105
+ author = OpenSSL::PKey::RSA.new(File.read(@private_key))
106
+ author.sign(hasher, token)
107
+ end
108
+
109
+ # Check for required claims and raise error if unset
110
+ #
111
+ # @return [TrueClass]
112
+ # @raises [KeyError]
113
+ def validate_claims!
114
+ REQUIRED_CLAIMS.each do |claim|
115
+ unless(claims.key?(claim))
116
+ raise KeyError.new "Missing required claim key `#{claim}`"
117
+ end
118
+ end
119
+ true
120
+ end
121
+
122
+ # Check that the private key exists, is readable, and is
123
+ def validate_key!
124
+ end
125
+
126
+ end
127
+
128
+ end
129
+
130
+ module ApiCommon
131
+
132
+ def self.included(klass)
133
+ klass.class_eval do
134
+ attribute :google_service_account_email, String, :required => true
135
+ attribute :google_service_account_private_key, String, :required => true
136
+ attribute :google_auth_scope, String, :required => true, :multiple => true, :default => 'cloud-platform'
137
+ attribute :google_auth_base, String, :default => 'https://www.googleapis.com/auth'
138
+ attribute :google_assertion_target, String, :required => true, :default => 'https://www.googleapis.com/oauth2/v4/token'
139
+ attribute :google_assertion_expiry, Integer, :required => true, :default => 120
140
+ attribute :google_project, String, :required => true
141
+ attribute :google_api_base_endpoint, String, :required => true, :default => 'https://www.googleapis.com'
142
+ end
143
+
144
+ klass.const_set(:TOKEN_GRANT_TYPE, 'urn:ietf:params:oauth:grant-type:jwt-bearer')
145
+ end
146
+
147
+ # @return [String]
148
+ def endpoint
149
+ point = google_api_base_endpoint.dup
150
+ if(self.class.const_defined?(:GOOGLE_SERVICE_PATH))
151
+ point << "/#{self.class.const_get(:GOOGLE_SERVICE_PATH)}"
152
+ end
153
+ if(self.class.const_defined?(:GOOGLE_SERVICE_PROJECT) && self.class.const_get(:GOOGLE_SERVICE_PROJECT))
154
+ point << "/projects/#{google_project}"
155
+ end
156
+ point
157
+ end
158
+
159
+ # Setup for API connections
160
+ def connect
161
+ @oauth_token_information = Smash.new
162
+ end
163
+
164
+ def oauth_token_information
165
+ @oauth_token_information
166
+ end
167
+
168
+ # @return [HTTP] connection for requests (forces headers)
169
+ def connection
170
+ super.headers(
171
+ 'Authorization' => "Bearer #{client_access_token}"
172
+ )
173
+ end
174
+
175
+ # @return [Contrib::Google::Signature::Jwt]
176
+ def signer
177
+ Contrib::Google::Signature::Jwt.new(
178
+ google_service_account_private_key,
179
+ :iss => google_service_account_email,
180
+ :scope => [google_auth_scope].flatten.compact.map{|scope|
181
+ "#{google_auth_base}/#{scope}"
182
+ },
183
+ :aud => google_assertion_target,
184
+ :exp => Time.now.to_i + google_assertion_expiry
185
+ )
186
+ end
187
+
188
+ # Request a new authentication token from the remote API
189
+ #
190
+ # @return [Smash] token information - :access_token, :token_type, :expires_in, :expires_on
191
+ def request_client_token
192
+ token_signer = signer
193
+ result = HTTP.post(
194
+ google_assertion_target,
195
+ :form => {
196
+ :grant_type => self.class.const_get(:TOKEN_GRANT_TYPE),
197
+ :assertion => token_signer.generate
198
+ }
199
+ )
200
+ unless(result.code == 200)
201
+ raise Miasma::Error::ApiError.new(
202
+ 'Request for client authentication token failed',
203
+ :response => result
204
+ )
205
+ end
206
+ @oauth_token_information = MultiJson.load(result.body.to_s).to_smash
207
+ @oauth_token_information[:expires_on] = Time.at(
208
+ @oauth_token_information[:expires_in] + token_signer.claims[:iat].to_i
209
+ )
210
+ @oauth_token_information
211
+ end
212
+
213
+ # @return [String] auth token
214
+ def client_access_token
215
+ request_client_token if access_token_expired?
216
+ oauth_token_information[:access_token]
217
+ end
218
+
219
+ # @return [TrueClass, FalseClass]
220
+ def access_token_expired?
221
+ if(oauth_token_information[:expires_on])
222
+ oauth_token_information[:expires_on] < Time.now
223
+ else
224
+ true
225
+ end
226
+ end
227
+
228
+ # When in debug mode, do not retry requests
229
+ #
230
+ # @return [TrueClass, FalseClass]
231
+ def retryable_allowed?(*_)
232
+ if(ENV['DEBUG'])
233
+ false
234
+ else
235
+ super
236
+ end
237
+ end
238
+
239
+ # Define when request should be retried
240
+ #
241
+ # @param exception [Exception]
242
+ # @return [TrueClass, FalseClass]
243
+ def perform_request_retry(exception)
244
+ if(exception.is_a?(Error::ApiError::RequestError))
245
+ exception.response.code >= 500
246
+ else
247
+ false
248
+ end
249
+ end
250
+
251
+ end
252
+ end
253
+ end
254
+
255
+ Models::Storage.autoload :Google, 'miasma/contrib/google/storage'
256
+ Models::Orchestration.autoload :Google, 'miasma/contrib/google/orchestration'
257
+
258
+ # Models::Compute.autoload :Google, 'misama/contrib/google/compute'
259
+ # Models::LoadBalancer.autoload :Google, 'misama/contrib/google/load_balancer'
260
+ # Models::AutoScale.autoload :Google, 'misama/contrib/google/auto_scale'
261
+ end
@@ -0,0 +1,338 @@
1
+ require 'securerandom'
2
+ require 'miasma'
3
+
4
+ module Miasma
5
+ module Models
6
+ class Orchestration
7
+ class Google < Orchestration
8
+
9
+ include Contrib::Google::ApiCommon
10
+
11
+ GOOGLE_SERVICE_PATH = '/deploymentmanager/v2'
12
+ GOOGLE_SERVICE_PROJECT = true
13
+
14
+ # Determine stack state based on last operation information
15
+ #
16
+ # @param operation [Hash]
17
+ # @option operation [String] :operationType
18
+ # @option operation [String] :status
19
+ # @return [Symbol]
20
+ def determine_state(operation)
21
+ prefix = case operation[:operationType]
22
+ when 'insert'
23
+ 'create'
24
+ when 'update'
25
+ 'update'
26
+ when 'delete'
27
+ 'delete'
28
+ end
29
+ suffix = case operation[:status]
30
+ when 'RUNNING', 'PENDING'
31
+ 'in_progress'
32
+ when 'DONE'
33
+ 'complete'
34
+ end
35
+ if(operation[:error])
36
+ suffix = 'failed'
37
+ end
38
+ if(prefix.nil? || suffix.nil?)
39
+ :unknown
40
+ else
41
+ "#{prefix}_#{suffix}".to_sym
42
+ end
43
+ end
44
+
45
+ # Create stack data hash from information
46
+ #
47
+ # @param info [Hash]
48
+ # @option info [String] :insertTime
49
+ # @option info [String] :description
50
+ # @option info [String] :name
51
+ # @option info [Hash] :operation
52
+ # @return [Hash]
53
+ def basic_stack_data_format(info)
54
+ info = info.to_smash
55
+ Smash.new(
56
+ :id => info[:id],
57
+ :created => Time.parse(info[:insertTime]),
58
+ :updated => Time.parse(info.fetch(:operation, :endTime, info.fetch(:operation, :startTime, info[:insertTime]))),
59
+ :description => info[:description],
60
+ :name => info[:name],
61
+ :state => determine_state(info.fetch(:operation, {})),
62
+ :status => determine_state(info.fetch(:operation, {})).to_s.split('_').map(&:capitalize).join(' '),
63
+ :custom => info
64
+ )
65
+ end
66
+
67
+ # @return [Array<Stack>]
68
+ def stack_all
69
+ result = request(
70
+ :path => 'global/deployments'
71
+ )
72
+ result.fetch(:body, :deployments, []).map do |item|
73
+ new_stack = Stack.new(self)
74
+ new_stack.load_data(basic_stack_data_format(item)).valid_state
75
+ end
76
+ end
77
+
78
+ # Save stack state
79
+ #
80
+ # @param stack [Stack]
81
+ # @return [Stack]
82
+ def stack_save(stack)
83
+ unless(stack.persisted?)
84
+ result = request(
85
+ :path => 'global/deployments',
86
+ :method => :post,
87
+ :json => {
88
+ :name => stack.name,
89
+ :target => template_data_unformat(stack.template)
90
+ }
91
+ )
92
+ else
93
+ result = request(
94
+ :path => "global/deployments/#{stack.name}",
95
+ :method => :put,
96
+ :json => {
97
+ :name => stack.name,
98
+ :target => template_data_unformat(stack.template),
99
+ :fingerprint => stack.custom[:fingerprint]
100
+ }
101
+ )
102
+ end
103
+ stack.id = result.get(:body, :id)
104
+ stack.valid_state
105
+ stack.reload
106
+ end
107
+
108
+ # Fetch the stack template
109
+ #
110
+ # @param stack [Stack]
111
+ # @return [Hash]
112
+ def stack_template_load(stack)
113
+ if(stack.persisted?)
114
+ result = request(
115
+ :endpoint => stack.custom.fetch(:manifest, stack.custom.get(:update, :manifest))
116
+ )
117
+ cache_template = stack.template = template_data_format(result[:body])
118
+ stack.custom = stack.custom.merge(result[:body])
119
+ if(stack.custom['expandedConfig'])
120
+ stack.custom['expandedConfig'] = YAML.load(stack.custom['expandedConfig']).to_smash
121
+ end
122
+ if(stack.custom['layout'])
123
+ stack.custom['layout'] = YAML.load(stack.custom['layout']).to_smash
124
+ end
125
+ stack.valid_state
126
+ cache_template
127
+ else
128
+ Smash.new
129
+ end
130
+ end
131
+
132
+ # Pack template data for shipping to API
133
+ #
134
+ # @param data [Hash]
135
+ # @option data [Hash] :config
136
+ # @option data [Array<Hash>] :imports
137
+ # @return [Hash]
138
+ def template_data_unformat(data)
139
+ Hash.new.tap do |result|
140
+ if(v = data.to_smash.get(:config, :content))
141
+ result[:config] = {
142
+ :content => yamlize(v)
143
+ }
144
+ end
145
+ if(data[:imports])
146
+ result[:imports] = data[:imports].map do |item|
147
+ Smash.new(
148
+ :name => item['name'],
149
+ :content => yamlize(item['content'])
150
+ )
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ # Convert value to YAML if not string
157
+ #
158
+ # @param value [Object]
159
+ # @return [String, Object]
160
+ def yamlize(value)
161
+ unless(value.is_a?(String))
162
+ if(value.is_a?(Hash) && value.respond_to?(:to_hash))
163
+ value = value.to_hash
164
+ end
165
+ value.to_yaml(:header => true)
166
+ else
167
+ value
168
+ end
169
+ end
170
+
171
+ # Unpack received template data for local model instance
172
+ #
173
+ # @param data [Hash]
174
+ # @option data [Hash] :config
175
+ # @option data [Array<Hash>] :imports
176
+ # @return [Hash]
177
+ def template_data_format(data)
178
+ data = data.to_smash
179
+ Smash.new.tap do |result|
180
+ result[:config] = data.fetch(:config, Smash.new)
181
+ result[:imports] = data.fetch(:imports, []).map do |item|
182
+ begin
183
+ Smash.new(
184
+ :name => item[:name],
185
+ :content => YAML.load(item[:content])
186
+ )
187
+ rescue
188
+ item
189
+ end
190
+ end
191
+ if(result.get(:config, :content))
192
+ result[:config][:content] = YAML.load(result[:config][:content]) || Smash.new
193
+ else
194
+ result[:config][:content] = Smash.new
195
+ end
196
+ end
197
+ end
198
+
199
+ # Reload the stack data
200
+ #
201
+ # @param stack [Stack]
202
+ # @return [Stack]
203
+ def stack_reload(stack)
204
+ if(stack.persisted?)
205
+ result = request(
206
+ :path => "global/deployments/#{stack.name}"
207
+ )
208
+ deploy = result[:body]
209
+ stack.load_data(basic_stack_data_format(deploy)).valid_state
210
+ stack_template_load(stack)
211
+ set_outputs_if_available(stack)
212
+ end
213
+ stack
214
+ end
215
+
216
+ # Set outputs into stack instance
217
+ #
218
+ # @param stack [Stack]
219
+ # @return [TrueClass, FalseClass]
220
+ def set_outputs_if_available(stack)
221
+ outputs = extract_outputs(stack.custom.fetch(:layout, {}))
222
+ unless(outputs.empty?)
223
+ stack.outputs = outputs
224
+ stack.valid_state
225
+ true
226
+ else
227
+ false
228
+ end
229
+ end
230
+
231
+ # Extract outputs from stack hash
232
+ #
233
+ # @param stack_hash [Hash]
234
+ # @return [Array<Hash>]
235
+ def extract_outputs(stack_hash)
236
+ outputs = []
237
+ if(stack_hash[:outputs])
238
+ outputs += stack_hash[:outputs].map do |output|
239
+ Smash.new(:key => output[:name], :value => output[:finalValue])
240
+ end
241
+ end
242
+ stack_hash.fetch(:resources, []).each do |resource|
243
+ outputs += extract_outputs(resource)
244
+ end
245
+ outputs
246
+ end
247
+
248
+ # Delete stack
249
+ #
250
+ # @param stack [Stack]
251
+ # @return [TrueClass, FalseClass]
252
+ def stack_destroy(stack)
253
+ if(stack.persisted?)
254
+ request(
255
+ :path => "global/deployments/#{stack.name}",
256
+ :method => :delete
257
+ )
258
+ true
259
+ else
260
+ false
261
+ end
262
+ end
263
+
264
+ # Fetch all events
265
+ #
266
+ # @param stack [Stack]
267
+ # @return [Array<Stack::Event>]
268
+ def event_all(stack, evt_id=nil)
269
+ result = request(
270
+ :path => 'global/operations',
271
+ :params => {
272
+ :filter => "targetId eq #{stack.id}"
273
+ }
274
+ )
275
+ result.fetch(:body, :operations, []).map do |event|
276
+ status_msg = [
277
+ event[:statusMessage],
278
+ *event.fetch(:error, :errors, []).map{|e| e[:message]}
279
+ ].compact.join(' -- ')
280
+ Stack::Event.new(
281
+ stack,
282
+ :id => event[:id],
283
+ :resource_id => event[:targetId],
284
+ :resource_name => stack.name,
285
+ :resource_logical_id => stack.name,
286
+ :resource_state => determine_state(event),
287
+ :resource_status => determine_state(event).to_s.split('_').map(&:capitalize).join(' '),
288
+ :resource_status_reason => "#{event[:status]} - #{event[:progress]}% complete #{status_msg}",
289
+ :time => Time.parse(event.fetch(:startTime, event[:insertTime]))
290
+ ).valid_state
291
+ end
292
+ end
293
+
294
+ # Fetch all stack resources
295
+ #
296
+ # @param stack [Stack]
297
+ # @return [Array<Stack::Resource>]
298
+ # @todo Add status reason extraction
299
+ def resource_all(stack)
300
+ request(
301
+ :path => "global/deployments/#{stack.name}/resources"
302
+ ).fetch('body', 'resources', []).map do |resource|
303
+ Stack::Resource.new(stack,
304
+ :id => resource[:id],
305
+ :type => resource[:type],
306
+ :name => resource[:name],
307
+ :logical_id => resource[:name],
308
+ :created => Time.parse(resource[:insertTime]),
309
+ :updated => resource[:updateTime] ? Time.parse(resource[:updateTime]) : nil,
310
+ :state => :create_complete,
311
+ :status => 'OK',
312
+ :status_reason => resource.fetch(:warnings, []).map{|w| w[:message]}.join(' ')
313
+ ).valid_state
314
+ end
315
+ end
316
+
317
+ # Reload resource data
318
+ #
319
+ # @param resource [Stack::Resource]
320
+ # @return [Stack::Resource]
321
+ def resource_reload(resource)
322
+ resource.stack.resources.reload
323
+ resource.stack.resources.get(resource.id)
324
+ end
325
+
326
+ # Reload event data
327
+ #
328
+ # @param event [Stack::Event]
329
+ # @return event [Stack::Event]
330
+ def event_reload(event)
331
+ event.stack.events.reload
332
+ event.stack.events.get(event.id)
333
+ end
334
+
335
+ end
336
+ end
337
+ end
338
+ end
@@ -0,0 +1,21 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
2
+ require 'miasma-google/version'
3
+ Gem::Specification.new do |s|
4
+ s.name = 'miasma-google'
5
+ s.version = MiasmaGoogle::VERSION.version
6
+ s.summary = 'Smoggy Google API'
7
+ s.author = 'Chris Roberts'
8
+ s.email = 'code@chrisroberts.org'
9
+ s.homepage = 'https://github.com/miasma-rb/miasma-google'
10
+ s.description = 'Smoggy Google API'
11
+ s.license = 'Apache 2.0'
12
+ s.require_path = 'lib'
13
+ s.add_runtime_dependency 'miasma', '>= 0.2.12'
14
+ s.add_development_dependency 'pry'
15
+ s.add_development_dependency 'minitest'
16
+ s.add_development_dependency 'vcr'
17
+ s.add_development_dependency 'webmock'
18
+ s.add_development_dependency 'psych', '>= 2.0.8'
19
+ s.add_runtime_dependency 'mime-types'
20
+ s.files = Dir['lib/**/*'] + %w(miasma-google.gemspec README.md CHANGELOG.md LICENSE)
21
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: miasma-google
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Roberts
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-04-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: miasma
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.12
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.12
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: vcr
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: psych
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 2.0.8
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 2.0.8
97
+ - !ruby/object:Gem::Dependency
98
+ name: mime-types
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Smoggy Google API
112
+ email: code@chrisroberts.org
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - CHANGELOG.md
118
+ - LICENSE
119
+ - README.md
120
+ - lib/miasma-google/version.rb
121
+ - lib/miasma/contrib/google.rb
122
+ - lib/miasma/contrib/google/orchestration.rb
123
+ - miasma-google.gemspec
124
+ homepage: https://github.com/miasma-rb/miasma-google
125
+ licenses:
126
+ - Apache 2.0
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.4.8
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: Smoggy Google API
148
+ test_files: []
149
+ has_rdoc: