fluent-plugin-opensearch 1.1.7 → 1.1.8
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/.github/workflows/coverage.yaml +1 -1
- data/.github/workflows/linux.yml +1 -1
- data/.github/workflows/macos.yml +1 -1
- data/.github/workflows/windows.yml +1 -1
- data/History.md +5 -0
- data/README.md +6 -0
- data/fluent-plugin-opensearch.gemspec +1 -1
- data/lib/fluent/plugin/in_opensearch.rb +3 -1
- data/lib/fluent/plugin/opensearch_index_template.rb +17 -1
- data/lib/fluent/plugin/out_opensearch.rb +130 -16
- data/lib/fluent/plugin/out_opensearch_data_stream.rb +1 -5
- data/test/plugin/test_in_opensearch.rb +15 -0
- data/test/plugin/test_out_opensearch.rb +327 -0
- data/test/plugin/test_settings_template.json +4 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '04843ec3381adb39f0e7fbc2a6cc76c25fa8d1d85d5733e500e58d7814beb57a'
|
|
4
|
+
data.tar.gz: b5e60e70521053e723b9c4df9953b03774a1207f7cf3c19b9c3aeb385e03f15f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 80cb6808fef85cc16fb835324f2ff3fbe3d42978d3c3f72dccf35251d39b3fe29f74d1271c30b2913959fd7cdc1f4e277ed91608663756e24d3d2e9b438e2943
|
|
7
|
+
data.tar.gz: 6d936e5608366a827cdd03f8bf51615b34780f9fdb7354a764cb2f39e4c570ad5f768ccc73cf286c9f22a3bafc529868b7e4434c8ea6b001af3328c541a7cb3e
|
|
@@ -8,7 +8,7 @@ jobs:
|
|
|
8
8
|
steps:
|
|
9
9
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
10
10
|
- name: Set up Ruby 3.0
|
|
11
|
-
uses: ruby/setup-ruby@
|
|
11
|
+
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
|
12
12
|
with:
|
|
13
13
|
ruby-version: '3.1'
|
|
14
14
|
- name: Build and test with Rake
|
data/.github/workflows/linux.yml
CHANGED
|
@@ -26,7 +26,7 @@ jobs:
|
|
|
26
26
|
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
|
|
27
27
|
steps:
|
|
28
28
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
29
|
-
- uses: ruby/setup-ruby@
|
|
29
|
+
- uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
|
30
30
|
with:
|
|
31
31
|
ruby-version: ${{ matrix.ruby }}
|
|
32
32
|
- name: unit testing
|
data/.github/workflows/macos.yml
CHANGED
|
@@ -26,7 +26,7 @@ jobs:
|
|
|
26
26
|
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
|
|
27
27
|
steps:
|
|
28
28
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
29
|
-
- uses: ruby/setup-ruby@
|
|
29
|
+
- uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
|
30
30
|
with:
|
|
31
31
|
ruby-version: ${{ matrix.ruby }}
|
|
32
32
|
- name: unit testing
|
|
@@ -26,7 +26,7 @@ jobs:
|
|
|
26
26
|
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
|
|
27
27
|
steps:
|
|
28
28
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
29
|
-
- uses: ruby/setup-ruby@
|
|
29
|
+
- uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
|
30
30
|
with:
|
|
31
31
|
ruby-version: ${{ matrix.ruby }}
|
|
32
32
|
- name: unit testing
|
data/History.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
### [Unreleased]
|
|
4
4
|
|
|
5
|
+
### 1.1.8
|
|
6
|
+
- `out_opensearch`: limit what a host placeholder can expand to (#192)
|
|
7
|
+
- `out_opensearch`: escape a `customize_template` value inside a string (#194)
|
|
8
|
+
- `out_opensearch`: lock the client cache against flush threads (#195)
|
|
9
|
+
|
|
5
10
|
### 1.1.7
|
|
6
11
|
- test: remove duplicate keys from bulk response fixtures (#190)
|
|
7
12
|
- out\_opensearch: keep AWS credential providers refreshable to fix expired SigV4 tokens with assume\_role\_arn (#188)
|
data/README.md
CHANGED
|
@@ -1241,6 +1241,12 @@ records = {key1: "value1", key2: "value2"}
|
|
|
1241
1241
|
</match>
|
|
1242
1242
|
```
|
|
1243
1243
|
|
|
1244
|
+
#### host and hosts
|
|
1245
|
+
|
|
1246
|
+
`host` and `hosts` take placeholders too, for example `hosts https://logs-${tag}.example.com:9201/os`. A value may only fill in the part of a host name that the placeholder stands for, and how much it may fill in depends on the names that you write after the placeholder. `logs-${tag}.example.com` lets a value add a sub domain of `example.com`, such as `nginx.access`. `logs-${tag}.com` lets a value fill in one name only, because a single name fixes the top level domain and nothing else: the name that decides who owns the domain would still come from the value. A value that does not fit raises `UnrecoverableRequestFailure`.
|
|
1247
|
+
|
|
1248
|
+
**Note**: the configured `user`, `password` and `custom_headers` are sent to whichever host the value picks. So write a domain that you control, where nobody else can create a sub domain. A shared domain such as `amazonaws.com` is not enough. Without such a domain, let only senders that you trust set the placeholder value.
|
|
1249
|
+
|
|
1244
1250
|
## Multi workers
|
|
1245
1251
|
|
|
1246
1252
|
Since Fluentd v0.14, multi workers feature had been implemented to increase throughput with multiple processes. This feature allows Fluentd processes to use one or more CPUs. This feature will be enabled by the following system configuration:
|
|
@@ -3,7 +3,7 @@ $:.push File.expand_path('../lib', __FILE__)
|
|
|
3
3
|
|
|
4
4
|
Gem::Specification.new do |s|
|
|
5
5
|
s.name = 'fluent-plugin-opensearch'
|
|
6
|
-
s.version = '1.1.
|
|
6
|
+
s.version = '1.1.8'
|
|
7
7
|
s.authors = ['Hiroshi Hatake']
|
|
8
8
|
s.email = ['cosmo0920.wp@gmail.com']
|
|
9
9
|
s.description = %q{Opensearch output plugin for Fluent event collector}
|
|
@@ -51,7 +51,9 @@ module Fluent::Plugin
|
|
|
51
51
|
config_param :password, :string, :default => nil, :secret => true
|
|
52
52
|
config_param :path, :string, :default => nil
|
|
53
53
|
config_param :scheme, :enum, :list => [:https, :http], :default => :http
|
|
54
|
-
|
|
54
|
+
# `hosts` accepts "https://user:password@host", so it is masked in the
|
|
55
|
+
# configuration dump like `password` is.
|
|
56
|
+
config_param :hosts, :string, :default => nil, :secret => true
|
|
55
57
|
config_param :index_name, :string, :default => "fluentd"
|
|
56
58
|
config_param :parse_timestamp, :bool, :default => false
|
|
57
59
|
config_param :timestamp_key_format, :string, :default => nil
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
# specific language governing permissions and limitations
|
|
25
25
|
# under the License.
|
|
26
26
|
|
|
27
|
+
require 'json'
|
|
28
|
+
|
|
27
29
|
require 'fluent/error'
|
|
28
30
|
require_relative './opensearch_error'
|
|
29
31
|
|
|
@@ -42,11 +44,25 @@ module Fluent::OpenSearchIndexTemplate
|
|
|
42
44
|
end
|
|
43
45
|
file_contents = IO.read(template_file).gsub(/\n/,'')
|
|
44
46
|
customize_template.each do |key, value|
|
|
45
|
-
file_contents = file_contents
|
|
47
|
+
file_contents = substitute_template_value(file_contents, key, value.downcase)
|
|
46
48
|
end
|
|
47
49
|
JSON.parse(file_contents, allow_duplicate_key: true)
|
|
48
50
|
end
|
|
49
51
|
|
|
52
|
+
# A value can come from a tag or a record field. If its key is inside a JSON
|
|
53
|
+
# string ($1), a `"` in the value would end that string, and the rest would
|
|
54
|
+
# be read as extra template keys, so the value is escaped there. If the key
|
|
55
|
+
# is anywhere else, the value is a piece of JSON, like a whole settings
|
|
56
|
+
# object, so it goes in as it is. The block form stops gsub from reading
|
|
57
|
+
# backslash sequences in the value.
|
|
58
|
+
def substitute_template_value(file_contents, key, value)
|
|
59
|
+
json_string = /"(?:\\.|[^"\\])*"/
|
|
60
|
+
file_contents.gsub(/(#{json_string})|#{Regexp.escape(key)}/) do
|
|
61
|
+
# to_json puts the value in quotes, so drop them: a"b => a\"b
|
|
62
|
+
$1 ? $1.gsub(key) { value.to_json[1..-2] } : value
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
50
66
|
def template_exists?(name, host = nil)
|
|
51
67
|
if @use_legacy_template
|
|
52
68
|
client(host).indices.get_template(:name => name)
|
|
@@ -95,13 +95,26 @@ module Fluent::Plugin
|
|
|
95
95
|
DEFAULT_TARGET_BULK_BYTES = -1
|
|
96
96
|
DEFAULT_POLICY_ID = "logstash-policy"
|
|
97
97
|
|
|
98
|
+
# A placeholder in `host`/`hosts` is expanded from the tag or a record
|
|
99
|
+
# field, so a log sender chooses its value. Without a check the value
|
|
100
|
+
# could add a host with ",", userinfo with "@" or a path with "/", and
|
|
101
|
+
# the configured user/password and custom_headers would go to that host.
|
|
102
|
+
HOST_PLACEHOLDER_PATTERN = /\$\{[^}]*\}/
|
|
103
|
+
HOST_LABEL_PATTERN = /\A[0-9A-Za-z_\-]+\z/
|
|
104
|
+
HOST_NAME_PATTERN = /\A[0-9A-Za-z_\-]+(?:\.[0-9A-Za-z_\-]+)*\z/
|
|
105
|
+
FIXED_SUFFIX_PATTERN = /\A[0-9A-Za-z_\-]*(?:\.[0-9A-Za-z_\-]+){2,}\.?\z/
|
|
106
|
+
# A host name or an IPv6 address in "[]", and the port that may follow it.
|
|
107
|
+
HOST_AND_PORT_PATTERN = /\A(\[[^\]]+\]|[^:]*)(?::([0-9]+))?\z/
|
|
108
|
+
|
|
98
109
|
config_param :host, :string, :default => 'localhost'
|
|
99
110
|
config_param :port, :integer, :default => 9200
|
|
100
111
|
config_param :user, :string, :default => nil
|
|
101
112
|
config_param :password, :string, :default => nil, :secret => true
|
|
102
113
|
config_param :path, :string, :default => nil
|
|
103
114
|
config_param :scheme, :enum, :list => [:https, :http], :default => :http
|
|
104
|
-
|
|
115
|
+
# `hosts` accepts "https://user:password@host" and "%{user}:%{password}@",
|
|
116
|
+
# so it is masked in the configuration dump like `password` is.
|
|
117
|
+
config_param :hosts, :string, :default => nil, :secret => true
|
|
105
118
|
config_param :target_index_key, :string, :default => nil
|
|
106
119
|
config_param :time_key_format, :string, :default => nil
|
|
107
120
|
config_param :time_precision, :integer, :default => 9
|
|
@@ -185,7 +198,8 @@ module Fluent::Plugin
|
|
|
185
198
|
|
|
186
199
|
config_section :endpoint, multi: false do
|
|
187
200
|
config_param :region, :string
|
|
188
|
-
|
|
201
|
+
# The URL can carry a user and a password, so mask it in the dump.
|
|
202
|
+
config_param :url, secret: true do |c|
|
|
189
203
|
c.chomp("/")
|
|
190
204
|
end
|
|
191
205
|
config_param :access_key_id, :string, :default => ""
|
|
@@ -211,6 +225,8 @@ module Fluent::Plugin
|
|
|
211
225
|
|
|
212
226
|
def initialize
|
|
213
227
|
super
|
|
228
|
+
# `configure` already builds clients, so this cannot wait until then.
|
|
229
|
+
@client_mutex = Mutex.new
|
|
214
230
|
end
|
|
215
231
|
|
|
216
232
|
######################################################################################################
|
|
@@ -316,6 +332,8 @@ module Fluent::Plugin
|
|
|
316
332
|
log.info "host placeholder and template installation makes your OpenSearch cluster a bit slow down(beta)."
|
|
317
333
|
end
|
|
318
334
|
|
|
335
|
+
warn_host_placeholder_with_credentials
|
|
336
|
+
|
|
319
337
|
@template_names = []
|
|
320
338
|
if !dry_run?
|
|
321
339
|
if @template_name && @template_file
|
|
@@ -347,7 +365,6 @@ module Fluent::Plugin
|
|
|
347
365
|
end
|
|
348
366
|
end
|
|
349
367
|
# Credential providers refresh themselves near expiration. This timer recreates them forcibly.
|
|
350
|
-
@credential_mutex = Mutex.new
|
|
351
368
|
if @endpoint
|
|
352
369
|
@_aws_credentials = aws_credentials(@endpoint)
|
|
353
370
|
|
|
@@ -355,12 +372,14 @@ module Fluent::Plugin
|
|
|
355
372
|
timer_execute(:out_opensearch_expire_credentials, @endpoint.refresh_credentials_interval) do
|
|
356
373
|
log.debug('Recreate the AWS credentials')
|
|
357
374
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
375
|
+
begin
|
|
376
|
+
credentials = aws_credentials(@endpoint)
|
|
377
|
+
rescue => e
|
|
378
|
+
log.error("Failed to get new AWS credentials: #{e}")
|
|
379
|
+
else
|
|
380
|
+
@client_mutex.synchronize do
|
|
381
|
+
@_os = nil
|
|
382
|
+
@_aws_credentials = credentials
|
|
364
383
|
end
|
|
365
384
|
end
|
|
366
385
|
end
|
|
@@ -481,6 +500,34 @@ module Fluent::Plugin
|
|
|
481
500
|
end
|
|
482
501
|
end
|
|
483
502
|
|
|
503
|
+
# A placeholder decides the host, so every credential the configuration
|
|
504
|
+
# holds follows it. `endpoint` is left alone because
|
|
505
|
+
# `get_connection_options` ignores the expanded host there.
|
|
506
|
+
def warn_host_placeholder_with_credentials
|
|
507
|
+
return if @endpoint
|
|
508
|
+
|
|
509
|
+
elements = (@hosts || @host).split(',')
|
|
510
|
+
return unless elements.any? { |element| element.match?(HOST_PLACEHOLDER_PATTERN) }
|
|
511
|
+
|
|
512
|
+
param_name = @hosts ? 'hosts' : 'host'
|
|
513
|
+
if @user || @password || !@custom_headers.empty?
|
|
514
|
+
log.warn "'#{param_name}' uses a placeholder, so 'user', 'password' and 'custom_headers' are sent to whichever host a tag or a record field expands to. Make sure that only trusted senders can set them."
|
|
515
|
+
end
|
|
516
|
+
if elements.any? { |element| credentials_before_placeholder?(element) }
|
|
517
|
+
# Do not put the element in the log: it might hold the password.
|
|
518
|
+
log.warn "'#{param_name}' has a user and password in front of a host that a placeholder decides, so they are sent to whichever host a tag or a record field expands to. Make sure that only trusted senders can set it."
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# A user and password written in a URL sit in front of the host, so a
|
|
523
|
+
# placeholder that comes after them picks where they are sent.
|
|
524
|
+
def credentials_before_placeholder?(element)
|
|
525
|
+
userinfo = element.index('@')
|
|
526
|
+
placeholder = element.index('${')
|
|
527
|
+
|
|
528
|
+
!userinfo.nil? && !placeholder.nil? && userinfo < placeholder
|
|
529
|
+
end
|
|
530
|
+
|
|
484
531
|
def placeholder?(name, param)
|
|
485
532
|
placeholder_validities = []
|
|
486
533
|
placeholder_validators(name, param).each do |v|
|
|
@@ -622,6 +669,14 @@ module Fluent::Plugin
|
|
|
622
669
|
end
|
|
623
670
|
|
|
624
671
|
def client(host = nil, compress_connection = false)
|
|
672
|
+
# Flush threads share @_os, and each of them may ask for a different
|
|
673
|
+
# host. Without this lock a thread can pass the check below and then
|
|
674
|
+
# pick up the client another thread just built for its own host, which
|
|
675
|
+
# sends the chunk to the wrong cluster.
|
|
676
|
+
@client_mutex.synchronize { build_or_reuse_client(host, compress_connection) }
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def build_or_reuse_client(host, compress_connection)
|
|
625
680
|
# check here to see if we already have a client connection for the given host
|
|
626
681
|
connection_options = get_connection_options(host)
|
|
627
682
|
|
|
@@ -847,6 +902,63 @@ module Fluent::Plugin
|
|
|
847
902
|
return logstash_prefix, logstash_dateformat, index_name, template_name, customize_template, application_name, pipeline
|
|
848
903
|
end
|
|
849
904
|
|
|
905
|
+
def expand_host_placeholders(chunk)
|
|
906
|
+
template = @hosts || @host
|
|
907
|
+
template.split(',').each do |element|
|
|
908
|
+
offset = 0
|
|
909
|
+
while (match = HOST_PLACEHOLDER_PATTERN.match(element, offset))
|
|
910
|
+
offset = match.end(0)
|
|
911
|
+
value = extract_placeholders(match[0], chunk)
|
|
912
|
+
next if valid_host_placeholder_value?(value, match.pre_match, match.post_match)
|
|
913
|
+
|
|
914
|
+
raise UnrecoverableRequestFailure, "Rejected #{value.dump} for #{match[0]} in 'host'/'hosts': a placeholder may only fill in the part of a host name that it stands for."
|
|
915
|
+
end
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
extract_placeholders(template, chunk)
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
# What a value may hold depends on where the operator put the placeholder.
|
|
922
|
+
# `before` and `after` are the parts of the element around it.
|
|
923
|
+
def valid_host_placeholder_value?(value, before, after)
|
|
924
|
+
# The operator wrote the "[]" that an IPv6 address needs.
|
|
925
|
+
return Resolv::IPv6::Regex.match?(value) if before.end_with?('[') && after.start_with?(']')
|
|
926
|
+
|
|
927
|
+
unless whole_host_placeholder?(before, after)
|
|
928
|
+
# The value continues a name the operator wrote, so a port cannot
|
|
929
|
+
# follow it and a "." may only add a sub domain to a fixed domain.
|
|
930
|
+
pattern = fixed_domain_suffix?(after) ? HOST_NAME_PATTERN : HOST_LABEL_PATTERN
|
|
931
|
+
return pattern.match?(value)
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
host, port = value.match(HOST_AND_PORT_PATTERN)&.captures
|
|
935
|
+
# The value may end with a port, unless the operator wrote one already.
|
|
936
|
+
return false if host.nil? || (port && after.start_with?(':'))
|
|
937
|
+
|
|
938
|
+
HOST_NAME_PATTERN.match?(host) || bracketed_ipv6?(host, before)
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
# The value fills in the whole host name when only a scheme or a userinfo
|
|
942
|
+
# comes before it and the host name ends where the value ends.
|
|
943
|
+
def whole_host_placeholder?(before, after)
|
|
944
|
+
(before.empty? || before.end_with?('//', '@')) &&
|
|
945
|
+
(after.empty? || after.start_with?(':', '/', '?', '#'))
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
# The domain is fixed when two or more names follow the last placeholder,
|
|
949
|
+
# because the last labels of the host name then come from the operator.
|
|
950
|
+
# One name, such as ".com", lets a value with a "." pick any name under it.
|
|
951
|
+
def fixed_domain_suffix?(after)
|
|
952
|
+
host_part = after[/\A[^:\/?#]*/]
|
|
953
|
+
FIXED_SUFFIX_PATTERN.match?(host_part.split(HOST_PLACEHOLDER_PATTERN, -1).last.to_s)
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
# `URI()` in `get_connection_options` needs a scheme to read an IPv6
|
|
957
|
+
# address, and the operator writes the scheme, not the value.
|
|
958
|
+
def bracketed_ipv6?(host, before)
|
|
959
|
+
!before.empty? && host.start_with?('[') && Resolv::IPv6::Regex.match?(host[1..-2])
|
|
960
|
+
end
|
|
961
|
+
|
|
850
962
|
def multi_workers_ready?
|
|
851
963
|
true
|
|
852
964
|
end
|
|
@@ -869,11 +981,7 @@ module Fluent::Plugin
|
|
|
869
981
|
tag = chunk.metadata.tag
|
|
870
982
|
chunk_id = dump_unique_id_hex(chunk.unique_id)
|
|
871
983
|
extracted_values = expand_placeholders(chunk)
|
|
872
|
-
host =
|
|
873
|
-
extract_placeholders(@hosts, chunk)
|
|
874
|
-
else
|
|
875
|
-
extract_placeholders(@host, chunk)
|
|
876
|
-
end
|
|
984
|
+
host = expand_host_placeholders(chunk)
|
|
877
985
|
|
|
878
986
|
affinity_target_indices = get_affinity_target_indices(chunk)
|
|
879
987
|
chunk.msgpack_each do |time, record|
|
|
@@ -1137,8 +1245,14 @@ module Fluent::Plugin
|
|
|
1137
1245
|
|
|
1138
1246
|
log.warn "Exception ignored in tag #{tag}: #{e.class.name} #{e.message}" if ignore
|
|
1139
1247
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1248
|
+
if @reconnect_on_error
|
|
1249
|
+
# Another flush thread may be building a client right now, so drop the
|
|
1250
|
+
# cached one under the lock that guards it.
|
|
1251
|
+
@client_mutex.synchronize do
|
|
1252
|
+
@_os = nil
|
|
1253
|
+
@_os_info = nil
|
|
1254
|
+
end
|
|
1255
|
+
end
|
|
1142
1256
|
|
|
1143
1257
|
raise UnrecoverableRequestFailure if ignore && @exception_backup
|
|
1144
1258
|
|
|
@@ -155,11 +155,7 @@ module Fluent::Plugin
|
|
|
155
155
|
data_stream_template_name = @data_stream_template_name
|
|
156
156
|
host = nil
|
|
157
157
|
if @use_placeholder
|
|
158
|
-
host =
|
|
159
|
-
extract_placeholders(@hosts, chunk)
|
|
160
|
-
else
|
|
161
|
-
extract_placeholders(@host, chunk)
|
|
162
|
-
end
|
|
158
|
+
host = expand_host_placeholders(chunk)
|
|
163
159
|
data_stream_name = extract_placeholders(@data_stream_name, chunk).downcase
|
|
164
160
|
data_stream_template_name = extract_placeholders(@data_stream_template_name, chunk).downcase
|
|
165
161
|
begin
|
|
@@ -322,6 +322,21 @@ class OpenSearchInputTest < Test::Unit::TestCase
|
|
|
322
322
|
assert_equal 'raw.opensearch', instance.tag
|
|
323
323
|
end
|
|
324
324
|
|
|
325
|
+
def test_hosts_is_masked_in_the_configuration_dump
|
|
326
|
+
config = Fluent::Config::Element.new(
|
|
327
|
+
'ROOT', '', {
|
|
328
|
+
'@type' => 'opensearch',
|
|
329
|
+
'hosts' => 'https://john:passw0rd@host1:443/elastic/',
|
|
330
|
+
'tag' => 'raw.opensearch',
|
|
331
|
+
'check_connection' => 'false',
|
|
332
|
+
}, [])
|
|
333
|
+
driver(config)
|
|
334
|
+
|
|
335
|
+
dump = config.to_masked_element.to_s
|
|
336
|
+
assert_false dump.include?('passw0rd')
|
|
337
|
+
assert_true dump.include?('hosts xxxxxx')
|
|
338
|
+
end
|
|
339
|
+
|
|
325
340
|
def test_hosts_list_with_escape_placeholders
|
|
326
341
|
config = %{
|
|
327
342
|
hosts https://%{j+hn}:%{passw@rd}@host1:443/elastic/,http://host2
|
|
@@ -106,6 +106,32 @@ class OpenSearchOutputTest < Test::Unit::TestCase
|
|
|
106
106
|
end
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
def driver_with_captured_credential_refresh
|
|
110
|
+
config = Fluent::Config::Element.new(
|
|
111
|
+
'ROOT', '', {
|
|
112
|
+
'@type' => 'opensearch',
|
|
113
|
+
}, [
|
|
114
|
+
Fluent::Config::Element.new('endpoint', '', {
|
|
115
|
+
'url' => "https://search-opensearch.aws.example.com/",
|
|
116
|
+
'region' => "local",
|
|
117
|
+
'access_key_id' => 'YOUR_AWESOME_KEY',
|
|
118
|
+
'secret_access_key' => 'YOUR_AWESOME_SECRET',
|
|
119
|
+
'refresh_credentials_interval' => '18000',
|
|
120
|
+
}, []),
|
|
121
|
+
Fluent::Config::Element.new('buffer', 'tag', {}, [])
|
|
122
|
+
])
|
|
123
|
+
d = Fluent::Test::Driver::Output.new(Fluent::Plugin::OpenSearchOutput)
|
|
124
|
+
timers = {}
|
|
125
|
+
d.instance.define_singleton_method(:timer_execute) do |title, interval, &block|
|
|
126
|
+
timers[title] = block
|
|
127
|
+
end
|
|
128
|
+
# Keep `configure` off the network.
|
|
129
|
+
d.instance.define_singleton_method(:detect_os_major_version) { 1 }
|
|
130
|
+
d.configure(config)
|
|
131
|
+
|
|
132
|
+
[d, timers[:out_opensearch_expire_credentials]]
|
|
133
|
+
end
|
|
134
|
+
|
|
109
135
|
def stub_opensearch_info(url="http://localhost:9200/", version="1.2.2")
|
|
110
136
|
body ="{\"version\":{\"number\":\"#{version}\", \"distribution\":\"opensearch\"},\"tagline\":\"The OpenSearch Project: https://opensearch.org/\"}"
|
|
111
137
|
stub_request(:get, url).to_return({:status => 200, :body => body, :headers => { 'Content-Type' => 'json' } })
|
|
@@ -340,6 +366,41 @@ class OpenSearchOutputTest < Test::Unit::TestCase
|
|
|
340
366
|
assert_equal :es, instance.endpoint.aws_service_name
|
|
341
367
|
end
|
|
342
368
|
|
|
369
|
+
test 'hosts is masked in the configuration dump' do
|
|
370
|
+
config = Fluent::Config::Element.new(
|
|
371
|
+
'ROOT', '', {
|
|
372
|
+
'@type' => 'opensearch',
|
|
373
|
+
'hosts' => 'https://john:passw0rd@host1:443/elastic/',
|
|
374
|
+
}, [
|
|
375
|
+
Fluent::Config::Element.new('buffer', 'tag', {}, [])
|
|
376
|
+
])
|
|
377
|
+
driver(config)
|
|
378
|
+
|
|
379
|
+
dump = config.to_masked_element.to_s
|
|
380
|
+
assert_false dump.include?('passw0rd')
|
|
381
|
+
assert_true dump.include?('hosts xxxxxx')
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
test 'endpoint url is masked in the configuration dump' do
|
|
385
|
+
config = Fluent::Config::Element.new(
|
|
386
|
+
'ROOT', '', {
|
|
387
|
+
'@type' => 'opensearch',
|
|
388
|
+
}, [
|
|
389
|
+
Fluent::Config::Element.new('endpoint', '', {
|
|
390
|
+
'url' => "https://john:passw0rd@search-opensearch.aws.example.com/",
|
|
391
|
+
'region' => "local",
|
|
392
|
+
'access_key_id' => 'YOUR_AWESOME_KEY',
|
|
393
|
+
'secret_access_key' => 'YOUR_AWESOME_SECRET',
|
|
394
|
+
}, []),
|
|
395
|
+
Fluent::Config::Element.new('buffer', 'tag', {}, [])
|
|
396
|
+
])
|
|
397
|
+
driver(config)
|
|
398
|
+
|
|
399
|
+
dump = config.to_masked_element.to_s
|
|
400
|
+
assert_false dump.include?('passw0rd')
|
|
401
|
+
assert_true dump.include?('url xxxxxx')
|
|
402
|
+
end
|
|
403
|
+
|
|
343
404
|
test 'aws_credentials returns credential provider with access key' do
|
|
344
405
|
config = Fluent::Config::Element.new(
|
|
345
406
|
'ROOT', '', {
|
|
@@ -1107,6 +1168,53 @@ class OpenSearchOutputTest < Test::Unit::TestCase
|
|
|
1107
1168
|
assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/myapp_alias_template-test.template", times: 1)
|
|
1108
1169
|
end
|
|
1109
1170
|
|
|
1171
|
+
# A customize_template value can come from a tag or a record field.
|
|
1172
|
+
INJECTED_TEMPLATE_VALUE = 'x": {}}, "index_patterns": ["*"], "order": 2147483647, "aliases": {"y'
|
|
1173
|
+
|
|
1174
|
+
def custom_template(template_file_name, appid)
|
|
1175
|
+
template_file = File.join(File.dirname(__FILE__), template_file_name)
|
|
1176
|
+
driver.instance.get_custom_template(template_file,
|
|
1177
|
+
{"--appid--" => appid, "--index_prefix--" => "mylogs"})
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
data("legacy_template" => ['test_alias_template.json',
|
|
1181
|
+
%w(aliases mappings order settings template)],
|
|
1182
|
+
"new_template" => ['test_index_alias_template.json',
|
|
1183
|
+
%w(index_patterns priority template)])
|
|
1184
|
+
def test_custom_template_value_cannot_add_template_keys(data)
|
|
1185
|
+
template_file_name, expected_keys = data
|
|
1186
|
+
|
|
1187
|
+
template = custom_template(template_file_name, INJECTED_TEMPLATE_VALUE)
|
|
1188
|
+
|
|
1189
|
+
assert_equal(expected_keys, template.keys.sort)
|
|
1190
|
+
end
|
|
1191
|
+
|
|
1192
|
+
def test_custom_template_value_stays_a_single_string
|
|
1193
|
+
template = custom_template('test_alias_template.json', INJECTED_TEMPLATE_VALUE)
|
|
1194
|
+
|
|
1195
|
+
assert_equal(5, template["order"])
|
|
1196
|
+
assert_equal(["#{INJECTED_TEMPLATE_VALUE}-alias"], template["aliases"].keys)
|
|
1197
|
+
end
|
|
1198
|
+
|
|
1199
|
+
def test_custom_template_value_keeps_a_backslash
|
|
1200
|
+
template = custom_template('test_alias_template.json', 'a\0b')
|
|
1201
|
+
|
|
1202
|
+
assert_equal(['a\0b-alias'], template["aliases"].keys)
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
# A key is not always inside a string. If JSON is expected there, the value
|
|
1206
|
+
# is that piece of JSON.
|
|
1207
|
+
def test_custom_template_value_outside_a_string_stays_json
|
|
1208
|
+
template_file = File.join(File.dirname(__FILE__), 'test_settings_template.json')
|
|
1209
|
+
|
|
1210
|
+
template = driver.instance.get_custom_template(template_file,
|
|
1211
|
+
{"--settings--" => '{"number_of_shards": 5}',
|
|
1212
|
+
"--index_prefix--" => "mylogs"})
|
|
1213
|
+
|
|
1214
|
+
assert_equal({"number_of_shards" => 5}, template["settings"])
|
|
1215
|
+
assert_equal("mylogs*", template["template"])
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1110
1218
|
data("legacy_template" => [true, "_template"],
|
|
1111
1219
|
"new_template" => [false, "_index_template"])
|
|
1112
1220
|
def test_custom_template_installation_for_host_placeholder(data)
|
|
@@ -1892,6 +2000,43 @@ class OpenSearchOutputTest < Test::Unit::TestCase
|
|
|
1892
2000
|
end
|
|
1893
2001
|
end
|
|
1894
2002
|
|
|
2003
|
+
# Flush threads share @_os, so only one of them may look at it at a time.
|
|
2004
|
+
def test_client_does_not_let_flush_threads_overlap
|
|
2005
|
+
instance = driver.instance
|
|
2006
|
+
counter = Mutex.new
|
|
2007
|
+
inside = 0
|
|
2008
|
+
max_inside = 0
|
|
2009
|
+
|
|
2010
|
+
instance.define_singleton_method(:get_connection_options) do |con_host = nil|
|
|
2011
|
+
counter.synchronize do
|
|
2012
|
+
inside += 1
|
|
2013
|
+
max_inside = inside if inside > max_inside
|
|
2014
|
+
end
|
|
2015
|
+
sleep 0.05
|
|
2016
|
+
counter.synchronize { inside -= 1 }
|
|
2017
|
+
{hosts: [{host: con_host, port: 9200, scheme: 'http'}]}
|
|
2018
|
+
end
|
|
2019
|
+
|
|
2020
|
+
threads = 3.times.map { |i| Thread.new { instance.client("host-#{i}") } }
|
|
2021
|
+
threads.each(&:join)
|
|
2022
|
+
|
|
2023
|
+
assert_equal(1, max_inside)
|
|
2024
|
+
end
|
|
2025
|
+
|
|
2026
|
+
def test_client_returns_a_client_for_the_host_it_was_asked_for
|
|
2027
|
+
instance = driver.instance
|
|
2028
|
+
|
|
2029
|
+
results = 3.times.map { |i|
|
|
2030
|
+
Thread.new do
|
|
2031
|
+
5.times.map { instance.client("host-#{i}:9201").transport.transport.hosts.first[:host] }
|
|
2032
|
+
end
|
|
2033
|
+
}.map(&:value)
|
|
2034
|
+
|
|
2035
|
+
results.each_with_index do |hosts, i|
|
|
2036
|
+
assert_equal(["host-#{i}"], hosts.uniq)
|
|
2037
|
+
end
|
|
2038
|
+
end
|
|
2039
|
+
|
|
1895
2040
|
def test_password_is_required_if_specify_user
|
|
1896
2041
|
config = %{
|
|
1897
2042
|
user john
|
|
@@ -2876,6 +3021,149 @@ class OpenSearchOutputTest < Test::Unit::TestCase
|
|
|
2876
3021
|
connection_spec = {host: "myhost-1", port: 9200, scheme: "http"}
|
|
2877
3022
|
assert_equal("could not push logs to OpenSearch cluster (#{connection_spec.inspect}): [503] ", exception.message)
|
|
2878
3023
|
end
|
|
3024
|
+
|
|
3025
|
+
# `extract_placeholders` needs a real chunk, so replace it with the values
|
|
3026
|
+
# that a crafted tag or record field would expand to. A bare value is the
|
|
3027
|
+
# one for "${pipeline_id}".
|
|
3028
|
+
def expand_hosts_with(hosts, placeholder_values)
|
|
3029
|
+
values = placeholder_values.is_a?(Hash) ? placeholder_values : {"pipeline_id" => placeholder_values}
|
|
3030
|
+
instance = driver.configure("hosts #{hosts}").instance
|
|
3031
|
+
instance.define_singleton_method(:extract_placeholders) do |value, _chunk|
|
|
3032
|
+
values.inject(value) { |expanded, (name, replacement)| expanded.gsub("${#{name}}", replacement) }
|
|
3033
|
+
end
|
|
3034
|
+
instance.expand_host_placeholders(nil)
|
|
3035
|
+
end
|
|
3036
|
+
|
|
3037
|
+
data("host name" => ["${pipeline_id},logs2.example.com:9201", "os.internal"],
|
|
3038
|
+
"with port" => ["${pipeline_id},logs2.example.com:9201", "os.internal:9200"],
|
|
3039
|
+
"in a url" => ["https://${pipeline_id}/os", "logs.example.com:9201"],
|
|
3040
|
+
"ipv6" => ["http://${pipeline_id}:9201", "[2001:db8::1]"],
|
|
3041
|
+
"ipv6 + port" => ["http://${pipeline_id}/os", "[2001:db8::1]:9201"],
|
|
3042
|
+
"ipv6 in []" => ["http://[${pipeline_id}]:9201", "2001:db8::1"])
|
|
3043
|
+
def test_accepts_placeholder_value_which_fills_in_the_whole_host(data)
|
|
3044
|
+
hosts, placeholder_value = data
|
|
3045
|
+
assert_equal(hosts.sub('${pipeline_id}', placeholder_value),
|
|
3046
|
+
expand_hosts_with(hosts, placeholder_value))
|
|
3047
|
+
end
|
|
3048
|
+
|
|
3049
|
+
data("extra host" => "evil.example.com,myhost",
|
|
3050
|
+
"userinfo" => "1@evil.example.com",
|
|
3051
|
+
"path" => "1/evil.example.com",
|
|
3052
|
+
"space" => "1 evil.example.com",
|
|
3053
|
+
"other domain" => "evil.example.net",
|
|
3054
|
+
"port" => "1:9999",
|
|
3055
|
+
"empty" => "")
|
|
3056
|
+
def test_rejects_placeholder_value_which_does_not_fit_its_place(placeholder_value)
|
|
3057
|
+
assert_raise(Fluent::Plugin::OpenSearchOutput::UnrecoverableRequestFailure) {
|
|
3058
|
+
expand_hosts_with("myhost-${pipeline_id},logs2.example.com:9201", placeholder_value)
|
|
3059
|
+
}
|
|
3060
|
+
end
|
|
3061
|
+
|
|
3062
|
+
# The operator wrote ".example.com", so a tag such as "nginx.access" only
|
|
3063
|
+
# adds a sub domain to it.
|
|
3064
|
+
def test_accepts_placeholder_value_with_a_sub_domain
|
|
3065
|
+
assert_equal("https://logs-nginx.access.example.com/os",
|
|
3066
|
+
expand_hosts_with("https://logs-${pipeline_id}.example.com/os", "nginx.access"))
|
|
3067
|
+
end
|
|
3068
|
+
|
|
3069
|
+
# ".example.com" is fixed even though another placeholder stands before it.
|
|
3070
|
+
def test_accepts_placeholder_value_with_a_sub_domain_before_another_placeholder
|
|
3071
|
+
assert_equal("https://nginx.access.us.example.com/os",
|
|
3072
|
+
expand_hosts_with("https://${pipeline_id}.${region}.example.com/os",
|
|
3073
|
+
{"pipeline_id" => "nginx.access", "region" => "us"}))
|
|
3074
|
+
end
|
|
3075
|
+
|
|
3076
|
+
# A trailing "." or a single name after the placeholder fixes no domain.
|
|
3077
|
+
data("trailing dot" => ["myhost-${pipeline_id}.", "evil.example.com"],
|
|
3078
|
+
"one name" => ["https://logs-${pipeline_id}.com/os", "evil.attacker"])
|
|
3079
|
+
def test_rejects_placeholder_value_which_leaves_the_configured_domain(data)
|
|
3080
|
+
hosts, placeholder_value = data
|
|
3081
|
+
assert_raise(Fluent::Plugin::OpenSearchOutput::UnrecoverableRequestFailure) {
|
|
3082
|
+
expand_hosts_with(hosts, placeholder_value)
|
|
3083
|
+
}
|
|
3084
|
+
end
|
|
3085
|
+
|
|
3086
|
+
data("extra host" => "evil.example.com,myhost",
|
|
3087
|
+
"port" => "1:9999",
|
|
3088
|
+
"empty" => "")
|
|
3089
|
+
def test_rejects_placeholder_value_which_is_not_a_sub_domain(placeholder_value)
|
|
3090
|
+
assert_raise(Fluent::Plugin::OpenSearchOutput::UnrecoverableRequestFailure) {
|
|
3091
|
+
expand_hosts_with("https://logs-${pipeline_id}.example.com/os", placeholder_value)
|
|
3092
|
+
}
|
|
3093
|
+
end
|
|
3094
|
+
|
|
3095
|
+
# `get_connection_options` reads an element with `URI()`, which cannot
|
|
3096
|
+
# read an IPv6 address when the element has no scheme.
|
|
3097
|
+
data("empty" => "",
|
|
3098
|
+
"port only" => ":9999",
|
|
3099
|
+
"two ports" => "os.internal:9200:9201",
|
|
3100
|
+
"path" => "myhost/evil.example.com",
|
|
3101
|
+
"ipv6" => "[2001:db8::1]")
|
|
3102
|
+
def test_rejects_placeholder_value_which_is_not_a_host_name(placeholder_value)
|
|
3103
|
+
assert_raise(Fluent::Plugin::OpenSearchOutput::UnrecoverableRequestFailure) {
|
|
3104
|
+
expand_hosts_with("${pipeline_id},logs2.example.com:9201", placeholder_value)
|
|
3105
|
+
}
|
|
3106
|
+
end
|
|
3107
|
+
|
|
3108
|
+
data("host name" => "os.internal:9200",
|
|
3109
|
+
"ipv6" => "[2001:db8::1]:9200")
|
|
3110
|
+
def test_rejects_placeholder_value_with_a_port_when_the_port_is_configured(placeholder_value)
|
|
3111
|
+
assert_raise(Fluent::Plugin::OpenSearchOutput::UnrecoverableRequestFailure) {
|
|
3112
|
+
expand_hosts_with("http://${pipeline_id}:9201", placeholder_value)
|
|
3113
|
+
}
|
|
3114
|
+
end
|
|
3115
|
+
|
|
3116
|
+
data("in []" => ["http://[${pipeline_id}]:9201", "2001:db8"],
|
|
3117
|
+
"extra host" => ["http://[${pipeline_id}]:9201", "[2001:db8::1],[2001:db8::2]"],
|
|
3118
|
+
"whole host" => ["http://${pipeline_id}:9201", "[2001:db8]"])
|
|
3119
|
+
def test_rejects_placeholder_value_which_is_not_an_ipv6_address(data)
|
|
3120
|
+
hosts, placeholder_value = data
|
|
3121
|
+
assert_raise(Fluent::Plugin::OpenSearchOutput::UnrecoverableRequestFailure) {
|
|
3122
|
+
expand_hosts_with(hosts, placeholder_value)
|
|
3123
|
+
}
|
|
3124
|
+
end
|
|
3125
|
+
|
|
3126
|
+
# The operator writes the structure, so only the placeholder value is
|
|
3127
|
+
# checked. Every form that `get_connection_options` accepts keeps working.
|
|
3128
|
+
data("plain host" => ["myhost-${pipeline_id},logs2.example.com:9201",
|
|
3129
|
+
"myhost-1,logs2.example.com:9201"],
|
|
3130
|
+
"url" => ["https://logs-${pipeline_id}.example.com:9201/os",
|
|
3131
|
+
"https://logs-1.example.com:9201/os"],
|
|
3132
|
+
"userinfo" => ["https://john:pass@logs-${pipeline_id}.example.com/os",
|
|
3133
|
+
"https://john:pass@logs-1.example.com/os"],
|
|
3134
|
+
"ipv6" => ["http://[2001:db8::1]:9201,myhost-${pipeline_id}",
|
|
3135
|
+
"http://[2001:db8::1]:9201,myhost-1"])
|
|
3136
|
+
def test_keeps_the_configured_host_structure(data)
|
|
3137
|
+
hosts, expected = data
|
|
3138
|
+
assert_equal(expected, expand_hosts_with(hosts, "1"))
|
|
3139
|
+
end
|
|
3140
|
+
|
|
3141
|
+
def test_warns_when_placeholder_host_is_used_with_credentials
|
|
3142
|
+
d = driver(%{
|
|
3143
|
+
host logs-${tag}.example.com
|
|
3144
|
+
user john
|
|
3145
|
+
password doe
|
|
3146
|
+
@log_level info
|
|
3147
|
+
})
|
|
3148
|
+
assert_true(d.logs.any? { |log| log.include?("'host' uses a placeholder") })
|
|
3149
|
+
end
|
|
3150
|
+
|
|
3151
|
+
def test_warns_when_hosts_embeds_credentials_in_front_of_a_placeholder
|
|
3152
|
+
d = driver(%{
|
|
3153
|
+
hosts https://john:secret@logs-${tag}.example.com/os
|
|
3154
|
+
@log_level info
|
|
3155
|
+
})
|
|
3156
|
+
assert_true(d.logs.any? { |log| log.include?("in front of a host that a placeholder decides") })
|
|
3157
|
+
assert_false(d.logs.any? { |log| log.include?("secret") })
|
|
3158
|
+
end
|
|
3159
|
+
|
|
3160
|
+
def test_does_not_warn_when_credentials_belong_to_a_static_host
|
|
3161
|
+
d = driver(%{
|
|
3162
|
+
hosts https://john:secret@static.example.com/os,logs-${tag}.example.com
|
|
3163
|
+
@log_level info
|
|
3164
|
+
})
|
|
3165
|
+
assert_false(d.logs.any? { |log| log.include?("in front of a host that a placeholder decides") })
|
|
3166
|
+
end
|
|
2879
3167
|
end
|
|
2880
3168
|
|
|
2881
3169
|
def test_writes_to_logstash_index_with_specified_prefix_uppercase
|
|
@@ -4117,4 +4405,43 @@ class OpenSearchOutputTest < Test::Unit::TestCase
|
|
|
4117
4405
|
d.run
|
|
4118
4406
|
assert { d.logs.any?(/\[error\]: Failed to get new AWS credentials: No valid AWS credentials found.\n/) }
|
|
4119
4407
|
end
|
|
4408
|
+
|
|
4409
|
+
test 'a failed credential refresh keeps the current credentials and client' do
|
|
4410
|
+
d, refresh = driver_with_captured_credential_refresh
|
|
4411
|
+
instance = d.instance
|
|
4412
|
+
credentials = instance.instance_variable_get(:@_aws_credentials)
|
|
4413
|
+
instance.instance_variable_set(:@_os, :cached_client)
|
|
4414
|
+
flexmock(instance).should_receive(:aws_credentials)
|
|
4415
|
+
.and_raise(::RuntimeError.new("No valid AWS credentials found."))
|
|
4416
|
+
|
|
4417
|
+
refresh.call
|
|
4418
|
+
|
|
4419
|
+
assert_same credentials, instance.instance_variable_get(:@_aws_credentials)
|
|
4420
|
+
assert_equal :cached_client, instance.instance_variable_get(:@_os)
|
|
4421
|
+
assert { d.logs.any?(/Failed to get new AWS credentials: No valid AWS credentials found./) }
|
|
4422
|
+
end
|
|
4423
|
+
|
|
4424
|
+
# The refresh shares @_os with the flush threads, so it has to use their lock
|
|
4425
|
+
# instead of one of its own.
|
|
4426
|
+
test 'a credential refresh swaps the credentials under the client lock' do
|
|
4427
|
+
d, refresh = driver_with_captured_credential_refresh
|
|
4428
|
+
instance = d.instance
|
|
4429
|
+
new_credentials = Aws::Credentials.new('NEW_KEY', 'NEW_SECRET')
|
|
4430
|
+
flexmock(instance).should_receive(:aws_credentials).and_return(new_credentials)
|
|
4431
|
+
instance.instance_variable_set(:@_os, :cached_client)
|
|
4432
|
+
|
|
4433
|
+
# Records what the plugin had done by the time it left the lock.
|
|
4434
|
+
seen = []
|
|
4435
|
+
fake_lock = Object.new
|
|
4436
|
+
fake_lock.define_singleton_method(:synchronize) do |&block|
|
|
4437
|
+
block.call
|
|
4438
|
+
seen << [instance.instance_variable_get(:@_aws_credentials),
|
|
4439
|
+
instance.instance_variable_get(:@_os)]
|
|
4440
|
+
end
|
|
4441
|
+
instance.instance_variable_set(:@client_mutex, fake_lock)
|
|
4442
|
+
|
|
4443
|
+
refresh.call
|
|
4444
|
+
|
|
4445
|
+
assert_equal [[new_credentials, nil]], seen
|
|
4446
|
+
end
|
|
4120
4447
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fluent-plugin-opensearch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hiroshi Hatake
|
|
@@ -249,6 +249,7 @@ files:
|
|
|
249
249
|
- test/plugin/test_opensearch_tls.rb
|
|
250
250
|
- test/plugin/test_out_opensearch.rb
|
|
251
251
|
- test/plugin/test_out_opensearch_data_stream.rb
|
|
252
|
+
- test/plugin/test_settings_template.json
|
|
252
253
|
- test/plugin/test_template.json
|
|
253
254
|
- test/test_log-ext.rb
|
|
254
255
|
homepage: https://github.com/fluent/fluent-plugin-opensearch
|
|
@@ -270,7 +271,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
270
271
|
- !ruby/object:Gem::Version
|
|
271
272
|
version: '0'
|
|
272
273
|
requirements: []
|
|
273
|
-
rubygems_version:
|
|
274
|
+
rubygems_version: 3.6.9
|
|
274
275
|
specification_version: 4
|
|
275
276
|
summary: Opensearch output plugin for Fluent event collector
|
|
276
277
|
test_files:
|
|
@@ -287,5 +288,6 @@ test_files:
|
|
|
287
288
|
- test/plugin/test_opensearch_tls.rb
|
|
288
289
|
- test/plugin/test_out_opensearch.rb
|
|
289
290
|
- test/plugin/test_out_opensearch_data_stream.rb
|
|
291
|
+
- test/plugin/test_settings_template.json
|
|
290
292
|
- test/plugin/test_template.json
|
|
291
293
|
- test/test_log-ext.rb
|