connectors_service 8.6.0.3 → 8.6.0.4.pre.20221104T200814Z
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/config/connectors.yml +8 -9
- data/lib/app/config.rb +2 -0
- data/lib/app/dispatcher.rb +5 -1
- data/lib/app/preflight_check.rb +4 -0
- data/lib/connectors/base/connector.rb +29 -3
- data/lib/connectors/connector_status.rb +4 -4
- data/lib/connectors/example/{example_attachments → attachments}/first_attachment.txt +0 -0
- data/lib/connectors/example/{example_attachments → attachments}/second_attachment.txt +0 -0
- data/lib/connectors/example/{example_attachments → attachments}/third_attachment.txt +0 -0
- data/lib/connectors/example/connector.rb +28 -4
- data/lib/connectors/gitlab/connector.rb +1 -1
- data/lib/connectors/mongodb/connector.rb +136 -32
- data/lib/connectors/registry.rb +2 -2
- data/lib/connectors/sync_status.rb +23 -4
- data/lib/core/configuration.rb +1 -1
- data/lib/core/connector_settings.rb +12 -14
- data/lib/core/elastic_connector_actions.rb +72 -12
- data/lib/core/native_scheduler.rb +3 -0
- data/lib/core/scheduler.rb +3 -0
- data/lib/core/single_scheduler.rb +3 -0
- data/lib/core/sync_job_runner.rb +19 -7
- data/lib/{connectors_app/// → utility/common.rb} +12 -5
- data/lib/utility/errors.rb +5 -0
- data/lib/utility/es_client.rb +6 -2
- data/lib/utility.rb +1 -0
- metadata +13 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23b78b5c5228f761b884af1bfd75b163ad912dc53331ce5a64cd3edb8f45ef86
|
4
|
+
data.tar.gz: 5c74e06f315ab9af4161e88fac626e84b6d2789c0b5b080c713e780a4560ef97
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 75295f0d26061a977089e999190f4974f4929ca92c25c09e7ecbbd1f0117476f758b7f200b719461bdd9e7b9e6193df455d3e52bcec0634fc85a7b512721ab6a
|
7
|
+
data.tar.gz: fa12af986f72d10081245e782a9152a887512e62f48ad525af59e59a4ef72238727e7bfd521f516fd452a2e74a1101c5bd1965f1e9b97f7b236b8ae2ccf626ff
|
data/config/connectors.yml
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
# general metadata
|
2
|
-
version: 8.6.0.
|
3
|
-
repository:
|
4
|
-
revision:
|
2
|
+
version: 8.6.0.4-20221104T200814Z
|
3
|
+
repository: git@github.com:elastic/ent-search-connectors.git
|
4
|
+
revision: 2051b3907639a1fe2ae68efdc33c06cf12d38383
|
5
5
|
elasticsearch:
|
6
|
-
cloud_id: CHANGEME
|
7
6
|
hosts: http://localhost:9200
|
8
|
-
api_key:
|
7
|
+
api_key: OW1FalJJUUI1clBtUVh5RVo1QmU6QVp5LV9pU3RRUXFYb2VVYnlCRWNZdw==
|
9
8
|
retry_on_failure: 3
|
10
9
|
request_timeout: 120
|
11
10
|
disable_warnings: true
|
@@ -16,10 +15,10 @@ thread_pool:
|
|
16
15
|
max_threads: 5
|
17
16
|
max_queue: 100
|
18
17
|
log_level: info
|
19
|
-
ecs_logging:
|
18
|
+
ecs_logging: false
|
20
19
|
poll_interval: 3
|
21
20
|
termination_timeout: 60
|
22
21
|
heartbeat_interval: 1800
|
23
|
-
native_mode:
|
24
|
-
connector_id:
|
25
|
-
service_type:
|
22
|
+
native_mode: false
|
23
|
+
connector_id: 9WEjRIQB5rPmQXyEWJB2
|
24
|
+
service_type: example
|
data/lib/app/config.rb
CHANGED
@@ -35,6 +35,8 @@ puts "Parsing #{CONFIG_FILE} configuration file."
|
|
35
35
|
optional(:disable_warnings).value(:bool?)
|
36
36
|
optional(:trace).value(:bool?)
|
37
37
|
optional(:log).value(:bool?)
|
38
|
+
optional(:ca_fingerprint).value(:string)
|
39
|
+
optional(:transport_options).value(:hash)
|
38
40
|
end
|
39
41
|
|
40
42
|
optional(:thread_pool).hash do
|
data/lib/app/dispatcher.rb
CHANGED
@@ -84,10 +84,14 @@ module App
|
|
84
84
|
def start_sync_task(connector_settings)
|
85
85
|
start_heartbeat_task(connector_settings)
|
86
86
|
pool.post do
|
87
|
-
Utility::Logger.info("
|
87
|
+
Utility::Logger.info("Initiating a sync job for #{connector_settings.formatted}...")
|
88
88
|
Core::ElasticConnectorActions.ensure_content_index_exists(connector_settings.index_name)
|
89
89
|
job_runner = Core::SyncJobRunner.new(connector_settings)
|
90
90
|
job_runner.execute
|
91
|
+
rescue Core::JobAlreadyRunningError
|
92
|
+
Utility::Logger.info("Sync job for #{connector_settings.formatted} is already running, skipping.")
|
93
|
+
rescue Core::ConnectorVersionChangedError => e
|
94
|
+
Utility::Logger.info("Could not start the job because #{connector_settings.formatted} has been updated externally. Message: #{e.message}")
|
91
95
|
rescue StandardError => e
|
92
96
|
Utility::ExceptionTracking.log_exception(e, "Sync job for #{connector_settings.formatted} failed due to unexpected error.")
|
93
97
|
end
|
data/lib/app/preflight_check.rb
CHANGED
@@ -75,6 +75,10 @@ module App
|
|
75
75
|
else
|
76
76
|
raise UnhealthyCluster, "Unexpected cluster status: #{response['status']}"
|
77
77
|
end
|
78
|
+
rescue *Utility::AUTHORIZATION_ERRORS => e
|
79
|
+
Utility::ExceptionTracking.log_exception(e)
|
80
|
+
|
81
|
+
fail_check!("Elasticsearch returned 'Unauthorized' response. Check your authentication details. Terminating...")
|
78
82
|
rescue *App::RETRYABLE_CONNECTION_ERRORS => e
|
79
83
|
Utility::Logger.warn('Could not connect to Elasticsearch. Make sure it is running and healthy.')
|
80
84
|
Utility::Logger.debug("Error: #{e.full_message}")
|
@@ -8,9 +8,9 @@
|
|
8
8
|
|
9
9
|
require 'bson'
|
10
10
|
require 'core/output_sink'
|
11
|
-
require 'utility
|
12
|
-
require 'utility/errors'
|
11
|
+
require 'utility'
|
13
12
|
require 'app/config'
|
13
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
14
14
|
|
15
15
|
module Connectors
|
16
16
|
module Base
|
@@ -19,6 +19,11 @@ module Connectors
|
|
19
19
|
raise 'Not implemented for this connector'
|
20
20
|
end
|
21
21
|
|
22
|
+
# Used as a framework util method, don't override
|
23
|
+
def self.configurable_fields_indifferent_access
|
24
|
+
configurable_fields.with_indifferent_access
|
25
|
+
end
|
26
|
+
|
22
27
|
def self.configurable_fields
|
23
28
|
{}
|
24
29
|
end
|
@@ -27,8 +32,16 @@ module Connectors
|
|
27
32
|
raise 'Not implemented for this connector'
|
28
33
|
end
|
29
34
|
|
30
|
-
|
35
|
+
attr_reader :rules, :advanced_filter_config
|
36
|
+
|
37
|
+
def initialize(configuration: {}, job_description: {})
|
31
38
|
@configuration = configuration.dup || {}
|
39
|
+
@job_description = job_description&.dup || {}
|
40
|
+
|
41
|
+
filter = get_filter(@job_description[:filtering])
|
42
|
+
|
43
|
+
@rules = Utility::Common.return_if_present(filter[:rules], [])
|
44
|
+
@advanced_filter_config = Utility::Common.return_if_present(filter[:advanced_config], {})
|
32
45
|
end
|
33
46
|
|
34
47
|
def yield_documents; end
|
@@ -52,6 +65,19 @@ module Connectors
|
|
52
65
|
Utility::ExceptionTracking.log_exception(e, "Connector for service #{self.class.service_type} failed the health check for 3rd-party service.")
|
53
66
|
false
|
54
67
|
end
|
68
|
+
|
69
|
+
def filtering_present?
|
70
|
+
@advanced_filter_config.present? || @rules.present?
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def get_filter(filtering)
|
76
|
+
# assume for now, that first object in filtering array or a filter object itself is the only filtering object
|
77
|
+
filter = filtering.is_a?(Array) ? filtering[0] : filtering
|
78
|
+
|
79
|
+
filter.present? ? filter : {}
|
80
|
+
end
|
55
81
|
end
|
56
82
|
end
|
57
83
|
end
|
@@ -8,11 +8,11 @@
|
|
8
8
|
|
9
9
|
module Connectors
|
10
10
|
class ConnectorStatus
|
11
|
-
CREATED
|
11
|
+
CREATED = 'created'
|
12
12
|
NEEDS_CONFIGURATION = 'needs_configuration'
|
13
|
-
CONFIGURED
|
14
|
-
CONNECTED
|
15
|
-
ERROR
|
13
|
+
CONFIGURED = 'configured'
|
14
|
+
CONNECTED = 'connected'
|
15
|
+
ERROR = 'error'
|
16
16
|
|
17
17
|
STATUSES = [
|
18
18
|
CREATED,
|
File without changes
|
File without changes
|
File without changes
|
@@ -20,16 +20,21 @@ module Connectors
|
|
20
20
|
'Example Connector'
|
21
21
|
end
|
22
22
|
|
23
|
+
# Field 'Foo' won't have a default value. Field 'Bar' will have the default value 'Value'.
|
23
24
|
def self.configurable_fields
|
24
25
|
{
|
25
26
|
'foo' => {
|
26
27
|
'label' => 'Foo',
|
27
28
|
'value' => nil
|
29
|
+
},
|
30
|
+
:bar => {
|
31
|
+
:label => 'Bar',
|
32
|
+
:value => 'Value'
|
28
33
|
}
|
29
34
|
}
|
30
35
|
end
|
31
36
|
|
32
|
-
def initialize(configuration: {})
|
37
|
+
def initialize(configuration: {}, job_description: {})
|
33
38
|
super
|
34
39
|
end
|
35
40
|
|
@@ -42,16 +47,35 @@ module Connectors
|
|
42
47
|
|
43
48
|
def yield_documents
|
44
49
|
attachments = [
|
45
|
-
|
46
|
-
|
47
|
-
|
50
|
+
load_attachment('first_attachment.txt'),
|
51
|
+
load_attachment('second_attachment.txt'),
|
52
|
+
load_attachment('third_attachment.txt'),
|
48
53
|
]
|
49
54
|
|
50
55
|
attachments.each_with_index do |att, index|
|
51
56
|
data = { id: (index + 1).to_s, name: "example document #{index + 1}", _attachment: File.read(att) }
|
57
|
+
|
58
|
+
# Uncomment one of these two lines to simulate longer running sync jobs
|
59
|
+
#
|
60
|
+
# sleep(rand(10..60).seconds)
|
61
|
+
# sleep(rand(1..10).minutes)
|
62
|
+
|
52
63
|
yield data
|
53
64
|
end
|
54
65
|
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def load_attachment(path)
|
70
|
+
attachment_dir = "#{File.dirname(__FILE__)}/attachments"
|
71
|
+
attachment_path = "#{attachment_dir}/#{path}"
|
72
|
+
|
73
|
+
unless File.exist?(attachment_path)
|
74
|
+
raise "Attachment at location '#{attachment_path}' doesn't exist. Attachments should be located under #{attachment_dir}"
|
75
|
+
end
|
76
|
+
|
77
|
+
File.open(attachment_path)
|
78
|
+
end
|
55
79
|
end
|
56
80
|
end
|
57
81
|
end
|
@@ -6,13 +6,18 @@
|
|
6
6
|
|
7
7
|
# frozen_string_literal: true
|
8
8
|
|
9
|
-
require 'active_support/core_ext/hash/indifferent_access'
|
10
9
|
require 'connectors/base/connector'
|
11
10
|
require 'mongo'
|
11
|
+
require 'utility'
|
12
12
|
|
13
13
|
module Connectors
|
14
14
|
module MongoDB
|
15
15
|
class Connector < Connectors::Base::Connector
|
16
|
+
|
17
|
+
ALLOWED_TOP_LEVEL_FILTER_KEYS = %w[find aggregate]
|
18
|
+
|
19
|
+
PAGE_SIZE = 100
|
20
|
+
|
16
21
|
def self.service_type
|
17
22
|
'mongodb'
|
18
23
|
end
|
@@ -44,7 +49,7 @@ module Connectors
|
|
44
49
|
}
|
45
50
|
end
|
46
51
|
|
47
|
-
def initialize(configuration: {})
|
52
|
+
def initialize(configuration: {}, job_description: {})
|
48
53
|
super
|
49
54
|
|
50
55
|
@host = configuration.dig(:host, :value)
|
@@ -56,17 +61,108 @@ module Connectors
|
|
56
61
|
end
|
57
62
|
|
58
63
|
def yield_documents
|
64
|
+
check_filtering
|
65
|
+
|
59
66
|
with_client do |client|
|
60
|
-
|
61
|
-
|
67
|
+
# We do paging using skip().limit() here to make Ruby recycle the memory for each page pulled from the server after it's not needed any more.
|
68
|
+
# This gives us more control on the usage of the memory (we can adjust PAGE_SIZE constant for that to decrease max memory consumption).
|
69
|
+
# It's done due to the fact that usage of .find.each leads to memory leaks or overuse of memory - the whole result set seems to stay in memory
|
70
|
+
# during the sync. Sometimes (not 100% sure) it even leads to a real leak, when the memory for these objects is never recycled.
|
71
|
+
cursor, options = create_db_cursor_on_collection(client[@collection])
|
72
|
+
skip = 0
|
73
|
+
|
74
|
+
found_overall = 0
|
75
|
+
|
76
|
+
# if no overall limit is specified by filtering use -1 to not break ingestion, when no overall limit is specified (found_overall is only increased,
|
77
|
+
# thus can never reach -1)
|
78
|
+
overall_limit = Float::INFINITY
|
79
|
+
|
80
|
+
if options.present?
|
81
|
+
# there could be a skip parameter defined for filtering
|
82
|
+
skip = options.fetch(:skip, skip)
|
83
|
+
# there could be a limit parameter defined for filtering -> used for an overall limit (not a page limit, which was introduced for memory optimization)
|
84
|
+
overall_limit = options.fetch(:limit, overall_limit)
|
85
|
+
end
|
86
|
+
|
87
|
+
overall_limit_reached = false
|
88
|
+
|
89
|
+
loop do
|
90
|
+
found_in_page = 0
|
91
|
+
|
92
|
+
view = cursor.skip(skip).limit(PAGE_SIZE)
|
93
|
+
view.each do |document|
|
94
|
+
yield serialize(document)
|
95
|
+
|
96
|
+
found_in_page += 1
|
97
|
+
found_overall += 1
|
98
|
+
|
99
|
+
overall_limit_reached = found_overall >= overall_limit && overall_limit != Float::INFINITY
|
100
|
+
|
101
|
+
break if overall_limit_reached
|
102
|
+
end
|
103
|
+
|
104
|
+
page_was_empty = found_in_page == 0
|
105
|
+
|
106
|
+
break if page_was_empty || overall_limit_reached
|
62
107
|
|
63
|
-
|
108
|
+
skip += PAGE_SIZE
|
64
109
|
end
|
65
110
|
end
|
66
111
|
end
|
67
112
|
|
68
113
|
private
|
69
114
|
|
115
|
+
def create_db_cursor_on_collection(collection)
|
116
|
+
return create_find_cursor(collection) if @advanced_filter_config[:find].present?
|
117
|
+
|
118
|
+
return create_aggregate_cursor(collection) if @advanced_filter_config[:aggregate].present?
|
119
|
+
|
120
|
+
collection.find
|
121
|
+
end
|
122
|
+
|
123
|
+
def check_filtering
|
124
|
+
return unless filtering_present?
|
125
|
+
|
126
|
+
check_find_and_aggregate
|
127
|
+
end
|
128
|
+
|
129
|
+
def check_find_and_aggregate
|
130
|
+
if @advanced_filter_config.keys.size != 1
|
131
|
+
invalid_keys_msg = "Only one of #{ALLOWED_TOP_LEVEL_FILTER_KEYS} is allowed in the filtering object. Keys present: '#{@advanced_filter_config.keys}'."
|
132
|
+
raise Utility::InvalidFilterConfigError.new(invalid_keys_msg)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def create_aggregate_cursor(collection)
|
137
|
+
aggregate = @advanced_filter_config[:aggregate]
|
138
|
+
|
139
|
+
pipeline = aggregate[:pipeline]
|
140
|
+
options = extract_options(aggregate)
|
141
|
+
|
142
|
+
if !pipeline.nil? && pipeline.empty? && !options.present?
|
143
|
+
Utility::Logger.warn('\'Aggregate\' was specified with an empty pipeline and empty options.')
|
144
|
+
end
|
145
|
+
|
146
|
+
[collection.aggregate(pipeline, options), options]
|
147
|
+
end
|
148
|
+
|
149
|
+
def create_find_cursor(collection)
|
150
|
+
find = @advanced_filter_config[:find]
|
151
|
+
|
152
|
+
filter = find[:filter]
|
153
|
+
options = extract_options(find)
|
154
|
+
|
155
|
+
if !filter.nil? && filter.empty? && !options.present?
|
156
|
+
Utility::Logger.warn('\'Find\' was specified with an empty filter and empty options.')
|
157
|
+
end
|
158
|
+
|
159
|
+
[collection.find(filter, options), options]
|
160
|
+
end
|
161
|
+
|
162
|
+
def extract_options(mongodb_function)
|
163
|
+
mongodb_function[:options].present? ? mongodb_function[:options] : {}
|
164
|
+
end
|
165
|
+
|
70
166
|
def do_health_check
|
71
167
|
with_client do |_client|
|
72
168
|
Utility::Logger.debug("Mongo at #{@host}/#{@database} looks healthy.")
|
@@ -76,34 +172,43 @@ module Connectors
|
|
76
172
|
def with_client
|
77
173
|
raise "Invalid value for 'Direct connection' : #{@direct_connection}." unless %w[true false].include?(@direct_connection.to_s.strip.downcase)
|
78
174
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
Utility::Logger.debug(
|
98
|
-
|
99
|
-
client.collections.each { |coll| Utility::Logger.debug(coll.name) }
|
175
|
+
args = {
|
176
|
+
database: @database,
|
177
|
+
direct_connection: to_boolean(@direct_connection)
|
178
|
+
}
|
179
|
+
|
180
|
+
if @user.present? || @password.present?
|
181
|
+
args[:user] = @user
|
182
|
+
args[:password] = @password
|
183
|
+
end
|
184
|
+
|
185
|
+
Mongo::Client.new(@host, args) do |client|
|
186
|
+
databases = client.database_names
|
187
|
+
|
188
|
+
Utility::Logger.debug("Existing Databases: #{databases}")
|
189
|
+
check_database_exists!(databases, @database)
|
190
|
+
|
191
|
+
collections = client.database.collection_names
|
192
|
+
|
193
|
+
Utility::Logger.debug("Existing Collections: #{collections}")
|
194
|
+
check_collection_exists!(collections, @database, @collection)
|
100
195
|
|
101
196
|
yield client
|
102
|
-
ensure
|
103
|
-
client.close
|
104
197
|
end
|
105
198
|
end
|
106
199
|
|
200
|
+
def check_database_exists!(databases, database)
|
201
|
+
return if databases.include?(database)
|
202
|
+
|
203
|
+
raise "Database (#{database}) does not exist. Existing databases: #{databases.join(', ')}"
|
204
|
+
end
|
205
|
+
|
206
|
+
def check_collection_exists!(collections, database, collection)
|
207
|
+
return if collections.include?(collection)
|
208
|
+
|
209
|
+
raise "Collection (#{collection}) does not exist within database '#{database}'. Existing collections: #{collections.join(', ')}"
|
210
|
+
end
|
211
|
+
|
107
212
|
def serialize(mongodb_document)
|
108
213
|
# This is some lazy serialization here.
|
109
214
|
# Problem: MongoDB has its own format of things - e.g. ids are Bson::ObjectId, which when serialized to JSON
|
@@ -120,11 +225,10 @@ module Connectors
|
|
120
225
|
mongodb_document.map { |v| serialize(v) }
|
121
226
|
when Hash
|
122
227
|
mongodb_document.map do |key, value|
|
123
|
-
|
124
|
-
|
228
|
+
key = 'id' if key == '_id'
|
125
229
|
remapped_value = serialize(value)
|
126
|
-
[
|
127
|
-
end.to_h
|
230
|
+
[key, remapped_value]
|
231
|
+
end.to_h
|
128
232
|
else
|
129
233
|
mongodb_document
|
130
234
|
end
|
data/lib/connectors/registry.rb
CHANGED
@@ -24,10 +24,10 @@ module Connectors
|
|
24
24
|
@connectors[name]
|
25
25
|
end
|
26
26
|
|
27
|
-
def connector(name, configuration)
|
27
|
+
def connector(name, configuration, job_description: {})
|
28
28
|
klass = connector_class(name)
|
29
29
|
if klass.present?
|
30
|
-
return klass.new(configuration: configuration)
|
30
|
+
return klass.new(configuration: configuration, job_description: job_description)
|
31
31
|
end
|
32
32
|
raise "Connector #{name} is not yet registered. You need to register it before use"
|
33
33
|
end
|
@@ -8,14 +8,33 @@
|
|
8
8
|
|
9
9
|
module Connectors
|
10
10
|
class SyncStatus
|
11
|
-
|
11
|
+
PENDING = 'pending'
|
12
12
|
IN_PROGRESS = 'in_progress'
|
13
|
-
|
13
|
+
CANCELING = 'canceling'
|
14
|
+
CANCELED = 'canceled'
|
15
|
+
SUSPENDED = 'suspended'
|
16
|
+
COMPLETED = 'completed'
|
17
|
+
ERROR = 'error'
|
14
18
|
|
15
19
|
STATUSES = [
|
16
|
-
|
20
|
+
PENDING,
|
17
21
|
IN_PROGRESS,
|
18
|
-
|
22
|
+
CANCELING,
|
23
|
+
CANCELED,
|
24
|
+
SUSPENDED,
|
25
|
+
COMPLETED,
|
26
|
+
ERROR
|
27
|
+
]
|
28
|
+
|
29
|
+
PENDING_STATUES = [
|
30
|
+
PENDING,
|
31
|
+
SUSPENDED
|
32
|
+
]
|
33
|
+
|
34
|
+
TERMINAL_STATUSES = [
|
35
|
+
CANCELED,
|
36
|
+
COMPLETED,
|
37
|
+
ERROR
|
19
38
|
]
|
20
39
|
end
|
21
40
|
end
|
data/lib/core/configuration.rb
CHANGED
@@ -23,7 +23,7 @@ module Core
|
|
23
23
|
Utility::Logger.error("Couldn't find connector for service type #{connector_settings.service_type || service_type}")
|
24
24
|
return
|
25
25
|
end
|
26
|
-
configuration = connector_class.
|
26
|
+
configuration = connector_class.configurable_fields_indifferent_access
|
27
27
|
doc = {
|
28
28
|
:configuration => configuration
|
29
29
|
}
|
@@ -19,6 +19,8 @@ module Core
|
|
19
19
|
DEFAULT_REDUCE_WHITESPACE = true
|
20
20
|
DEFAULT_RUN_ML_INFERENCE = true
|
21
21
|
|
22
|
+
DEFAULT_FILTERING = {}
|
23
|
+
|
22
24
|
DEFAULT_PAGE_SIZE = 100
|
23
25
|
|
24
26
|
# Error Classes
|
@@ -80,20 +82,24 @@ module Core
|
|
80
82
|
self[:scheduling]
|
81
83
|
end
|
82
84
|
|
85
|
+
def filtering
|
86
|
+
Utility::Common.return_if_present(@elasticsearch_response[:filtering], DEFAULT_FILTERING)
|
87
|
+
end
|
88
|
+
|
83
89
|
def request_pipeline
|
84
|
-
return_if_present(@elasticsearch_response.dig(:pipeline, :name), @connectors_meta.dig(:pipeline, :default_name), DEFAULT_REQUEST_PIPELINE)
|
90
|
+
Utility::Common.return_if_present(@elasticsearch_response.dig(:pipeline, :name), @connectors_meta.dig(:pipeline, :default_name), DEFAULT_REQUEST_PIPELINE)
|
85
91
|
end
|
86
92
|
|
87
93
|
def extract_binary_content?
|
88
|
-
return_if_present(@elasticsearch_response.dig(:pipeline, :extract_binary_content), @connectors_meta.dig(:pipeline, :default_extract_binary_content), DEFAULT_EXTRACT_BINARY_CONTENT)
|
94
|
+
Utility::Common.return_if_present(@elasticsearch_response.dig(:pipeline, :extract_binary_content), @connectors_meta.dig(:pipeline, :default_extract_binary_content), DEFAULT_EXTRACT_BINARY_CONTENT)
|
89
95
|
end
|
90
96
|
|
91
97
|
def reduce_whitespace?
|
92
|
-
return_if_present(@elasticsearch_response.dig(:pipeline, :reduce_whitespace), @connectors_meta.dig(:pipeline, :default_reduce_whitespace), DEFAULT_REDUCE_WHITESPACE)
|
98
|
+
Utility::Common.return_if_present(@elasticsearch_response.dig(:pipeline, :reduce_whitespace), @connectors_meta.dig(:pipeline, :default_reduce_whitespace), DEFAULT_REDUCE_WHITESPACE)
|
93
99
|
end
|
94
100
|
|
95
101
|
def run_ml_inference?
|
96
|
-
return_if_present(@elasticsearch_response.dig(:pipeline, :run_ml_inference), @connectors_meta.dig(:pipeline, :default_run_ml_inference), DEFAULT_RUN_ML_INFERENCE)
|
102
|
+
Utility::Common.return_if_present(@elasticsearch_response.dig(:pipeline, :run_ml_inference), @connectors_meta.dig(:pipeline, :default_run_ml_inference), DEFAULT_RUN_ML_INFERENCE)
|
97
103
|
end
|
98
104
|
|
99
105
|
def formatted
|
@@ -110,8 +116,6 @@ module Core
|
|
110
116
|
index_name&.start_with?(Utility::Constants::CONTENT_INDEX_PREFIX)
|
111
117
|
end
|
112
118
|
|
113
|
-
private
|
114
|
-
|
115
119
|
def self.fetch_connectors_by_query(query, page_size)
|
116
120
|
connectors_meta = ElasticConnectorActions.connectors_meta
|
117
121
|
|
@@ -120,8 +124,8 @@ module Core
|
|
120
124
|
loop do
|
121
125
|
response = ElasticConnectorActions.search_connectors(query, page_size, offset)
|
122
126
|
|
123
|
-
hits = response
|
124
|
-
total = response
|
127
|
+
hits = response.dig('hits', 'hits') || []
|
128
|
+
total = response.dig('hits', 'total', 'value') || 0
|
125
129
|
results += hits.map do |hit|
|
126
130
|
Core::ConnectorSettings.new(hit, connectors_meta)
|
127
131
|
end
|
@@ -132,11 +136,5 @@ module Core
|
|
132
136
|
results
|
133
137
|
end
|
134
138
|
|
135
|
-
def return_if_present(*args)
|
136
|
-
args.each do |arg|
|
137
|
-
return arg unless arg.nil?
|
138
|
-
end
|
139
|
-
nil
|
140
|
-
end
|
141
139
|
end
|
142
140
|
end
|
@@ -10,8 +10,21 @@ require 'active_support/core_ext/hash'
|
|
10
10
|
require 'connectors/connector_status'
|
11
11
|
require 'connectors/sync_status'
|
12
12
|
require 'utility'
|
13
|
+
require 'elastic-transport'
|
13
14
|
|
14
15
|
module Core
|
16
|
+
class JobAlreadyRunningError < StandardError
|
17
|
+
def initialize(connector_id)
|
18
|
+
super("Sync job for connector '#{connector_id}' is already running.")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class ConnectorVersionChangedError < StandardError
|
23
|
+
def initialize(connector_id, seq_no, primary_term)
|
24
|
+
super("Version conflict: seq_no [#{seq_no}] and primary_term [#{primary_term}] do not match for connector '#{connector_id}'.")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
15
28
|
class ElasticConnectorActions
|
16
29
|
class << self
|
17
30
|
|
@@ -72,20 +85,53 @@ module Core
|
|
72
85
|
end
|
73
86
|
|
74
87
|
def claim_job(connector_id)
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
88
|
+
seq_no = nil
|
89
|
+
primary_term = nil
|
90
|
+
sync_in_progress = false
|
91
|
+
connector_record = client.get(
|
92
|
+
:index => Utility::Constants::CONNECTORS_INDEX,
|
93
|
+
:id => connector_id,
|
94
|
+
:ignore => 404,
|
95
|
+
:refresh => true
|
96
|
+
).tap do |response|
|
97
|
+
seq_no = response['_seq_no']
|
98
|
+
primary_term = response['_primary_term']
|
99
|
+
sync_in_progress = response.dig('_source', 'last_sync_status') == Connectors::SyncStatus::IN_PROGRESS
|
100
|
+
end
|
101
|
+
if sync_in_progress
|
102
|
+
raise JobAlreadyRunningError.new(connector_id)
|
103
|
+
end
|
104
|
+
update_connector_fields(
|
105
|
+
connector_id,
|
106
|
+
{ :sync_now => false,
|
107
|
+
:last_sync_status => Connectors::SyncStatus::IN_PROGRESS,
|
108
|
+
:last_synced => Time.now },
|
109
|
+
seq_no,
|
110
|
+
primary_term
|
111
|
+
)
|
79
112
|
|
80
113
|
body = {
|
81
114
|
:connector_id => connector_id,
|
82
115
|
:status => Connectors::SyncStatus::IN_PROGRESS,
|
83
116
|
:worker_hostname => Socket.gethostname,
|
84
|
-
:created_at => Time.now
|
117
|
+
:created_at => Time.now,
|
118
|
+
:filtering => convert_connector_filtering_to_job_filtering(connector_record.dig('_source', 'filtering'))
|
85
119
|
}
|
86
|
-
job = client.index(:index => Utility::Constants::JOB_INDEX, :body => body)
|
87
120
|
|
88
|
-
|
121
|
+
client.index(:index => Utility::Constants::JOB_INDEX, :body => body)
|
122
|
+
end
|
123
|
+
|
124
|
+
def convert_connector_filtering_to_job_filtering(connector_filtering)
|
125
|
+
return [] unless connector_filtering
|
126
|
+
connector_filtering = [connector_filtering] unless connector_filtering.is_a?(Array)
|
127
|
+
connector_filtering.each_with_object([]) do |filtering_domain, job_filtering|
|
128
|
+
job_filtering << {
|
129
|
+
'domain' => filtering_domain['domain'],
|
130
|
+
'rules' => filtering_domain.dig('active', 'rules'),
|
131
|
+
'advanced_snippet' => filtering_domain.dig('active', 'advanced_snippet'),
|
132
|
+
'warnings' => [] # TODO: in https://github.com/elastic/enterprise-search-team/issues/3174
|
133
|
+
}
|
134
|
+
end
|
89
135
|
end
|
90
136
|
|
91
137
|
def update_connector_status(connector_id, status, error_message = nil)
|
@@ -100,7 +146,7 @@ module Core
|
|
100
146
|
end
|
101
147
|
|
102
148
|
def complete_sync(connector_id, job_id, status)
|
103
|
-
sync_status = status[:error] ? Connectors::SyncStatus::
|
149
|
+
sync_status = status[:error] ? Connectors::SyncStatus::ERROR : Connectors::SyncStatus::COMPLETED
|
104
150
|
|
105
151
|
update_connector_fields(connector_id,
|
106
152
|
:last_sync_status => sync_status,
|
@@ -136,7 +182,7 @@ module Core
|
|
136
182
|
}
|
137
183
|
loop do
|
138
184
|
response = client.search(:body => body)
|
139
|
-
hits = response
|
185
|
+
hits = response.dig('hits', 'hits') || []
|
140
186
|
|
141
187
|
ids = hits.map { |h| h['_id'] }
|
142
188
|
result += ids
|
@@ -242,15 +288,29 @@ module Core
|
|
242
288
|
ensure_index_exists("#{Utility::Constants::JOB_INDEX}-v1", system_index_body(:alias_name => Utility::Constants::JOB_INDEX, :mappings => mappings))
|
243
289
|
end
|
244
290
|
|
245
|
-
def update_connector_fields(connector_id, doc = {})
|
291
|
+
def update_connector_fields(connector_id, doc = {}, seq_no = nil, primary_term = nil)
|
246
292
|
return if doc.empty?
|
247
|
-
|
293
|
+
update_args = {
|
248
294
|
:index => Utility::Constants::CONNECTORS_INDEX,
|
249
295
|
:id => connector_id,
|
250
296
|
:body => { :doc => doc },
|
251
297
|
:refresh => true,
|
252
298
|
:retry_on_conflict => 3
|
253
|
-
|
299
|
+
}
|
300
|
+
# seq_no and primary_term are used for optimistic concurrency control
|
301
|
+
# see https://www.elastic.co/guide/en/elasticsearch/reference/current/optimistic-concurrency-control.html
|
302
|
+
if seq_no && primary_term
|
303
|
+
update_args[:if_seq_no] = seq_no
|
304
|
+
update_args[:if_primary_term] = primary_term
|
305
|
+
update_args.delete(:retry_on_conflict)
|
306
|
+
end
|
307
|
+
begin
|
308
|
+
client.update(update_args)
|
309
|
+
rescue Elastic::Transport::Transport::Errors::Conflict
|
310
|
+
# VersionConflictException
|
311
|
+
# see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#optimistic-concurrency-control-index
|
312
|
+
raise ConnectorVersionChangedError.new(connector_id, seq_no, primary_term)
|
313
|
+
end
|
254
314
|
end
|
255
315
|
|
256
316
|
private
|
@@ -16,6 +16,9 @@ module Core
|
|
16
16
|
class NativeScheduler < Core::Scheduler
|
17
17
|
def connector_settings
|
18
18
|
Core::ConnectorSettings.fetch_native_connectors || []
|
19
|
+
rescue *Utility::AUTHORIZATION_ERRORS => e
|
20
|
+
# should be handled by the general scheduler
|
21
|
+
raise e
|
19
22
|
rescue StandardError => e
|
20
23
|
Utility::ExceptionTracking.log_exception(e, 'Could not retrieve native connectors due to unexpected error.')
|
21
24
|
[]
|
data/lib/core/scheduler.rb
CHANGED
@@ -9,6 +9,7 @@
|
|
9
9
|
require 'time'
|
10
10
|
require 'fugit'
|
11
11
|
require 'core/connector_settings'
|
12
|
+
require 'core/elastic_connector_actions'
|
12
13
|
require 'utility/cron'
|
13
14
|
require 'utility/logger'
|
14
15
|
require 'utility/exception_tracking'
|
@@ -41,6 +42,8 @@ module Core
|
|
41
42
|
if @is_shutting_down
|
42
43
|
break
|
43
44
|
end
|
45
|
+
rescue *Utility::AUTHORIZATION_ERRORS => e
|
46
|
+
Utility::ExceptionTracking.log_exception(e, 'Could not retrieve connectors settings due to authorization error.')
|
44
47
|
rescue StandardError => e
|
45
48
|
Utility::ExceptionTracking.log_exception(e, 'Sync failed due to unexpected error.')
|
46
49
|
ensure
|
@@ -21,6 +21,9 @@ module Core
|
|
21
21
|
def connector_settings
|
22
22
|
connector_settings = Core::ConnectorSettings.fetch_by_id(@connector_id)
|
23
23
|
[connector_settings]
|
24
|
+
rescue *Utility::AUTHORIZATION_ERRORS => e
|
25
|
+
# should be handled by the general scheduler
|
26
|
+
raise e
|
24
27
|
rescue StandardError => e
|
25
28
|
Utility::ExceptionTracking.log_exception(e, "Could not retrieve the connector by id #{@connector_id} due to unexpected error.")
|
26
29
|
[]
|
data/lib/core/sync_job_runner.rb
CHANGED
@@ -23,7 +23,7 @@ module Core
|
|
23
23
|
@connector_settings = connector_settings
|
24
24
|
@sink = Core::OutputSink::EsSink.new(connector_settings.index_name, @connector_settings.request_pipeline)
|
25
25
|
@connector_class = Connectors::REGISTRY.connector_class(connector_settings.service_type)
|
26
|
-
@
|
26
|
+
@sync_finished = false
|
27
27
|
@status = {
|
28
28
|
:indexed_document_count => 0,
|
29
29
|
:deleted_document_count => 0,
|
@@ -39,9 +39,10 @@ module Core
|
|
39
39
|
private
|
40
40
|
|
41
41
|
def do_sync!
|
42
|
-
Utility::Logger.info("
|
42
|
+
Utility::Logger.info("Claiming a sync job for connector #{@connector_settings.id}.")
|
43
43
|
|
44
|
-
|
44
|
+
job_description = ElasticConnectorActions.claim_job(@connector_settings.id)
|
45
|
+
job_id = job_description['_id']
|
45
46
|
|
46
47
|
unless job_id.present?
|
47
48
|
Utility::Logger.error("Failed to claim the job for #{@connector_settings.id}. Please check the logs for the cause of this error.")
|
@@ -51,17 +52,19 @@ module Core
|
|
51
52
|
begin
|
52
53
|
Utility::Logger.debug("Successfully claimed job for connector #{@connector_settings.id}.")
|
53
54
|
|
54
|
-
|
55
|
+
connector_instance = Connectors::REGISTRY.connector(@connector_settings.service_type, @connector_settings.configuration, job_description: job_description)
|
56
|
+
|
57
|
+
connector_instance.do_health_check!
|
55
58
|
|
56
59
|
incoming_ids = []
|
57
60
|
existing_ids = ElasticConnectorActions.fetch_document_ids(@connector_settings.index_name)
|
58
61
|
|
59
62
|
Utility::Logger.debug("#{existing_ids.size} documents are present in index #{@connector_settings.index_name}.")
|
60
63
|
|
61
|
-
|
64
|
+
connector_instance.yield_documents do |document|
|
62
65
|
document = add_ingest_metadata(document)
|
63
66
|
@sink.ingest(document)
|
64
|
-
incoming_ids << document[
|
67
|
+
incoming_ids << document['id']
|
65
68
|
@status[:indexed_document_count] += 1
|
66
69
|
end
|
67
70
|
|
@@ -75,6 +78,10 @@ module Core
|
|
75
78
|
end
|
76
79
|
|
77
80
|
@sink.flush
|
81
|
+
|
82
|
+
# We use this mechanism for checking, whether an interrupt (or something else lead to the thread not finishing)
|
83
|
+
# occurred as most of the time the main execution thread is interrupted and we miss this Signal/Exception here
|
84
|
+
@sync_finished = true
|
78
85
|
rescue StandardError => e
|
79
86
|
@status[:error] = e.message
|
80
87
|
Utility::ExceptionTracking.log_exception(e)
|
@@ -83,10 +90,15 @@ module Core
|
|
83
90
|
Utility::Logger.info("Upserted #{@status[:indexed_document_count]} documents into #{@connector_settings.index_name}.")
|
84
91
|
Utility::Logger.info("Deleted #{@status[:deleted_document_count]} documents into #{@connector_settings.index_name}.")
|
85
92
|
|
93
|
+
# Make sure to not override a previous error message
|
94
|
+
if !@sync_finished && @status[:error].nil?
|
95
|
+
@status[:error] = 'Sync thread didn\'t finish execution. Check connector logs for more details.'
|
96
|
+
end
|
97
|
+
|
86
98
|
ElasticConnectorActions.complete_sync(@connector_settings.id, job_id, @status.dup)
|
87
99
|
|
88
100
|
if @status[:error]
|
89
|
-
Utility::Logger.info("Failed to sync for connector #{@connector_settings.id} with error #{@status[:error]}.")
|
101
|
+
Utility::Logger.info("Failed to sync for connector #{@connector_settings.id} with error '#{@status[:error]}'.")
|
90
102
|
else
|
91
103
|
Utility::Logger.info("Successfully synced for connector #{@connector_settings.id}.")
|
92
104
|
end
|
@@ -4,10 +4,17 @@
|
|
4
4
|
# you may not use this file except in compliance with the Elastic License.
|
5
5
|
#
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
module Utility
|
10
|
+
class Common
|
11
|
+
class << self
|
12
|
+
def return_if_present(*args)
|
13
|
+
args.each do |arg|
|
14
|
+
return arg unless arg.nil?
|
15
|
+
end
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
end
|
12
19
|
end
|
13
20
|
end
|
data/lib/utility/errors.rb
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
#
|
6
6
|
|
7
7
|
require 'active_support/core_ext/string'
|
8
|
+
require 'elasticsearch'
|
8
9
|
|
9
10
|
module Utility
|
10
11
|
class DocumentError
|
@@ -31,6 +32,8 @@ module Utility
|
|
31
32
|
end
|
32
33
|
|
33
34
|
class ClientError < StandardError; end
|
35
|
+
|
36
|
+
class InvalidFilterConfigError < StandardError; end
|
34
37
|
class EvictionWithNoProgressError < StandardError; end
|
35
38
|
class EvictionError < StandardError
|
36
39
|
attr_accessor :cursors
|
@@ -89,6 +92,7 @@ module Utility
|
|
89
92
|
class InvalidTokenError < StandardError; end
|
90
93
|
class TokenRefreshFailedError < StandardError; end
|
91
94
|
class ConnectorNotAvailableError < StandardError; end
|
95
|
+
class AuthorizationError < StandardError; end
|
92
96
|
|
93
97
|
# For when we want to explicitly set a #cause but can't
|
94
98
|
class ExplicitlyCausedError < StandardError
|
@@ -124,6 +128,7 @@ module Utility
|
|
124
128
|
end
|
125
129
|
end
|
126
130
|
|
131
|
+
AUTHORIZATION_ERRORS = [Elastic::Transport::Transport::Errors::Unauthorized]
|
127
132
|
INTERNAL_SERVER_ERROR = Utility::Error.new(500, 'INTERNAL_SERVER_ERROR', 'Internal server error')
|
128
133
|
INVALID_API_KEY = Utility::Error.new(401, 'INVALID_API_KEY', 'Invalid API key')
|
129
134
|
UNSUPPORTED_AUTH_SCHEME = Utility::Error.new(401, 'UNSUPPORTED_AUTH_SCHEME', 'Unsupported authorization scheme')
|
data/lib/utility/es_client.rb
CHANGED
@@ -20,8 +20,8 @@ module Utility
|
|
20
20
|
attr_reader :cause
|
21
21
|
end
|
22
22
|
|
23
|
-
def initialize(es_config)
|
24
|
-
super(connection_configs(es_config))
|
23
|
+
def initialize(es_config, &block)
|
24
|
+
super(connection_configs(es_config), &block)
|
25
25
|
end
|
26
26
|
|
27
27
|
def connection_configs(es_config)
|
@@ -39,6 +39,10 @@ module Utility
|
|
39
39
|
configs[:log] = es_config[:log] || false
|
40
40
|
configs[:trace] = es_config[:trace] || false
|
41
41
|
|
42
|
+
# transport options
|
43
|
+
configs[:transport_options] = es_config[:transport_options] if es_config[:transport_options]
|
44
|
+
configs[:ca_fingerprint] = es_config[:ca_fingerprint] if es_config[:ca_fingerprint]
|
45
|
+
|
42
46
|
# if log or trace is activated, we use the application logger
|
43
47
|
configs[:logger] = if configs[:log] || configs[:trace]
|
44
48
|
Utility::Logger.logger
|
data/lib/utility.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: connectors_service
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 8.6.0.
|
4
|
+
version: 8.6.0.4.pre.20221104T200814Z
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Elastic
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-11-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -198,14 +198,14 @@ dependencies:
|
|
198
198
|
requirements:
|
199
199
|
- - "~>"
|
200
200
|
- !ruby/object:Gem::Version
|
201
|
-
version: 8.
|
201
|
+
version: 8.5.0
|
202
202
|
type: :runtime
|
203
203
|
prerelease: false
|
204
204
|
version_requirements: !ruby/object:Gem::Requirement
|
205
205
|
requirements:
|
206
206
|
- - "~>"
|
207
207
|
- !ruby/object:Gem::Version
|
208
|
-
version: 8.
|
208
|
+
version: 8.5.0
|
209
209
|
- !ruby/object:Gem::Dependency
|
210
210
|
name: faraday
|
211
211
|
requirement: !ruby/object:Gem::Requirement
|
@@ -400,10 +400,10 @@ files:
|
|
400
400
|
- lib/connectors/base/custom_client.rb
|
401
401
|
- lib/connectors/connector_status.rb
|
402
402
|
- lib/connectors/crawler/scheduler.rb
|
403
|
+
- lib/connectors/example/attachments/first_attachment.txt
|
404
|
+
- lib/connectors/example/attachments/second_attachment.txt
|
405
|
+
- lib/connectors/example/attachments/third_attachment.txt
|
403
406
|
- lib/connectors/example/connector.rb
|
404
|
-
- lib/connectors/example/example_attachments/first_attachment.txt
|
405
|
-
- lib/connectors/example/example_attachments/second_attachment.txt
|
406
|
-
- lib/connectors/example/example_attachments/third_attachment.txt
|
407
407
|
- lib/connectors/gitlab/adapter.rb
|
408
408
|
- lib/connectors/gitlab/connector.rb
|
409
409
|
- lib/connectors/gitlab/custom_client.rb
|
@@ -411,7 +411,6 @@ files:
|
|
411
411
|
- lib/connectors/mongodb/connector.rb
|
412
412
|
- lib/connectors/registry.rb
|
413
413
|
- lib/connectors/sync_status.rb
|
414
|
-
- lib/connectors_app/\
|
415
414
|
- lib/connectors_service.rb
|
416
415
|
- lib/connectors_utility.rb
|
417
416
|
- lib/core.rb
|
@@ -433,6 +432,7 @@ files:
|
|
433
432
|
- lib/stubs/connectors/stats.rb
|
434
433
|
- lib/stubs/service_type.rb
|
435
434
|
- lib/utility.rb
|
435
|
+
- lib/utility/common.rb
|
436
436
|
- lib/utility/constants.rb
|
437
437
|
- lib/utility/cron.rb
|
438
438
|
- lib/utility/elasticsearch/index/language_data.yml
|
@@ -451,7 +451,7 @@ homepage: https://github.com/elastic/connectors-ruby
|
|
451
451
|
licenses:
|
452
452
|
- Elastic-2.0
|
453
453
|
metadata: {}
|
454
|
-
post_install_message:
|
454
|
+
post_install_message:
|
455
455
|
rdoc_options: []
|
456
456
|
require_paths:
|
457
457
|
- lib
|
@@ -462,12 +462,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
462
462
|
version: '0'
|
463
463
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
464
464
|
requirements:
|
465
|
-
- - "
|
465
|
+
- - ">"
|
466
466
|
- !ruby/object:Gem::Version
|
467
|
-
version:
|
467
|
+
version: 1.3.1
|
468
468
|
requirements: []
|
469
469
|
rubygems_version: 3.0.3.1
|
470
|
-
signing_key:
|
470
|
+
signing_key:
|
471
471
|
specification_version: 4
|
472
472
|
summary: Gem containing Elastic connectors service
|
473
473
|
test_files: []
|