fluent-plugin-azurestorage-gen2 0.2.7 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 968d3c5f271eaabe28496c3c1a7a145fc4223068
4
- data.tar.gz: d451b9a7fee0eeef3d2dbbced493ac49495a43cd
3
+ metadata.gz: cabf8cf518b936b57ea4674553cfeb10126f0740
4
+ data.tar.gz: baba4ab34b169b1316a02d9b8a70e7bde1c373bd
5
5
  SHA512:
6
- metadata.gz: f91296dd56d58faad94aede59b47afa740c746dc5207154ec31e627790428597d08518bd679f87d6ab9f540600017f725428218297731280b476dadb1f993dac
7
- data.tar.gz: 452f1de528f620b44fa317918558218d5a757c3018d67ec6dbbf43dcd86f817c86757f1bc6eea11047c68efacebfd80b187895023a480111499ede945ceba196
6
+ metadata.gz: e7821f9a44d3520ebe7f596f7770d1fc236c5e5e40c7bb077eefefee29bca471954701e64b8b1a6b20c3aba7db2bd1013eda38c5b71a8b3ea259dd9e6b10987f
7
+ data.tar.gz: d3f03f3bae956b1ccd4566e54a563e4e5d43cea1478bc9833aec44917947fe7f5a12a6f0e918b50459b8593a4e2663d8772241be66b3845bffd13f3473eca361
data/README.md CHANGED
@@ -140,6 +140,10 @@ If that setting is disabled, the worker won't fail on initialization (getting fi
140
140
 
141
141
  The defaultt `url_domain_suffix` is `.dfs.core.windows.net`, you can override this in case of private endpoints.
142
142
 
143
+ ### url_storage_resource
144
+
145
+ The url that is used during accessing a resource. Default value: `https://storage.azure.com/`
146
+
143
147
  ### azure_object_key_format
144
148
 
145
149
  The format of Azure Storage object keys. You can use several built-in variables:
@@ -148,6 +152,7 @@ The format of Azure Storage object keys. You can use several built-in variables:
148
152
  - %{time_slice}
149
153
  - %{index}
150
154
  - %{file_extension}
155
+ - %{upload_timestamp}
151
156
 
152
157
  to decide keys dynamically.
153
158
 
@@ -155,6 +160,7 @@ to decide keys dynamically.
155
160
  %{time_slice} is the time-slice in text that are formatted with *time_slice_format*.
156
161
  %{index} is the sequential number starts from 0, increments when multiple files are uploaded to Azure Storage in the same time slice.
157
162
  %{file_extention} is always "gz" for now.
163
+ %{upload_timestamp} is an upload timestamp in text that are formatted with *upload_timestamp_format*. Difference between time_slice and upload_timestamp is that the second one is the actual system timestamp (other one is from the metadata)
158
164
 
159
165
  The default format is "%{path}%{time_slice}_%{index}.%{file_extension}".
160
166
 
@@ -279,10 +285,30 @@ Format of the time used as the file name. Default is '%Y%m%d'. Use '%Y%m%d%H' to
279
285
 
280
286
  The time to wait old logs. Default is 10 minutes.
281
287
 
288
+ ### upload_timestamp_format
289
+
290
+ Format of the upload timestamp used as the file name. Can be used instead of index in case of `write_only` option is enabled. Default value is '%H%M%S%L'.
291
+
282
292
  ### utc
283
293
 
284
294
  Use UTC instead of local time.
285
295
 
296
+ ### write_only
297
+
298
+ If that option is enabled, HEAD calls are skipped during blob operations. (so make sure to set the chunk limit to 4MB in order to avoid HEAD operation because of the append operation needs the last position of the uploaded blobs).
299
+
300
+ ### proxy_url
301
+
302
+ Proxy URL for Azure endpoint.
303
+
304
+ ### proxy_username
305
+
306
+ Proxy username for Azure proxy endpoint (used only if `proxy_url` is filled)
307
+
308
+ ### proxy_password
309
+
310
+ Proxy password for Azure `proxy_username` (used only if `proxy_url` is filled)
311
+
286
312
  ## TODOs
287
313
 
288
314
  - add storage key support
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.7
1
+ 0.3.1
@@ -21,6 +21,7 @@ Gem::Specification.new do |gem|
21
21
  gem.add_runtime_dependency 'typhoeus', '~> 1.0', '>= 1.0.1'
22
22
  gem.add_runtime_dependency 'json', '~> 2.1', '>= 2.1.0'
23
23
  gem.add_runtime_dependency "yajl-ruby", '~> 1.4'
24
+ gem.add_runtime_dependency 'concurrent-ruby', '~> 1.1', '>= 1.1.5'
24
25
  gem.add_development_dependency 'rake', '~> 12.3', '>= 12.3.1'
25
26
  gem.add_development_dependency 'test-unit', '~> 3.3', '>= 3.3.3'
26
27
  gem.add_development_dependency 'test-unit-rr', '~> 1.0', '>= 1.0.5'
@@ -6,6 +6,7 @@ require 'tempfile'
6
6
  require 'time'
7
7
  require 'typhoeus'
8
8
  require 'fluent/plugin/output'
9
+ require 'concurrent'
9
10
  require 'zlib'
10
11
 
11
12
  module Fluent::Plugin
@@ -38,9 +39,16 @@ module Fluent::Plugin
38
39
  config_param :enable_retry, :bool, :default => false
39
40
  config_param :startup_fail_on_error, :bool, :default => true
40
41
  config_param :url_domain_suffix, :string, :default => '.dfs.core.windows.net'
42
+ config_param :url_storage_resource, :string, :default => 'https://storage.azure.com/'
41
43
  config_param :format, :string, :default => "out_file"
42
44
  config_param :time_slice_format, :string, :default => '%Y%m%d'
45
+ config_param :hex_random_length, :integer, default: 4
43
46
  config_param :command_parameter, :string, :default => nil
47
+ config_param :proxy_url, :string, :default => nil
48
+ config_param :proxy_username, :string, :default => nil
49
+ config_param :proxy_password, :string, :default => nil, :secret => true
50
+ config_param :write_only, :bool, :default => false
51
+ config_param :upload_timestamp_format, :string, :default => '%H%M%S%L'
44
52
 
45
53
  DEFAULT_FORMAT_TYPE = "out_file"
46
54
  ACCESS_TOKEN_API_VERSION = "2018-02-01"
@@ -74,16 +82,6 @@ module Fluent::Plugin
74
82
 
75
83
  @formatter = formatter_create
76
84
 
77
- if @localtime
78
- @path_slicer = Proc.new {|path|
79
- Time.now.strftime(path)
80
- }
81
- else
82
- @path_slicer = Proc.new {|path|
83
- Time.now.utc.strftime(path)
84
- }
85
- end
86
-
87
85
  if @azure_container.nil?
88
86
  raise Fluent::ConfigError, "azure_container is needed"
89
87
  end
@@ -97,7 +95,7 @@ module Fluent::Plugin
97
95
  else
98
96
  @final_file_extension = @compressor.ext
99
97
  end
100
-
98
+ @values_for_object_chunk = {}
101
99
  end
102
100
 
103
101
  def multi_workers_ready?
@@ -109,12 +107,20 @@ module Fluent::Plugin
109
107
  if !@skip_container_check
110
108
  if @failsafe_container_check
111
109
  begin
112
- ensure_container
110
+ if @write_only && @auto_create_container
111
+ create_container
112
+ else
113
+ ensure_container
114
+ end
113
115
  rescue Exception => e
114
116
  log.warn("#{e.message}, container list/create failsafe is enabled. Continue without those operations.")
115
117
  end
116
118
  else
117
- ensure_container
119
+ if @write_only && @auto_create_container
120
+ create_container
121
+ else
122
+ ensure_container
123
+ end
118
124
  end
119
125
  end
120
126
  super
@@ -126,17 +132,16 @@ module Fluent::Plugin
126
132
  end
127
133
 
128
134
  def write(chunk)
129
- metadata = chunk.metadata
130
135
  if @store_as.nil? || @store_as == "none"
131
- generate_log_name(metadata, @current_index)
136
+ generate_log_name(chunk, @current_index)
132
137
  if @last_azure_storage_path != @azure_storage_path
133
138
  @current_index = 0
134
- generate_log_name(metadata, @current_index)
139
+ generate_log_name(chunk, @current_index)
135
140
  end
136
141
  raw_data = chunk.read
137
142
  unless raw_data.empty?
138
143
  log.debug "azurestorage_gen2: processing raw data", chunk_id: dump_unique_id_hex(chunk.unique_id)
139
- upload_blob(raw_data, metadata)
144
+ upload_blob(raw_data, chunk)
140
145
  end
141
146
  chunk.close rescue nil
142
147
  @last_azure_storage_path = @azure_storage_path
@@ -146,51 +151,76 @@ module Fluent::Plugin
146
151
  begin
147
152
  @compressor.compress(chunk, tmp)
148
153
  tmp.rewind
149
- generate_log_name(metadata, @current_index)
154
+ generate_log_name(chunk, @current_index)
150
155
  if @last_azure_storage_path != @azure_storage_path
151
156
  @current_index = 0
152
- generate_log_name(metadata, @current_index)
157
+ generate_log_name(chunk, @current_index)
153
158
  end
154
159
  log.debug "azurestorage_gen2: Start uploading temp file: #{tmp.path}"
155
160
  content = File.open(tmp.path, 'rb') { |file| file.read }
156
- upload_blob(content, metadata)
161
+ upload_blob(content, chunk)
157
162
  @last_azure_storage_path = @azure_storage_path
158
163
  ensure
159
164
  tmp.close(true) rescue nil
160
165
  end
166
+ @values_for_object_chunk.delete(chunk.unique_id)
161
167
  end
162
168
 
163
169
  end
164
170
 
165
171
  private
166
- def upload_blob(content, metadata)
172
+ def upload_blob(content, chunk)
167
173
  log.debug "azurestorage_gen2: Uploading blob: #{@azure_storage_path}"
168
- existing_content_length = get_blob_properties(@azure_storage_path)
169
- if existing_content_length == 0
174
+ if @write_only
170
175
  create_blob(@azure_storage_path)
176
+ append_blob(content, chunk, 0)
177
+ else
178
+ existing_content_length = get_blob_properties(@azure_storage_path)
179
+ if existing_content_length == 0
180
+ create_blob(@azure_storage_path)
181
+ end
182
+ append_blob(content, chunk, existing_content_length)
171
183
  end
172
- append_blob(content, metadata, existing_content_length)
173
184
  end
174
185
 
175
186
  private
176
- def generate_log_name(metadata, index)
187
+ def generate_log_name(chunk, index)
188
+ metadata = chunk.metadata
177
189
  time_slice = if metadata.timekey.nil?
178
190
  ''.freeze
179
191
  else
180
192
  Time.at(metadata.timekey).utc.strftime(@time_slice_format)
181
193
  end
182
- path = @path_slicer.call(@path)
183
- values_for_object_key = {
184
- "%{path}" => path,
185
- "%{time_slice}" => time_slice,
194
+ if @localtime
195
+ hms_slicer = Time.now.strftime("%H%M%S")
196
+ upload_timestamp = Time.now.strftime(@upload_timestamp_format)
197
+ else
198
+ hms_slicer = Time.now.utc.strftime("%H%M%S")
199
+ upload_timestamp = Time.now.utc.strftime(@upload_timestamp_format)
200
+ end
201
+
202
+ @values_for_object_chunk[chunk.unique_id] ||= {
203
+ "%{hex_random}" => hex_random(chunk),
204
+ }
205
+ values_for_object_key_pre = {
206
+ "%{path}" => @path,
186
207
  "%{index}" => index,
187
208
  "%{uuid_flush}" => uuid_random,
188
- "%{file_extension}" => @final_file_extension
209
+ "%{file_extension}" => @final_file_extension,
210
+ "%{upload_timestamp}" => upload_timestamp,
189
211
  }
190
- storage_path = @azure_object_key_format.gsub(%r(%{[^}]+}), values_for_object_key)
191
- extracted_path = extract_placeholders(storage_path, metadata)
192
- extracted_path = "/" + extracted_path unless extracted_path.start_with?("/")
193
- @azure_storage_path = extracted_path
212
+ values_for_object_key_post = {
213
+ "%{date_slice}" => time_slice,
214
+ "%{time_slice}" => time_slice,
215
+ "%{hms_slice}" => hms_slicer,
216
+ }.merge!(@values_for_object_chunk[chunk.unique_id])
217
+ storage_path = @azure_object_key_format.gsub(%r(%{[^}]+})) do |matched_key|
218
+ values_for_object_key_pre.fetch(matched_key, matched_key)
219
+ end
220
+ storage_path = extract_placeholders(storage_path, chunk)
221
+ storage_path = storage_path.gsub(%r(%{[^}]+}), values_for_object_key_post)
222
+ storage_path = "/" + storage_path unless storage_path.start_with?("/")
223
+ @azure_storage_path = storage_path
194
224
  end
195
225
 
196
226
  def setup_access_token
@@ -242,11 +272,16 @@ module Fluent::Plugin
242
272
  # https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/tutorial-linux-vm-access-storage#get-an-access-token-and-use-it-to-call-azure-storage
243
273
  private
244
274
  def acquire_access_token_msi
245
- params = { :"api-version" => ACCESS_TOKEN_API_VERSION, :resource => "https://storage.azure.com/" }
275
+ params = { :"api-version" => ACCESS_TOKEN_API_VERSION, :resource => "#{@url_storage_resource}" }
246
276
  unless @azure_instance_msi.nil?
247
277
  params[:msi_res_id] = @azure_instance_msi
248
278
  end
249
- request = Typhoeus::Request.new("http://169.254.169.254/metadata/identity/oauth2/token", params: params, headers: { Metadata: "true"})
279
+ req_opts = {
280
+ :params => params,
281
+ :headers => { Metadata: "true" }
282
+ }
283
+ add_proxy_options(req_opts)
284
+ request = Typhoeus::Request.new("http://169.254.169.254/metadata/identity/oauth2/token", req_opts)
250
285
  request.on_complete do |response|
251
286
  if response.success?
252
287
  data = JSON.parse(response.body)
@@ -261,10 +296,16 @@ module Fluent::Plugin
261
296
 
262
297
  private
263
298
  def acquire_access_token_oauth_app
264
- params = { :"api-version" => ACCESS_TOKEN_API_VERSION, :resource => "https://storage.azure.com/"}
299
+ params = { :"api-version" => ACCESS_TOKEN_API_VERSION, :resource => "#{@url_storage_resource}"}
265
300
  headers = {:"Content-Type" => "application/x-www-form-urlencoded"}
266
- content = "grant_type=client_credentials&client_id=#{@azure_oauth_app_id}&client_secret=#{@azure_oauth_secret}&resource=https://storage.azure.com/"
267
- request = Typhoeus::Request.new("https://login.microsoftonline.com/#{@azure_oauth_tenant_id}/oauth2/token", :body => content, :headers => headers)
301
+ content = "grant_type=client_credentials&client_id=#{@azure_oauth_app_id}&client_secret=#{@azure_oauth_secret}&resource=#{@url_storage_resource}"
302
+ req_opts = {
303
+ :params => params,
304
+ :body => content,
305
+ :headers => headers
306
+ }
307
+ add_proxy_options(req_opts)
308
+ request = Typhoeus::Request.new("https://login.microsoftonline.com/#{@azure_oauth_tenant_id}/oauth2/token", req_opts)
268
309
  request.on_complete do |response|
269
310
  if response.success?
270
311
  data = JSON.parse(response.body)
@@ -279,7 +320,7 @@ module Fluent::Plugin
279
320
 
280
321
  private
281
322
  def acquire_access_token_by_az
282
- access_token=`az account get-access-token --resource https://storage.azure.com/ --query accessToken -o tsv`
323
+ access_token=`az account get-access-token --resource #{@url_storage_resource} --query accessToken -o tsv`
283
324
  log.debug "azurestorage_gen2: Token response: #{access_token}"
284
325
  @azure_access_token = access_token.chomp
285
326
  end
@@ -291,7 +332,13 @@ module Fluent::Plugin
291
332
  params = {:resource => "filesystem" }
292
333
  auth_header = create_auth_header("head", datestamp, "#{@azure_container}", headers, params)
293
334
  headers[:Authorization] = auth_header
294
- request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}", :method => :head, :params => params, :headers=> headers)
335
+ req_opts = {
336
+ :method => :head,
337
+ :params => params,
338
+ :headers => headers
339
+ }
340
+ add_proxy_options(req_opts)
341
+ request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}", req_opts)
295
342
  request.on_complete do |response|
296
343
  if response.success?
297
344
  log.info "azurestorage_gen2: Container '#{@azure_container}' exists."
@@ -318,7 +365,13 @@ module Fluent::Plugin
318
365
  params = {:resource => "filesystem" }
319
366
  auth_header = create_auth_header("put", datestamp, "#{@azure_container}", headers, params)
320
367
  headers[:Authorization] = auth_header
321
- request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}", :method => :put, :params => params, :headers=> headers)
368
+ req_opts = {
369
+ :method => :put,
370
+ :params => params,
371
+ :headers => headers
372
+ }
373
+ add_proxy_options(req_opts)
374
+ request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}", req_opts)
322
375
  request.on_complete do |response|
323
376
  if response.success?
324
377
  log.debug "azurestorage_gen2: Container '#{@azure_container}' created, response code: #{response.code}"
@@ -338,7 +391,13 @@ module Fluent::Plugin
338
391
  params = {:resource => "file", :recursive => "false"}
339
392
  auth_header = create_auth_header("put", datestamp, "#{@azure_container}#{blob_path}", headers, params)
340
393
  headers[:Authorization] = auth_header
341
- request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}#{blob_path}", :method => :put, :params => params, :headers=> headers)
394
+ req_opts = {
395
+ :method => :put,
396
+ :params => params,
397
+ :headers => headers
398
+ }
399
+ add_proxy_options(req_opts)
400
+ request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}#{blob_path}", req_opts)
342
401
  request.on_complete do |response|
343
402
  if response.success?
344
403
  log.debug "azurestorage_gen2: Blob '#{blob_path}' has been created, response code: #{response.code}"
@@ -361,7 +420,14 @@ module Fluent::Plugin
361
420
  params = {:action => "append", :position => "#{position}"}
362
421
  auth_header = create_auth_header("patch", datestamp, "#{@azure_container}#{blob_path}", headers, params)
363
422
  headers[:Authorization] = auth_header
364
- request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}#{blob_path}", :method => :patch, :headers=> headers, :params => params, :body => content)
423
+ req_opts = {
424
+ :method => :patch,
425
+ :params => params,
426
+ :headers => headers,
427
+ :body => content
428
+ }
429
+ add_proxy_options(req_opts)
430
+ request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}#{blob_path}", req_opts)
365
431
  request.on_complete do |response|
366
432
  if response.success?
367
433
  log.debug "azurestorage_gen2: Blob '#{blob_path}' has been appended, response code: #{response.code}"
@@ -386,7 +452,13 @@ module Fluent::Plugin
386
452
  params = {:action => "flush", :position => "#{position}"}
387
453
  auth_header = create_auth_header("patch", datestamp, "#{@azure_container}#{blob_path}",headers, params)
388
454
  headers[:Authorization] = auth_header
389
- request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}#{blob_path}", :method => :patch, :params => params, :headers=> headers)
455
+ req_opts = {
456
+ :method => :patch,
457
+ :params => params,
458
+ :headers => headers
459
+ }
460
+ add_proxy_options(req_opts)
461
+ request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}#{blob_path}", req_opts)
390
462
  request.on_complete do |response|
391
463
  if response.success?
392
464
  log.debug "azurestorage_gen2: Blob '#{blob_path}' flush was successful, response code: #{response.code}"
@@ -407,7 +479,13 @@ module Fluent::Plugin
407
479
  content_length = -1
408
480
  auth_header = create_auth_header("head", datestamp, "#{@azure_container}#{blob_path}", headers, params)
409
481
  headers[:Authorization] = auth_header
410
- request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}#{blob_path}", :method => :head, :params => params, :headers=> headers)
482
+ req_opts = {
483
+ :method => :head,
484
+ :params => params,
485
+ :headers => headers
486
+ }
487
+ add_proxy_options(req_opts)
488
+ request = Typhoeus::Request.new("https://#{azure_storage_account}#{@url_domain_suffix}/#{@azure_container}#{blob_path}", req_opts)
411
489
  request.on_complete do |response|
412
490
  if response.success?
413
491
  log.debug "azurestorage_gen2: Get blob properties for '#{blob_path}', response headers: #{response.headers}"
@@ -426,7 +504,7 @@ module Fluent::Plugin
426
504
  end
427
505
 
428
506
  private
429
- def append_blob(content, metadata, existing_content_length)
507
+ def append_blob(content, chunk, existing_content_length)
430
508
  position = 0
431
509
  log.debug "azurestorage_gen2: append_blob.start: Content size: #{content.length}"
432
510
  loop do
@@ -475,6 +553,16 @@ module Fluent::Plugin
475
553
  "SharedKey #{@azure_storage_account}:#{signed(method, datestamp, resource, headers, params)}"
476
554
  end
477
555
  end
556
+
557
+ private
558
+ def add_proxy_options(req_opts = {})
559
+ unless @proxy_url.nil?
560
+ req_opts[:proxy] = @proxy_url
561
+ unless @proxy_username.nil? || @proxy_password.nil?
562
+ req_opts[:proxyuserpwd] = "#{@proxy_username}:#{@proxy_password}"
563
+ end
564
+ end
565
+ end
478
566
 
479
567
  private
480
568
  def signed(method, datestamp, resource, headers, params)
@@ -544,6 +632,12 @@ module Fluent::Plugin
544
632
  require 'uuidtools'
545
633
  ::UUIDTools::UUID.random_create.to_s
546
634
  end
635
+
636
+ def hex_random(chunk)
637
+ unique_hex = Fluent::UniqueId.hex(chunk.unique_id)
638
+ unique_hex.reverse!
639
+ unique_hex[0...@hex_random_length]
640
+ end
547
641
 
548
642
  def timekey_to_timeformat(timekey)
549
643
  case timekey
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-azurestorage-gen2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Szabo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-20 00:00:00.000000000 Z
11
+ date: 2020-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fluentd
@@ -104,6 +104,26 @@ dependencies:
104
104
  - - "~>"
105
105
  - !ruby/object:Gem::Version
106
106
  version: '1.4'
107
+ - !ruby/object:Gem::Dependency
108
+ name: concurrent-ruby
109
+ requirement: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '1.1'
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 1.1.5
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.1'
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 1.1.5
107
127
  - !ruby/object:Gem::Dependency
108
128
  name: rake
109
129
  requirement: !ruby/object:Gem::Requirement