fluent-plugin-s3 1.8.3 → 1.8.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a674e172940ab48c2892af28f310ed186d382f34c73da8d9ac01287965691110
4
- data.tar.gz: ed90652d734c43c099b58d050214de9d3a033ea38b94dd9b1c9dc0a427dc9de4
3
+ metadata.gz: 4ad2143d493bcb5b5805df10225cdb068bffc8ae24a1a6838bde343b18a845ed
4
+ data.tar.gz: faf67f2a0e65e73b385944113a485f509365b3b9f1e8059ef1b06df8c6719607
5
5
  SHA512:
6
- metadata.gz: 677cc94165eeb960faf1af30fa2f1a31df4cbafa694801758f45eca7101a5370192299207a47c9b7c3fce973a4c87e97aaa53e420fdfc6b76a5ab2d1f9315c88
7
- data.tar.gz: 7b974daffad50c509fe40c5574ba63240ec75d86480d74a1ab31a9ff85018356dc3be7573965b8c9245523c2bbc4fd699da8f54a6aeeccda2a8316d57d04d9c6
6
+ metadata.gz: 51c6cd59240d0d2055260b779e9887ab5a8fc0839246cf9154566271de72e30a73ba0587ae2945dd275c1b16b71c43e165a77e43419e73279fbb938904790a48
7
+ data.tar.gz: ff8aa7a1c32cf273527165eee2dec5df4df217d20e5047c53908469bf13fd8baf6b803781e30d09126d83e0ba4658bf8b28ae2b034e403f627ae9a9e98b312a3
@@ -4,3 +4,20 @@ updates:
4
4
  directory: '/'
5
5
  schedule:
6
6
  interval: 'monthly'
7
+ groups:
8
+ # PR: "Security update [package] from [old] to [new]"
9
+ # This PR should be merged in hurry
10
+ security-updates:
11
+ applies-to: security-updates
12
+ patterns:
13
+ - '*'
14
+
15
+ # PR: "Bump [package] from [old] to [new]"
16
+ # No need to be merged this PR in hurry. It is enough to merge
17
+ # once in a month.
18
+ monthly-updates:
19
+ applies-to: version-updates
20
+ patterns:
21
+ - '*'
22
+ # Allow to create PR both of security and normal updates.
23
+ open-pull-requests-limit: 1
@@ -4,25 +4,34 @@ on:
4
4
  branches: [master]
5
5
  pull_request:
6
6
  branches: [master]
7
+ schedule:
8
+ - cron: '0 0 1 * *'
7
9
  jobs:
10
+ ruby-versions:
11
+ uses: ruby/actions/.github/workflows/ruby_versions.yml@master
12
+ with:
13
+ engine: cruby
14
+ min_version: 2.7
8
15
  build:
16
+ needs: ruby-versions
9
17
  runs-on: ${{ matrix.os }}
10
18
  strategy:
11
19
  fail-fast: false
12
20
  matrix:
13
- ruby: [ '3.4', '3.3', '3.2', '3.1', '3.0', '2.7' ]
21
+ ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
22
+ exclude:
23
+ - ruby: head
14
24
  os:
15
25
  - ubuntu-latest
16
26
  name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
17
27
  steps:
18
- - uses: actions/checkout@v4
19
- - uses: ruby/setup-ruby@v1
28
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
29
+ - uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
20
30
  with:
21
31
  ruby-version: ${{ matrix.ruby }}
22
32
  - name: unit testing
23
33
  env:
24
34
  CI: true
25
35
  run: |
26
- gem install rake
27
36
  bundle install --jobs 4 --retry 3
28
37
  bundle exec rake test
@@ -6,8 +6,11 @@ on:
6
6
  jobs:
7
7
  stale:
8
8
  runs-on: ubuntu-latest
9
+ permissions:
10
+ issues: write
11
+ pull-requests: write
9
12
  steps:
10
- - uses: actions/stale@v8
13
+ - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
11
14
  with:
12
15
  repo-token: ${{ secrets.GITHUB_TOKEN }}
13
16
  days-before-stale: 30
data/ChangeLog CHANGED
@@ -1,3 +1,16 @@
1
+ Release 1.8.5 - 2026/06/25
2
+
3
+ * in_s3: enforce size limits on decompressed payloads
4
+
5
+ Release 1.8.4 - 2026/03/04
6
+
7
+ * in_s3: add aws_profile / aws_credential_process parameters for credencials (GitHub: #464)
8
+ * out_s3: add aws_profile / aws_credential_process parameters for credencials (GitHub: #464)
9
+ * out_s3: stop unnecessary re-compression when chunk is already compressed (GitHub: #460)
10
+ * gem: use same zstd-ruby version with Fluentd (GitHub: #463)
11
+ * gem: add ostruct gem as dependency for Ruby 4.0 (GitHub: #461)
12
+ * docs: add missing zstd compression (GitHub: #459)
13
+
1
14
  Release 1.8.3 - 2025/02/18
2
15
 
3
16
  * out_s3: Add `sts_http_proxy` and `sts_endpoint_url` to web_identity_credentials (GitHub: #452)
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # Amazon S3 plugin for [Fluentd](http://github.com/fluent/fluentd)
2
2
 
3
- [<img src="https://travis-ci.org/fluent/fluent-plugin-s3.svg?branch=master"
4
- alt="Build Status" />](https://travis-ci.org/fluent/fluent-plugin-s3) [<img
3
+ [![linux](https://github.com/fluent/fluent-plugin-s3/actions/workflows/linux.yml/badge.svg)](https://github.com/fluent/fluent-plugin-s3/actions/workflows/linux.yml) [<img
5
4
  src="https://codeclimate.com/github/fluent/fluent-plugin-s3/badges/gpa.svg"
6
5
  />](https://codeclimate.com/github/fluent/fluent-plugin-s3)
7
6
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.8.3
1
+ 1.8.5
data/docs/credentials.md CHANGED
@@ -14,6 +14,23 @@ AWS access key id.
14
14
 
15
15
  AWS secret key.
16
16
 
17
+ ### aws_profile
18
+
19
+ Specify a named profile from your shared AWS configuration files (`~/.aws/credentials` or `~/.aws/config`).
20
+ This is useful for environments using IAM Roles or short-lived credentials.
21
+
22
+ ### aws_credential_process
23
+
24
+ Specify a `credential_process` command directly in the Fluentd configuration.
25
+ This enables integration with external credential helpers like IAM Roles Anywhere.
26
+
27
+ <match *>
28
+ @type s3
29
+ # Example for IAM Roles Anywhere
30
+ aws_credential_process "/path/to/aws_signing_helper credential-process --certificate /path/to/cert.pem --private-key /path/to/key.pem --trust-anchor-arn <ARN> --profile-arn <ARN> --role-arn <ARN>"
31
+ # ... other settings
32
+ </match>
33
+
17
34
  ## \<assume_role_credentials\> section
18
35
 
19
36
  Typically, you use AssumeRole for cross-account access or federation.
data/docs/input.md CHANGED
@@ -29,6 +29,11 @@ See also [Configuration: credentials](credentials.md) for common comprehensive p
29
29
 
30
30
  Whether or not object metadata should be added to the record. Defaults to `false`. See below for details.
31
31
 
32
+ ## decompression_size_limit
33
+
34
+ The size limit of the decompressed data. The default is `256m` (256 MiB).
35
+ This parameter is designed to prevent memory exhaustion when extracting highly compressed objects from S3.
36
+
32
37
  ## match_regexp
33
38
 
34
39
  If provided, process the S3 object only if its keys matches the regular expression
data/docs/output.md CHANGED
@@ -185,6 +185,7 @@ archive format on S3. You can use several format:
185
185
  * parquet (Need columnify command)
186
186
  * This compressor uses an external [columnify](https://github.com/reproio/columnify) command.
187
187
  * Use [`<compress>`](#compress-for-parquet-compressor-only) section to configure columnify command behavior.
188
+ * zstd (Need `zstd-ruby` gem, supported since v1.8.0)
188
189
 
189
190
  See [Use your compression algorithm](howto.md#use-your-compression-algorighm) section for adding another format.
190
191
 
@@ -208,7 +209,7 @@ parquet file page size. default: 8192 bytes
208
209
 
209
210
  ### parquet_row_group_size
210
211
 
211
- parquet file row group size. default: 128 MB
212
+ parquet file row group size. default: 128 MiB
212
213
 
213
214
  ### record_type
214
215
 
@@ -23,8 +23,9 @@ Gem::Specification.new do |gem|
23
23
  gem.add_development_dependency "test-unit", ">= 3.0.8"
24
24
  gem.add_development_dependency "test-unit-rr", ">= 1.0.3"
25
25
  gem.add_development_dependency "timecop"
26
+ gem.add_development_dependency "ostruct"
26
27
  # aws-sdk-core requires one of ox, oga, libxml, nokogiri or rexml,
27
28
  # and rexml is no longer default gem as of Ruby 3.0.
28
29
  gem.add_development_dependency "rexml"
29
- gem.add_development_dependency "zstd-ruby"
30
+ gem.add_development_dependency "zstd-ruby", ["~> 1.5"]
30
31
  end
@@ -8,6 +8,7 @@ require 'cgi/util'
8
8
  require 'zlib'
9
9
  require 'time'
10
10
  require 'tempfile'
11
+ require 'shellwords'
11
12
 
12
13
  module Fluent::Plugin
13
14
  class S3Input < Input
@@ -21,15 +22,22 @@ module Fluent::Plugin
21
22
  end
22
23
 
23
24
  DEFAULT_PARSE_TYPE = "none"
25
+ DECOMPRESSION_SIZE_LIMIT = 256 * 1024 * 1024
24
26
 
25
27
  desc "Use aws-sdk-ruby bundled cert"
26
28
  config_param :use_bundled_cert, :bool, default: false
27
29
  desc "Add object metadata to the records parsed out of a given object"
28
30
  config_param :add_object_metadata, :bool, default: false
31
+ desc 'The size limit of the extracted element.'
32
+ config_param :decompression_size_limit, :size, default: DECOMPRESSION_SIZE_LIMIT
29
33
  desc "AWS access key id"
30
34
  config_param :aws_key_id, :string, default: nil, secret: true
31
35
  desc "AWS secret key."
32
36
  config_param :aws_sec_key, :string, default: nil, secret: true
37
+ desc "AWS profile name."
38
+ config_param :aws_profile, :string, default: nil
39
+ desc "The command to run to generate credentials."
40
+ config_param :aws_credential_process, :string, default: nil
33
41
  config_section :assume_role_credentials, multi: false do
34
42
  desc "The Amazon Resource Name (ARN) of the role to assume"
35
43
  config_param :role_arn, :string
@@ -154,7 +162,7 @@ module Fluent::Plugin
154
162
 
155
163
  Aws.use_bundled_cert! if @use_bundled_cert
156
164
 
157
- @extractor = EXTRACTOR_REGISTRY.lookup(@store_as).new(log: log)
165
+ @extractor = EXTRACTOR_REGISTRY.lookup(@store_as).new(log: log, decompression_size_limit: @decompression_size_limit)
158
166
  @extractor.configure(conf)
159
167
 
160
168
  @parser = parser_create(conf: parser_config, default_type: DEFAULT_PARSE_TYPE)
@@ -205,13 +213,13 @@ module Fluent::Plugin
205
213
  begin
206
214
  @poller.poll(options) do |message|
207
215
  begin
208
- body = Yajl.load(message.body)
216
+ body = JSON.parse(message.body)
209
217
  log.debug(body)
210
218
  next unless is_valid_queue(body) # skip test queue
211
219
  if @match_regexp
212
220
  raw_key = get_raw_key(body)
213
221
  key = CGI.unescape(raw_key)
214
- next unless @match_regexp.match?(key)
222
+ next unless @match_regexp.match?(key)
215
223
  end
216
224
  process(body)
217
225
  rescue => e
@@ -231,7 +239,7 @@ module Fluent::Plugin
231
239
  if @sqs.event_bridge_mode
232
240
  log.debug("checking for eventbridge property")
233
241
  !!body["detail"]
234
- else
242
+ else
235
243
  log.debug("checking for Records property")
236
244
  !!body["Records"]
237
245
  end
@@ -242,7 +250,7 @@ module Fluent::Plugin
242
250
  body["detail"]["object"]["key"]
243
251
  else
244
252
  body["Records"].first["s3"]["object"]["key"]
245
- end
253
+ end
246
254
  end
247
255
 
248
256
  def setup_credentials
@@ -252,6 +260,10 @@ module Fluent::Plugin
252
260
  when @aws_key_id && @aws_sec_key
253
261
  options[:access_key_id] = @aws_key_id
254
262
  options[:secret_access_key] = @aws_sec_key
263
+ when @aws_profile
264
+ options[:profile] = @aws_profile
265
+ when @aws_credential_process
266
+ options[:credentials] = Aws::ProcessCredentials.new(Shellwords.split(@aws_credential_process))
255
267
  when @assume_role_credentials
256
268
  c = @assume_role_credentials
257
269
  credentials_options[:role_arn] = c.role_arn
@@ -356,13 +368,18 @@ module Fluent::Plugin
356
368
  end
357
369
 
358
370
  class Extractor
371
+ class SizeLimitError < StandardError; end
372
+
359
373
  include Fluent::Configurable
360
374
 
361
375
  attr_reader :log
362
376
 
363
- def initialize(log: $log, **options)
377
+ BYTES_TO_READ = 64 * 1024
378
+
379
+ def initialize(log: $log, decompression_size_limit: DECOMPRESSION_SIZE_LIMIT, **options)
364
380
  super()
365
381
  @log = log
382
+ @decompression_size_limit = decompression_size_limit
366
383
  end
367
384
 
368
385
  def configure(conf)
@@ -390,6 +407,38 @@ module Fluent::Plugin
390
407
  raise Fluent::ConfigError, "'#{command}' utility must be in PATH for #{algo} compression"
391
408
  end
392
409
  end
410
+
411
+ def extract_with_command(command, io, tempfile_basename = "s3-extractor-tmp")
412
+ path = if io.respond_to?(:path)
413
+ io.path
414
+ else
415
+ extractor = TextExtractor.new(log: log, decompression_size_limit: @decompression_size_limit)
416
+ temp = Tempfile.new(tempfile_basename)
417
+ temp.write(extractor.extract(io))
418
+ temp.close
419
+ temp.path
420
+ end
421
+
422
+ out = ''
423
+ begin
424
+ Open3.popen3("#{command} #{path}") do |stdin, stdout, stderr, wait_thr|
425
+ stdin.close
426
+ while (chunk = stdout.read(BYTES_TO_READ))
427
+ out << chunk
428
+ if out.bytesize > @decompression_size_limit
429
+ Process.kill("TERM", wait_thr.pid) rescue nil
430
+ raise SizeLimitError, "Extracted data exceeds limit of #{@decompression_size_limit} bytes"
431
+ end
432
+ end
433
+
434
+ if wait_thr.value.success?
435
+ out
436
+ else
437
+ raise "Command execution failed: #{command} (status: #{wait_thr.value})"
438
+ end
439
+ end
440
+ end
441
+ end
393
442
  end
394
443
 
395
444
  class GzipExtractor < Extractor
@@ -405,19 +454,25 @@ module Fluent::Plugin
405
454
  # https://bugs.ruby-lang.org/issues/11180
406
455
  # https://github.com/exAspArk/multiple_files_gzip_reader
407
456
  def extract(io)
408
- parts = []
457
+ out = ''
409
458
  loop do
410
459
  unused = nil
411
460
  Zlib::GzipReader.wrap(io) do |gz|
412
- parts << gz.read
461
+ while (chunk = gz.read(BYTES_TO_READ))
462
+ out << chunk
463
+ if out.bytesize > @decompression_size_limit
464
+ raise SizeLimitError, "Extracted data exceeds limit of #{@decompression_size_limit} bytes"
465
+ end
466
+ end
413
467
  unused = gz.unused
414
468
  gz.finish
415
469
  end
416
470
  io.pos -= unused ? unused.length : 0
417
471
  break if io.eof?
418
472
  end
419
- io.close
420
- parts.join
473
+ out
474
+ ensure
475
+ io.close unless io.closed?
421
476
  end
422
477
  end
423
478
 
@@ -431,7 +486,14 @@ module Fluent::Plugin
431
486
  end
432
487
 
433
488
  def extract(io)
434
- io.read
489
+ out = ''
490
+ while (chunk = io.read(BYTES_TO_READ))
491
+ out << chunk
492
+ if out.bytesize > @decompression_size_limit
493
+ raise SizeLimitError, "Extracted data exceeds limit of #{@decompression_size_limit} bytes"
494
+ end
495
+ end
496
+ out
435
497
  end
436
498
  end
437
499
 
@@ -6,6 +6,7 @@ require 'zlib'
6
6
  require 'time'
7
7
  require 'tempfile'
8
8
  require 'securerandom'
9
+ require 'shellwords'
9
10
 
10
11
  module Fluent::Plugin
11
12
  class S3Output < Output
@@ -29,6 +30,10 @@ module Fluent::Plugin
29
30
  config_param :aws_key_id, :string, default: nil, secret: true
30
31
  desc "AWS secret key."
31
32
  config_param :aws_sec_key, :string, default: nil, secret: true
33
+ desc "AWS profile name."
34
+ config_param :aws_profile, :string, default: nil
35
+ desc "The command to run to generate credentials."
36
+ config_param :aws_credential_process, :string, default: nil
32
37
  config_section :assume_role_credentials, multi: false do
33
38
  desc "The Amazon Resource Name (ARN) of the role to assume"
34
39
  config_param :role_arn, :string, secret: true
@@ -203,7 +208,7 @@ module Fluent::Plugin
203
208
 
204
209
  begin
205
210
  buffer_type = @buffer_config[:@type]
206
- @compressor = COMPRESSOR_REGISTRY.lookup(@store_as).new(buffer_type: buffer_type, log: log)
211
+ @compressor = COMPRESSOR_REGISTRY.lookup(@store_as).new(buffer_type: buffer_type, buffer_compressed_type: @buffer.compress, log: log)
207
212
  rescue => e
208
213
  log.warn "'#{@store_as}' not supported. Use 'text' instead: error = #{e.message}"
209
214
  @compressor = TextCompressor.new
@@ -542,6 +547,10 @@ module Fluent::Plugin
542
547
  when @aws_key_id && @aws_sec_key
543
548
  options[:access_key_id] = @aws_key_id
544
549
  options[:secret_access_key] = @aws_sec_key
550
+ when @aws_profile
551
+ options[:profile] = @aws_profile
552
+ when @aws_credential_process
553
+ options[:credentials] = Aws::ProcessCredentials.new(Shellwords.split(@aws_credential_process))
545
554
  when @web_identity_credentials
546
555
  c = @web_identity_credentials
547
556
  region = c.sts_region || @s3_region
@@ -600,6 +609,7 @@ module Fluent::Plugin
600
609
  def initialize(opts = {})
601
610
  super()
602
611
  @buffer_type = opts[:buffer_type]
612
+ @buffer_compressed_type = opts[:buffer_compressed_type]
603
613
  @log = opts[:log]
604
614
  end
605
615
 
@@ -642,11 +652,18 @@ module Fluent::Plugin
642
652
  end
643
653
 
644
654
  def compress(chunk, tmp)
655
+ if @buffer_compressed_type == :gzip
656
+ chunk.write_to(tmp, compressed: @buffer_compressed_type)
657
+ return
658
+ end
659
+
645
660
  w = Zlib::GzipWriter.new(tmp)
646
661
  chunk.write_to(w)
647
662
  w.finish
648
663
  ensure
649
- w.finish rescue nil
664
+ if w
665
+ w.finish rescue nil
666
+ end
650
667
  end
651
668
  end
652
669
 
@@ -19,6 +19,11 @@ module Fluent::Plugin
19
19
  end
20
20
 
21
21
  def compress(chunk, tmp)
22
+ if @buffer_compressed_type == :gzip
23
+ chunk.write_to(tmp, compressed: @buffer_compressed_type)
24
+ return
25
+ end
26
+
22
27
  chunk_is_file = @buffer_type == 'file'
23
28
  path = if chunk_is_file
24
29
  chunk.path
@@ -25,6 +25,11 @@ module Fluent::Plugin
25
25
  end
26
26
 
27
27
  def compress(chunk, tmp)
28
+ if @buffer_compressed_type == :zstd
29
+ chunk.write_to(tmp, compressed: @buffer_compressed_type)
30
+ return
31
+ end
32
+
28
33
  compressed = Zstd.compress(chunk.read, level: @compress_config.level)
29
34
  tmp.write(compressed)
30
35
  rescue => e
@@ -19,26 +19,15 @@ module Fluent::Plugin
19
19
  end
20
20
 
21
21
  def extract(io)
22
- path = if io.respond_to?(:path)
23
- io.path
24
- else
25
- temp = Tempfile.new("gzip-temp")
26
- temp.write(io.read)
27
- temp.close
28
- temp.path
29
- end
30
-
31
- stdout, succeeded = Open3.capture2("gzip #{@command_parameter} #{path}")
32
- if succeeded
33
- stdout
34
- else
35
- log.warn "failed to execute gzip command. Fallback to GzipReader. status = #{succeeded}"
36
- begin
37
- io.rewind
38
- Zlib::GzipReader.wrap(io) do |gz|
39
- gz.read
40
- end
41
- end
22
+ begin
23
+ extract_with_command("gzip #{@command_parameter}", io, "gzip-temp")
24
+ rescue SizeLimitError
25
+ raise
26
+ rescue => e
27
+ log.warn "gzip command execution failed: #{e.message}. Fallback to GzipExtractor."
28
+ io.rewind
29
+ extractor = GzipExtractor.new(log: log, decompression_size_limit: @decompression_size_limit)
30
+ extractor.extract(io)
42
31
  end
43
32
  end
44
33
  end
@@ -19,19 +19,11 @@ module Fluent::Plugin
19
19
  end
20
20
 
21
21
  def extract(io)
22
- path = if io.respond_to?(path)
23
- io.path
24
- else
25
- temp = Tempfile.new("xz-temp")
26
- temp.write(io.read)
27
- temp.close
28
- temp.path
29
- end
30
-
31
- stdout, succeeded = Open3.capture2("xz #{@command_parameter} #{path}")
32
- if succeeded
33
- stdout
34
- else
22
+ begin
23
+ extract_with_command("xz #{@command_parameter}", io, "xz-temp")
24
+ rescue SizeLimitError
25
+ raise
26
+ rescue
35
27
  raise "Failed to extract #{path} with xz command."
36
28
  end
37
29
  end
@@ -19,19 +19,11 @@ module Fluent::Plugin
19
19
  end
20
20
 
21
21
  def extract(io)
22
- path = if io.respond_to?(path)
23
- io.path
24
- else
25
- temp = Tempfile.new("lzop-temp")
26
- temp.write(io.read)
27
- temp.close
28
- temp.path
29
- end
30
-
31
- stdout, succeeded = Open3.capture2("lzop #{@command_parameter} #{path}")
32
- if succeeded
33
- stdout
34
- else
22
+ begin
23
+ extract_with_command("lzop #{@command_parameter}", io, "lzop-temp")
24
+ rescue SizeLimitError
25
+ raise
26
+ rescue
35
27
  raise "Failed to extract #{path} with lzop command."
36
28
  end
37
29
  end
data/test/test_in_s3.rb CHANGED
@@ -71,6 +71,32 @@ class S3InputTest < Test::Unit::TestCase
71
71
  end
72
72
  end
73
73
 
74
+ def test_aws_profile
75
+ d = create_driver(%[
76
+ aws_profile test_profile
77
+ s3_bucket test_bucket
78
+ buffer_type memory
79
+ <sqs>
80
+ queue_name test_queue
81
+ queue_owner_aws_account_id 123456789123
82
+ </sqs>
83
+ ])
84
+ assert_equal 'test_profile', d.instance.aws_profile
85
+ end
86
+
87
+ def test_aws_credential_process
88
+ d = create_driver(%[
89
+ aws_credential_process "/path/to/aws_signing_helper credential-process"
90
+ s3_bucket test_bucket
91
+ buffer_type memory
92
+ <sqs>
93
+ queue_name test_queue
94
+ queue_owner_aws_account_id 123456789123
95
+ </sqs>
96
+ ])
97
+ assert_equal '/path/to/aws_signing_helper credential-process', d.instance.aws_credential_process
98
+ end
99
+
74
100
  def test_without_sqs_section
75
101
  conf = %[
76
102
  aws_key_id test_key_id
@@ -315,7 +341,7 @@ EOS
315
341
  }
316
342
  ]
317
343
  }
318
- message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
344
+ message = Struct::StubMessage.new(1, 1, JSON.generate(body))
319
345
  @sqs_poller.get_messages(anything, anything) do |config, stats|
320
346
  config.before_request.call(stats) if config.before_request
321
347
  stats.request_count += 1
@@ -350,7 +376,7 @@ EOS
350
376
  }
351
377
  ]
352
378
  }
353
- message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
379
+ message = Struct::StubMessage.new(1, 1, JSON.generate(body))
354
380
  @sqs_poller.get_messages(anything, anything) do |config, stats|
355
381
  config.before_request.call(stats) if config.before_request
356
382
  stats.request_count += 1
@@ -385,7 +411,7 @@ EOS
385
411
  }
386
412
  ]
387
413
  }
388
- message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
414
+ message = Struct::StubMessage.new(1, 1, JSON.generate(body))
389
415
  @sqs_poller.get_messages(anything, anything) do |config, stats|
390
416
  config.before_request.call(stats) if config.before_request
391
417
  stats.request_count += 1
@@ -420,7 +446,7 @@ EOS
420
446
  }
421
447
  ]
422
448
  }
423
- message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
449
+ message = Struct::StubMessage.new(1, 1, JSON.generate(body))
424
450
  @sqs_poller.get_messages(anything, anything) do |config, stats|
425
451
  config.before_request.call(stats) if config.before_request
426
452
  stats.request_count += 1
@@ -455,7 +481,7 @@ EOS
455
481
  }
456
482
  ]
457
483
  }
458
- message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
484
+ message = Struct::StubMessage.new(1, 1, JSON.generate(body))
459
485
  @sqs_poller.get_messages(anything, anything) do |config, stats|
460
486
  config.before_request.call(stats) if config.before_request
461
487
  stats.request_count += 1
@@ -495,7 +521,7 @@ EOS
495
521
  }
496
522
  ]
497
523
  }
498
- message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
524
+ message = Struct::StubMessage.new(1, 1, JSON.generate(body))
499
525
  @sqs_poller.get_messages(anything, anything) do |config, stats|
500
526
  config.before_request.call(stats) if config.before_request
501
527
  stats.request_count += 1
@@ -543,7 +569,7 @@ EOS
543
569
  }
544
570
  ]
545
571
  }
546
- message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
572
+ message = Struct::StubMessage.new(1, 1, JSON.generate(body))
547
573
  @sqs_poller.get_messages(anything, anything) do |config, stats|
548
574
  config.before_request.call(stats) if config.before_request
549
575
  stats.request_count += 1
@@ -594,7 +620,7 @@ EOS
594
620
  }
595
621
  ]
596
622
  }
597
- message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
623
+ message = Struct::StubMessage.new(1, 1, JSON.generate(body))
598
624
  @sqs_poller.get_messages(anything, anything) do |config, stats|
599
625
  config.before_request.call(stats) if config.before_request
600
626
  stats.request_count += 1
@@ -614,6 +640,91 @@ EOS
614
640
  assert_equal(expected_records, events.map {|_tag, _time, record| record })
615
641
  end
616
642
 
643
+ data(
644
+ "limit_gzip" => { type: "gzip", input: "StringIO", limit: 10, expected_error: true },
645
+ "limit_text" => { type: "text", input: "StringIO", limit: 10, expected_error: true },
646
+ "limit_gzip_command1" => { type: "gzip_command", input: "StringIO", limit: 10, expected_error: true },
647
+ "limit_gzip_command2" => { type: "gzip_command", input: "Tempfile", limit: 10, expected_error: true },
648
+ "normal_gzip_command" => { type: "gzip_command", input: "Tempfile", limit: 100, expected_error: false },
649
+ )
650
+ def test_decompression_size_limit(data)
651
+ store_type = data[:type]
652
+ input_type = data[:input]
653
+ limit = data[:limit]
654
+ setup_mocks
655
+
656
+ config = <<~CONF
657
+ #{CONFIG}
658
+ check_apikey_on_start false
659
+ store_as #{store_type}
660
+ format none
661
+ decompression_size_limit #{limit}
662
+ CONF
663
+
664
+ d = create_driver(config)
665
+
666
+ s3_object = stub(Object.new)
667
+ s3_response = stub(Object.new)
668
+ s3_response.body {
669
+ content = "#{'a'*10}\n#{'b'*10}\n"
670
+
671
+ # Switching between Tempfile and StringIO to cover both branches of the
672
+ # `io.respond_to?(:path)` condition in `extract_with_command`.
673
+ # This ensures that:
674
+ # 1. The StringIO route correctly uses TextExtractor to create a protected temporary file.
675
+ # 2. The Tempfile route correctly limits the output size during Open3.popen3 execution.
676
+ io = (input_type == "Tempfile") ? Tempfile.new : StringIO.new
677
+
678
+ case store_type
679
+ when "gzip", "gzip_command"
680
+ io.binmode
681
+ Zlib::GzipWriter.wrap(io) { |gz|
682
+ gz.write content
683
+ gz.finish
684
+ }
685
+ when "text"
686
+ io.write content
687
+ end
688
+
689
+ io.rewind
690
+ io
691
+ }
692
+ s3_object.get { s3_response }
693
+ @s3_bucket.object(anything).at_least(1) { s3_object }
694
+
695
+ body = {
696
+ "Records" => [
697
+ {
698
+ "s3" => {
699
+ "object" => {
700
+ "key" => "test_key"
701
+ }
702
+ }
703
+ }
704
+ ]
705
+ }
706
+ message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
707
+ @sqs_poller.get_messages(anything, anything) do |config, stats|
708
+ config.before_request.call(stats) if config.before_request
709
+ stats.request_count += 1
710
+ if stats.request_count >= 1
711
+ d.instance.instance_variable_set(:@running, false)
712
+ end
713
+ [message]
714
+ end
715
+ d.run
716
+
717
+ if data[:expected_error]
718
+ # Verify the protection mechanism: ensure SizeLimitError is logged.
719
+ assert_true d.logs.any? { |l| l.include?("Extracted data exceeds limit of #{limit} bytes") }
720
+ else
721
+ # Verify the normal execution path: ensure data is correctly extracted via Open3.popen3.
722
+ expected_records = [{ "message" => "#{'a'*10}\n" }, { "message" => "#{'b'*10}\n" }]
723
+ assert_equal(expected_records, d.events.map {|_tag, _time, record| record })
724
+ assert_false d.logs.any? { |l| l.include?("error_class") }
725
+ end
726
+ end
727
+
617
728
  def test_regexp_matching
618
729
  setup_mocks
619
730
  d = create_driver(CONFIG + "\ncheck_apikey_on_start false\nstore_as text\nformat none\nmatch_regexp .*_key?")
@@ -635,7 +746,7 @@ EOS
635
746
  }
636
747
  ]
637
748
  }
638
- message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
749
+ message = Struct::StubMessage.new(1, 1, JSON.generate(body))
639
750
  @sqs_poller.get_messages(anything, anything) do |config, stats|
640
751
  config.before_request.call(stats) if config.before_request
641
752
  stats.request_count += 1
@@ -664,7 +775,7 @@ EOS
664
775
  }
665
776
  ]
666
777
  }
667
- message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
778
+ message = Struct::StubMessage.new(1, 1, JSON.generate(body))
668
779
  @sqs_poller.get_messages(anything, anything) do |config, stats|
669
780
  config.before_request.call(stats) if config.before_request
670
781
  stats.request_count += 1
@@ -708,8 +819,8 @@ EOS
708
819
  }
709
820
  }
710
821
  }
711
-
712
- message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
822
+
823
+ message = Struct::StubMessage.new(1, 1, JSON.generate(body))
713
824
  @sqs_poller.get_messages(anything, anything) do |config, stats|
714
825
  config.before_request.call(stats) if config.before_request
715
826
  stats.request_count += 1
data/test/test_out_s3.rb CHANGED
@@ -2,6 +2,7 @@ require 'fluent/test'
2
2
  require 'fluent/test/helpers'
3
3
  require 'fluent/test/log'
4
4
  require 'fluent/test/driver/output'
5
+ require 'fluent/version'
5
6
  require 'aws-sdk-s3'
6
7
  require 'fluent/plugin/out_s3'
7
8
 
@@ -68,6 +69,30 @@ class S3OutputTest < Test::Unit::TestCase
68
69
  assert_equal true, d.instance.check_object
69
70
  end
70
71
 
72
+ def test_aws_profile
73
+ d = create_driver(%[
74
+ aws_profile test_profile
75
+ s3_bucket test_bucket
76
+ path log
77
+ utc
78
+ buffer_type memory
79
+ time_slice_format %Y%m%d-%H
80
+ ])
81
+ assert_equal 'test_profile', d.instance.aws_profile
82
+ end
83
+
84
+ def test_aws_credential_process
85
+ d = create_driver(%[
86
+ aws_credential_process "/path/to/aws_signing_helper credential-process"
87
+ s3_bucket test_bucket
88
+ path log
89
+ utc
90
+ buffer_type memory
91
+ time_slice_format %Y%m%d-%H
92
+ ])
93
+ assert_equal '/path/to/aws_signing_helper credential-process', d.instance.aws_credential_process
94
+ end
95
+
71
96
  def test_s3_endpoint_with_valid_endpoint
72
97
  d = create_driver(CONFIG + 's3_endpoint riak-cs.example.com')
73
98
  assert_equal 'riak-cs.example.com', d.instance.s3_endpoint
@@ -495,6 +520,125 @@ EOC
495
520
  FileUtils.rm_f(s3_local_file_path)
496
521
  end
497
522
 
523
+ def test_no_compress_by_gzip_with_compressed_buffer
524
+ setup_mocks(true)
525
+ s3_local_file_path = "/tmp/s3-test.gz"
526
+ setup_s3_object_mocks(s3_local_file_path: s3_local_file_path)
527
+
528
+ d = create_time_sliced_driver(CONFIG_TIME_SLICE + <<~EOF)
529
+ <buffer tag,time>
530
+ @type memory
531
+ compress gzip
532
+ timekey 3600
533
+ timekey_use_utc true
534
+ </buffer>
535
+ EOF
536
+
537
+ # GzipCompressor should not use Zlib::GzipWriter
538
+ dont_allow(Zlib::GzipWriter).new
539
+
540
+ time = event_time("2011-01-02 13:14:15 UTC")
541
+ d.run(default_tag: "test") do
542
+ d.feed(time, {"a"=>1})
543
+ d.feed(time, {"a"=>2})
544
+ end
545
+
546
+ data = ""
547
+ File.open(s3_local_file_path, "rb") do |f|
548
+ until f.eof?
549
+ gz = Zlib::GzipReader.new(f)
550
+ data << gz.read
551
+
552
+ unused = gz.unused
553
+ gz.finish
554
+ f.pos -= unused.length if unused
555
+ end
556
+ end
557
+ assert_equal %[2011-01-02T13:14:15Z\ttest\t{"a":1}\n] +
558
+ %[2011-01-02T13:14:15Z\ttest\t{"a":2}\n],
559
+ data
560
+ FileUtils.rm_f(s3_local_file_path)
561
+ end
562
+
563
+ def test_no_compress_by_gzip_command_with_compressed_buffer
564
+ setup_mocks(true)
565
+ s3_local_file_path = "/tmp/s3-test.gz"
566
+ setup_s3_object_mocks(s3_local_file_path: s3_local_file_path)
567
+
568
+ d = create_time_sliced_driver(CONFIG_TIME_SLICE + <<~EOF)
569
+ store_as gzip_command
570
+ <buffer tag,time>
571
+ @type memory
572
+ compress gzip
573
+ timekey 3600
574
+ timekey_use_utc true
575
+ </buffer>
576
+ EOF
577
+
578
+ # GzipCommandCompressor should not use Kernel.system and Zlib::GzipWriter
579
+ dont_allow(Kernel).system
580
+ dont_allow(Zlib::GzipWriter).new
581
+
582
+ time = event_time("2011-01-02 13:14:15 UTC")
583
+ d.run(default_tag: "test") do
584
+ d.feed(time, {"a"=>1})
585
+ d.feed(time, {"a"=>2})
586
+ end
587
+
588
+ data = ""
589
+ File.open(s3_local_file_path, "rb") do |f|
590
+ until f.eof?
591
+ gz = Zlib::GzipReader.new(f)
592
+ data << gz.read
593
+
594
+ unused = gz.unused
595
+ gz.finish
596
+ f.pos -= unused.length if unused
597
+ end
598
+ end
599
+ assert_equal %[2011-01-02T13:14:15Z\ttest\t{"a":1}\n] +
600
+ %[2011-01-02T13:14:15Z\ttest\t{"a":2}\n],
601
+ data
602
+ FileUtils.rm_f(s3_local_file_path)
603
+ end
604
+
605
+ def test_no_compress_by_zstd_with_compressed_buffer
606
+ pend "This test require Fluentd 1.19.0 or later which supports zstd buffer compressed." if Gem::Version.new(Fluent::VERSION) < Gem::Version.new('1.19.0')
607
+
608
+ setup_mocks(true)
609
+ s3_local_file_path = "/tmp/s3-test.zst"
610
+ expected_s3path = "log/events/ts=20110102-13/events_0-#{Socket.gethostname}.zst"
611
+ setup_s3_object_mocks(s3_local_file_path: s3_local_file_path, s3path: expected_s3path)
612
+
613
+ d = create_time_sliced_driver(CONFIG_TIME_SLICE + <<~EOF)
614
+ store_as zstd
615
+ <buffer tag,time>
616
+ @type memory
617
+ compress zstd
618
+ timekey 3600
619
+ timekey_use_utc true
620
+ </buffer>
621
+ EOF
622
+
623
+ # ZstdCompressor should not use Zstd.compress
624
+ dont_allow(Zstd).compress
625
+
626
+ time = event_time("2011-01-02 13:14:15 UTC")
627
+ d.run(default_tag: "test") do
628
+ d.feed(time, {"a"=>1})
629
+ d.feed(time, {"a"=>2})
630
+ end
631
+
632
+ File.open(s3_local_file_path, 'rb') do |file|
633
+ compressed_data = file.read
634
+ uncompressed_data = Zstd.decompress(compressed_data)
635
+ expected_data = %[2011-01-02T13:14:15Z\ttest\t{"a":1}\n] +
636
+ %[2011-01-02T13:14:15Z\ttest\t{"a":2}\n]
637
+ assert_equal expected_data, uncompressed_data
638
+ end
639
+ FileUtils.rm_f(s3_local_file_path)
640
+ end
641
+
498
642
  class MockResponse
499
643
  attr_reader :data
500
644
 
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-s3
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.3
4
+ version: 1.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sadayuki Furuhashi
8
8
  - Masahiro Nakagawa
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2025-02-18 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: fluentd
@@ -116,7 +115,7 @@ dependencies:
116
115
  - !ruby/object:Gem::Version
117
116
  version: '0'
118
117
  - !ruby/object:Gem::Dependency
119
- name: rexml
118
+ name: ostruct
120
119
  requirement: !ruby/object:Gem::Requirement
121
120
  requirements:
122
121
  - - ">="
@@ -130,7 +129,7 @@ dependencies:
130
129
  - !ruby/object:Gem::Version
131
130
  version: '0'
132
131
  - !ruby/object:Gem::Dependency
133
- name: zstd-ruby
132
+ name: rexml
134
133
  requirement: !ruby/object:Gem::Requirement
135
134
  requirements:
136
135
  - - ">="
@@ -143,6 +142,20 @@ dependencies:
143
142
  - - ">="
144
143
  - !ruby/object:Gem::Version
145
144
  version: '0'
145
+ - !ruby/object:Gem::Dependency
146
+ name: zstd-ruby
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '1.5'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '1.5'
146
159
  description: Amazon S3 output plugin for Fluentd event collector
147
160
  email: frsyuki@gmail.com
148
161
  executables: []
@@ -187,7 +200,6 @@ homepage: https://github.com/fluent/fluent-plugin-s3
187
200
  licenses:
188
201
  - Apache-2.0
189
202
  metadata: {}
190
- post_install_message:
191
203
  rdoc_options: []
192
204
  require_paths:
193
205
  - lib
@@ -202,8 +214,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
202
214
  - !ruby/object:Gem::Version
203
215
  version: '0'
204
216
  requirements: []
205
- rubygems_version: 3.5.22
206
- signing_key:
217
+ rubygems_version: 4.0.10
207
218
  specification_version: 4
208
219
  summary: Amazon S3 output plugin for Fluentd event collector
209
- test_files: []
220
+ test_files:
221
+ - test/test_in_s3.rb
222
+ - test/test_out_s3.rb