fluent-plugin-azurestorage 0.0.8 → 0.1.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 +4 -4
- data/.gitignore +1 -0
- data/README.md +42 -0
- data/VERSION +1 -1
- data/fluent-plugin-azurestorage.gemspec +2 -2
- data/lib/fluent/plugin/out_azurestorage.rb +64 -27
- data/test/test_out_azurestorage.rb +80 -62
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19fc61140e33ed5db2e6f41736d8a41515fd2eab
|
4
|
+
data.tar.gz: c83005cb236f4b19a8c547945930e4783bd02366
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ca632bb038f91cf996ff63bb171bdda0e67baee3c6911513ef4f884862cb2fd0ea17161273518134eb952eb406b2561ae1b07968466e1cb190b112c7a7bbd65a
|
7
|
+
data.tar.gz: 12c019aa3ef994cd7089f5fe419e5ffd301226429054a85f18b94e3abf24b157c2427854755f772fa27fd726d70632fdbf21178c61fce7950253fb20bd36309f
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -16,6 +16,48 @@ $ gem install fluent-plugin-azurestorage
|
|
16
16
|
|
17
17
|
## Configuration
|
18
18
|
|
19
|
+
### v0.14 style
|
20
|
+
|
21
|
+
```
|
22
|
+
<match pattern>
|
23
|
+
type azurestorage
|
24
|
+
|
25
|
+
azure_storage_account <your azure storage account>
|
26
|
+
azure_storage_access_key <your azure storage access key>
|
27
|
+
azure_container <your azure storage container>
|
28
|
+
azure_storage_type blob
|
29
|
+
store_as gzip
|
30
|
+
auto_create_container true
|
31
|
+
path logs/
|
32
|
+
azure_object_key_format %{path}%{time_slice}_%{index}.%{file_extension}
|
33
|
+
time_slice_format %Y%m%d-%H
|
34
|
+
# if you want to use ${tag} or %Y/%m/%d/ like syntax in path / s3_object_key_format,
|
35
|
+
# need to specify tag for ${tag} and time for %Y/%m/%d in <buffer> argument.
|
36
|
+
<buffer tag,time>
|
37
|
+
@type file
|
38
|
+
path /var/log/fluent/azurestorage
|
39
|
+
timekey 3600 # 1 hour partition
|
40
|
+
timekey_wait 10m
|
41
|
+
timekey_use_utc true # use utc
|
42
|
+
</buffer>
|
43
|
+
</match>
|
44
|
+
```
|
45
|
+
|
46
|
+
For `<buffer>`, you can use any record field in `path` / `azure_object_key_format`.
|
47
|
+
|
48
|
+
```
|
49
|
+
path logs/${tag}/${foo}
|
50
|
+
<buffer tag,foo>
|
51
|
+
# parameters...
|
52
|
+
</buffer>
|
53
|
+
```
|
54
|
+
|
55
|
+
See official article for more detail: Buffer section configurations
|
56
|
+
|
57
|
+
Note that this configuration doesn't work with fluentd v0.12.
|
58
|
+
|
59
|
+
### v0.12 style
|
60
|
+
|
19
61
|
```
|
20
62
|
<match pattern>
|
21
63
|
type azurestorage
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0
|
1
|
+
0.1.0
|
@@ -17,9 +17,9 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
18
|
gem.require_paths = ['lib']
|
19
19
|
|
20
|
-
gem.add_dependency "fluentd", [">= 0.
|
20
|
+
gem.add_dependency "fluentd", [">= 0.14.0", "< 2"]
|
21
21
|
gem.add_dependency "azure", [">= 0.7.1", "<= 0.7.7"]
|
22
|
-
gem.add_dependency "
|
22
|
+
gem.add_dependency "uuidtools", ">= 2.1.5"
|
23
23
|
gem.add_development_dependency "rake", ">= 0.9.2"
|
24
24
|
gem.add_development_dependency "test-unit", ">= 3.0.8"
|
25
25
|
gem.add_development_dependency "test-unit-rr", ">= 1.0.3"
|
@@ -1,16 +1,18 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
require 'azure'
|
2
|
+
require 'fluent/plugin/upload_service'
|
3
|
+
require 'zlib'
|
4
|
+
require 'time'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'fluent/plugin/output'
|
7
|
+
|
8
|
+
module Fluent::Plugin
|
9
|
+
class AzureStorageOutput < Fluent::Plugin::Output
|
5
10
|
Fluent::Plugin.register_output('azurestorage', self)
|
6
11
|
|
12
|
+
helpers :compat_parameters, :formatter, :inject
|
13
|
+
|
7
14
|
def initialize
|
8
15
|
super
|
9
|
-
require 'azure'
|
10
|
-
require 'fluent/plugin/upload_service'
|
11
|
-
require 'zlib'
|
12
|
-
require 'time'
|
13
|
-
require 'tempfile'
|
14
16
|
|
15
17
|
@compressor = nil
|
16
18
|
end
|
@@ -26,27 +28,32 @@ module Fluent
|
|
26
28
|
config_param :format, :string, :default => "out_file"
|
27
29
|
config_param :command_parameter, :string, :default => nil
|
28
30
|
|
29
|
-
|
31
|
+
DEFAULT_FORMAT_TYPE = "out_file"
|
30
32
|
|
31
|
-
|
33
|
+
config_section :format do
|
34
|
+
config_set_default :@type, DEFAULT_FORMAT_TYPE
|
35
|
+
end
|
32
36
|
|
33
|
-
|
34
|
-
[
|
37
|
+
config_section :buffer do
|
38
|
+
config_set_default :chunk_keys, ['time']
|
39
|
+
config_set_default :timekey, (60 * 60 * 24)
|
35
40
|
end
|
36
41
|
|
42
|
+
attr_reader :bs
|
43
|
+
|
37
44
|
def configure(conf)
|
45
|
+
compat_parameters_convert(conf, :buffer, :formatter, :inject)
|
38
46
|
super
|
39
47
|
|
40
48
|
begin
|
41
49
|
@compressor = COMPRESSOR_REGISTRY.lookup(@store_as).new(:buffer_type => @buffer_type, :log => log)
|
42
50
|
rescue => e
|
43
|
-
|
51
|
+
log.warn "#{@store_as} not found. Use 'text' instead"
|
44
52
|
@compressor = TextCompressor.new
|
45
53
|
end
|
46
54
|
@compressor.configure(conf)
|
47
55
|
|
48
|
-
@formatter =
|
49
|
-
@formatter.configure(conf)
|
56
|
+
@formatter = formatter_create
|
50
57
|
|
51
58
|
if @localtime
|
52
59
|
@path_slicer = Proc.new {|path|
|
@@ -70,6 +77,13 @@ module Fluent
|
|
70
77
|
else
|
71
78
|
'blob'
|
72
79
|
end
|
80
|
+
# For backward compatibility
|
81
|
+
# TODO: Remove time_slice_format when end of support compat_parameters
|
82
|
+
@configured_time_slice_format = conf['time_slice_format']
|
83
|
+
end
|
84
|
+
|
85
|
+
def multi_workers_ready?
|
86
|
+
true
|
73
87
|
end
|
74
88
|
|
75
89
|
def start
|
@@ -88,25 +102,32 @@ module Fluent
|
|
88
102
|
end
|
89
103
|
|
90
104
|
def format(tag, time, record)
|
91
|
-
|
105
|
+
r = inject_values_to_record(tag, time, record)
|
106
|
+
@formatter.format(tag, time, r)
|
92
107
|
end
|
93
108
|
|
94
109
|
def write(chunk)
|
95
110
|
i = 0
|
111
|
+
metadata = chunk.metadata
|
96
112
|
previous_path = nil
|
113
|
+
time_slice_format = @configured_time_slice_format || timekey_to_timeformat(@buffer_config['timekey'])
|
114
|
+
time_slice = if metadata.timekey.nil?
|
115
|
+
''.freeze
|
116
|
+
else
|
117
|
+
Time.at(metadata.timekey).utc.strftime(time_slice_format)
|
118
|
+
end
|
97
119
|
|
98
120
|
begin
|
99
121
|
path = @path_slicer.call(@path)
|
100
122
|
values_for_object_key = {
|
101
|
-
"path" => path,
|
102
|
-
"time_slice" =>
|
103
|
-
"file_extension" => @compressor.ext,
|
104
|
-
"index" => i,
|
105
|
-
"uuid_flush" => uuid_random
|
106
|
-
}
|
107
|
-
storage_path = @azure_object_key_format.gsub(%r(%{[^}]+})) { |expr|
|
108
|
-
values_for_object_key[expr[2...expr.size-1]]
|
123
|
+
"%{path}" => path,
|
124
|
+
"%{time_slice}" => time_slice,
|
125
|
+
"%{file_extension}" => @compressor.ext,
|
126
|
+
"%{index}" => i,
|
127
|
+
"%{uuid_flush}" => uuid_random
|
109
128
|
}
|
129
|
+
storage_path = @azure_object_key_format.gsub(%r(%{[^}]+}), values_for_object_key)
|
130
|
+
storage_path = extract_placeholders(storage_path, metadata)
|
110
131
|
if (i > 0) && (storage_path == previous_path)
|
111
132
|
raise "duplicated path is generated. use %{index} in azure_object_key_format: path = #{storage_path}"
|
112
133
|
end
|
@@ -140,8 +161,24 @@ module Fluent
|
|
140
161
|
end
|
141
162
|
end
|
142
163
|
|
164
|
+
def uuid_random
|
165
|
+
require 'uuidtools'
|
166
|
+
::UUIDTools::UUID.random_create.to_s
|
167
|
+
end
|
168
|
+
|
169
|
+
# This is stolen from Fluentd
|
170
|
+
def timekey_to_timeformat(timekey)
|
171
|
+
case timekey
|
172
|
+
when nil then ''
|
173
|
+
when 0...60 then '%Y%m%d%H%M%S' # 60 exclusive
|
174
|
+
when 60...3600 then '%Y%m%d%H%M'
|
175
|
+
when 3600...86400 then '%Y%m%d%H'
|
176
|
+
else '%Y%m%d'
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
143
180
|
class Compressor
|
144
|
-
include Configurable
|
181
|
+
include Fluent::Configurable
|
145
182
|
|
146
183
|
def initialize(opts = {})
|
147
184
|
super()
|
@@ -220,7 +257,7 @@ module Fluent
|
|
220
257
|
end
|
221
258
|
end
|
222
259
|
|
223
|
-
COMPRESSOR_REGISTRY = Registry.new(:azurestorage_compressor_type, 'fluent/plugin/azurestorage_compressor_')
|
260
|
+
COMPRESSOR_REGISTRY = Fluent::Registry.new(:azurestorage_compressor_type, 'fluent/plugin/azurestorage_compressor_')
|
224
261
|
{
|
225
262
|
'gzip' => GzipCompressor,
|
226
263
|
'json' => JsonCompressor,
|
@@ -1,10 +1,14 @@
|
|
1
1
|
require 'fluent/test'
|
2
|
+
require 'fluent/test/driver/output'
|
3
|
+
require 'fluent/test/helpers'
|
2
4
|
require 'fluent/plugin/out_azurestorage'
|
3
5
|
|
4
6
|
require 'test/unit/rr'
|
5
7
|
require 'zlib'
|
6
8
|
require 'fileutils'
|
7
9
|
|
10
|
+
include Fluent::Test::Helpers
|
11
|
+
|
8
12
|
class AzureStorageOutputTest < Test::Unit::TestCase
|
9
13
|
def setup
|
10
14
|
require 'azure'
|
@@ -21,9 +25,16 @@ class AzureStorageOutputTest < Test::Unit::TestCase
|
|
21
25
|
]
|
22
26
|
|
23
27
|
def create_driver(conf = CONFIG)
|
24
|
-
Fluent::Test::
|
28
|
+
Fluent::Test::Driver::Output.new(Fluent::Plugin::AzureStorageOutput) do
|
29
|
+
# for testing.
|
30
|
+
def contents
|
31
|
+
@emit_streams
|
32
|
+
end
|
33
|
+
|
25
34
|
def write(chunk)
|
26
|
-
|
35
|
+
@emit_streams = []
|
36
|
+
event = chunk.read
|
37
|
+
@emit_streams << event
|
27
38
|
end
|
28
39
|
|
29
40
|
private
|
@@ -64,11 +75,9 @@ class AzureStorageOutputTest < Test::Unit::TestCase
|
|
64
75
|
conf = CONFIG.clone
|
65
76
|
conf << "\nstore_as lzo\n"
|
66
77
|
d = create_driver(conf)
|
67
|
-
|
68
|
-
assert_equal '
|
69
|
-
|
70
|
-
# TODO: replace code with disable lzop command
|
71
|
-
assert(e.is_a?(Fluent::ConfigError))
|
78
|
+
# Fallback to text/plain.
|
79
|
+
assert_equal 'txt', d.instance.instance_variable_get(:@compressor).ext
|
80
|
+
assert_equal 'text/plain', d.instance.instance_variable_get(:@compressor).content_type
|
72
81
|
end
|
73
82
|
|
74
83
|
def test_path_slicing
|
@@ -93,109 +102,118 @@ class AzureStorageOutputTest < Test::Unit::TestCase
|
|
93
102
|
def test_format
|
94
103
|
d = create_driver
|
95
104
|
|
96
|
-
time =
|
97
|
-
d.
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
d.
|
105
|
+
time = event_time("2011-01-02 13:14:15 UTC")
|
106
|
+
d.run(default_tag: "test") do
|
107
|
+
d.feed(time, {"a"=>1})
|
108
|
+
d.feed(time, {"a"=>2})
|
109
|
+
end
|
110
|
+
formatted = d.formatted
|
102
111
|
|
103
|
-
|
112
|
+
assert_equal %[2011-01-02T13:14:15Z\ttest\t{"a":1}\n], formatted[0]
|
113
|
+
assert_equal %[2011-01-02T13:14:15Z\ttest\t{"a":2}\n], formatted[1]
|
104
114
|
end
|
105
115
|
|
106
116
|
def test_format_included_tag_and_time
|
107
117
|
config = [CONFIG, 'include_tag_key true', 'include_time_key true'].join("\n")
|
108
118
|
d = create_driver(config)
|
109
119
|
|
110
|
-
time =
|
111
|
-
d.
|
112
|
-
|
120
|
+
time = event_time("2011-01-02 13:14:15 UTC")
|
121
|
+
d.run(default_tag: "test") do
|
122
|
+
d.feed(time, {"a"=>1})
|
123
|
+
d.feed(time, {"a"=>2})
|
124
|
+
end
|
125
|
+
formatted = d.formatted
|
113
126
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
d.run
|
127
|
+
assert_equal %[2011-01-02T13:14:15Z\ttest\t{"a":1,"tag":"test","time":"2011-01-02T13:14:15Z"}\n], formatted[0]
|
128
|
+
assert_equal %[2011-01-02T13:14:15Z\ttest\t{"a":2,"tag":"test","time":"2011-01-02T13:14:15Z"}\n], d.formatted[1]
|
118
129
|
end
|
119
130
|
|
120
131
|
def test_format_with_format_ltsv
|
121
132
|
config = [CONFIG, 'format ltsv'].join("\n")
|
122
133
|
d = create_driver(config)
|
123
134
|
|
124
|
-
time =
|
125
|
-
d.
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
d.
|
135
|
+
time = event_time("2011-01-02 13:14:15 UTC")
|
136
|
+
d.run(default_tag: "test") do
|
137
|
+
d.feed(time, {"a"=>1, "b"=>1})
|
138
|
+
d.feed(time, {"a"=>2, "b"=>2})
|
139
|
+
end
|
140
|
+
formatted = d.formatted
|
130
141
|
|
131
|
-
|
142
|
+
assert_equal %[a:1\tb:1\n], formatted[0]
|
143
|
+
assert_equal %[a:2\tb:2\n], formatted[1]
|
132
144
|
end
|
133
145
|
|
134
146
|
def test_format_with_format_json
|
135
147
|
config = [CONFIG, 'format json'].join("\n")
|
136
148
|
d = create_driver(config)
|
137
149
|
|
138
|
-
time =
|
139
|
-
d.
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
d.
|
150
|
+
time = event_time("2011-01-02 13:14:15 UTC")
|
151
|
+
d.run(default_tag: "test") do
|
152
|
+
d.feed(time, {"a"=>1})
|
153
|
+
d.feed(time, {"a"=>2})
|
154
|
+
end
|
155
|
+
formatted = d.formatted
|
144
156
|
|
145
|
-
|
157
|
+
assert_equal %[{"a":1}\n], formatted[0]
|
158
|
+
assert_equal %[{"a":2}\n], formatted[1]
|
146
159
|
end
|
147
160
|
|
148
161
|
def test_format_with_format_json_included_tag
|
149
162
|
config = [CONFIG, 'format json', 'include_tag_key true'].join("\n")
|
150
163
|
d = create_driver(config)
|
151
164
|
|
152
|
-
time =
|
153
|
-
d.
|
154
|
-
|
165
|
+
time = event_time("2011-01-02 13:14:15 UTC")
|
166
|
+
d.run(default_tag: "test") do
|
167
|
+
d.feed(time, {"a"=>1})
|
168
|
+
d.feed(time, {"a"=>2})
|
169
|
+
end
|
170
|
+
formatted = d.formatted
|
155
171
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
d.run
|
172
|
+
assert_equal %[{"a":1,"tag":"test"}\n], formatted[0]
|
173
|
+
assert_equal %[{"a":2,"tag":"test"}\n], formatted[1]
|
160
174
|
end
|
161
175
|
|
162
176
|
def test_format_with_format_json_included_time
|
163
177
|
config = [CONFIG, 'format json', 'include_time_key true'].join("\n")
|
164
178
|
d = create_driver(config)
|
165
179
|
|
166
|
-
time =
|
167
|
-
d.
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
d.
|
180
|
+
time = event_time("2011-01-02 13:14:15 UTC")
|
181
|
+
d.run(default_tag: "test") do
|
182
|
+
d.feed(time, {"a"=>1})
|
183
|
+
d.feed(time, {"a"=>2})
|
184
|
+
end
|
185
|
+
formatted = d.formatted
|
172
186
|
|
173
|
-
|
187
|
+
assert_equal %[{"a":1,"time":"2011-01-02T13:14:15Z"}\n], formatted[0]
|
188
|
+
assert_equal %[{"a":2,"time":"2011-01-02T13:14:15Z"}\n], formatted[1]
|
174
189
|
end
|
175
190
|
|
176
191
|
def test_format_with_format_json_included_tag_and_time
|
177
192
|
config = [CONFIG, 'format json', 'include_tag_key true', 'include_time_key true'].join("\n")
|
178
193
|
d = create_driver(config)
|
179
194
|
|
180
|
-
time =
|
181
|
-
d.
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
d.
|
195
|
+
time = event_time("2011-01-02 13:14:15 UTC")
|
196
|
+
d.run(default_tag: "test") do
|
197
|
+
d.feed(time, {"a"=>1})
|
198
|
+
d.feed(time, {"a"=>2})
|
199
|
+
end
|
200
|
+
formatted = d.formatted
|
186
201
|
|
187
|
-
|
202
|
+
assert_equal %[{"a":1,"tag":"test","time":"2011-01-02T13:14:15Z"}\n], formatted[0]
|
203
|
+
assert_equal %[{"a":2,"tag":"test","time":"2011-01-02T13:14:15Z"}\n], formatted[1]
|
188
204
|
end
|
189
205
|
|
190
206
|
def test_chunk_to_write
|
191
207
|
d = create_driver
|
192
208
|
|
193
|
-
time =
|
194
|
-
d.
|
195
|
-
|
209
|
+
time = event_time("2011-01-02 13:14:15 UTC")
|
210
|
+
d.run(default_tag: "test") do
|
211
|
+
d.feed(time, {"a"=>1})
|
212
|
+
d.feed(time, {"a"=>2})
|
213
|
+
end
|
196
214
|
|
197
|
-
#
|
198
|
-
data = d.
|
215
|
+
# Stubbed #write and #emit_streams returns chunk.read result.
|
216
|
+
data = d.instance.contents
|
199
217
|
|
200
218
|
assert_equal [%[2011-01-02T13:14:15Z\ttest\t{"a":1}\n] +
|
201
219
|
%[2011-01-02T13:14:15Z\ttest\t{"a":2}\n]],
|
@@ -216,7 +234,7 @@ class AzureStorageOutputTest < Test::Unit::TestCase
|
|
216
234
|
]
|
217
235
|
|
218
236
|
def create_time_sliced_driver(conf = CONFIG_TIME_SLICE)
|
219
|
-
d = Fluent::Test::
|
237
|
+
d = Fluent::Test::Driver::Output.new(Fluent::Plugin::AzureStorageOutput) do
|
220
238
|
end.configure(conf)
|
221
239
|
d
|
222
240
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fluent-plugin-azurestorage
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hidemasa Togashi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fluentd
|
@@ -16,7 +16,7 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.14.0
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
22
|
version: '2'
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 0.
|
29
|
+
version: 0.14.0
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '2'
|
@@ -51,19 +51,19 @@ dependencies:
|
|
51
51
|
- !ruby/object:Gem::Version
|
52
52
|
version: 0.7.7
|
53
53
|
- !ruby/object:Gem::Dependency
|
54
|
-
name:
|
54
|
+
name: uuidtools
|
55
55
|
requirement: !ruby/object:Gem::Requirement
|
56
56
|
requirements:
|
57
57
|
- - ">="
|
58
58
|
- !ruby/object:Gem::Version
|
59
|
-
version:
|
59
|
+
version: 2.1.5
|
60
60
|
type: :runtime
|
61
61
|
prerelease: false
|
62
62
|
version_requirements: !ruby/object:Gem::Requirement
|
63
63
|
requirements:
|
64
64
|
- - ">="
|
65
65
|
- !ruby/object:Gem::Version
|
66
|
-
version:
|
66
|
+
version: 2.1.5
|
67
67
|
- !ruby/object:Gem::Dependency
|
68
68
|
name: rake
|
69
69
|
requirement: !ruby/object:Gem::Requirement
|