connectors_service 8.5.0.1
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 +7 -0
- data/LICENSE +93 -0
- data/NOTICE.txt +2 -0
- data/bin/connectors_service +4 -0
- data/bin/list_connectors +4 -0
- data/config/connectors.yml +25 -0
- data/lib/app/app.rb +25 -0
- data/lib/app/config.rb +132 -0
- data/lib/app/console_app.rb +278 -0
- data/lib/app/dispatcher.rb +121 -0
- data/lib/app/menu.rb +104 -0
- data/lib/app/preflight_check.rb +134 -0
- data/lib/app/version.rb +10 -0
- data/lib/connectors/base/adapter.rb +119 -0
- data/lib/connectors/base/connector.rb +57 -0
- data/lib/connectors/base/custom_client.rb +111 -0
- data/lib/connectors/connector_status.rb +31 -0
- data/lib/connectors/crawler/scheduler.rb +32 -0
- data/lib/connectors/example/connector.rb +57 -0
- data/lib/connectors/example/example_attachments/first_attachment.txt +1 -0
- data/lib/connectors/example/example_attachments/second_attachment.txt +1 -0
- data/lib/connectors/example/example_attachments/third_attachment.txt +1 -0
- data/lib/connectors/gitlab/adapter.rb +50 -0
- data/lib/connectors/gitlab/connector.rb +67 -0
- data/lib/connectors/gitlab/custom_client.rb +44 -0
- data/lib/connectors/gitlab/extractor.rb +69 -0
- data/lib/connectors/mongodb/connector.rb +138 -0
- data/lib/connectors/registry.rb +52 -0
- data/lib/connectors/sync_status.rb +21 -0
- data/lib/connectors.rb +16 -0
- data/lib/connectors_app/// +13 -0
- data/lib/connectors_service.rb +24 -0
- data/lib/connectors_utility.rb +16 -0
- data/lib/core/configuration.rb +48 -0
- data/lib/core/connector_settings.rb +142 -0
- data/lib/core/elastic_connector_actions.rb +269 -0
- data/lib/core/heartbeat.rb +32 -0
- data/lib/core/native_scheduler.rb +24 -0
- data/lib/core/output_sink/base_sink.rb +33 -0
- data/lib/core/output_sink/combined_sink.rb +38 -0
- data/lib/core/output_sink/console_sink.rb +51 -0
- data/lib/core/output_sink/es_sink.rb +74 -0
- data/lib/core/output_sink.rb +13 -0
- data/lib/core/scheduler.rb +158 -0
- data/lib/core/single_scheduler.rb +29 -0
- data/lib/core/sync_job_runner.rb +111 -0
- data/lib/core.rb +16 -0
- data/lib/list_connectors.rb +22 -0
- data/lib/stubs/app_config.rb +35 -0
- data/lib/stubs/connectors/stats.rb +35 -0
- data/lib/stubs/service_type.rb +13 -0
- data/lib/utility/constants.rb +20 -0
- data/lib/utility/cron.rb +81 -0
- data/lib/utility/elasticsearch/index/language_data.yml +111 -0
- data/lib/utility/elasticsearch/index/mappings.rb +104 -0
- data/lib/utility/elasticsearch/index/text_analysis_settings.rb +226 -0
- data/lib/utility/environment.rb +33 -0
- data/lib/utility/errors.rb +132 -0
- data/lib/utility/es_client.rb +84 -0
- data/lib/utility/exception_tracking.rb +64 -0
- data/lib/utility/extension_mapping_util.rb +123 -0
- data/lib/utility/logger.rb +84 -0
- data/lib/utility/middleware/basic_auth.rb +27 -0
- data/lib/utility/middleware/bearer_auth.rb +27 -0
- data/lib/utility/middleware/restrict_hostnames.rb +73 -0
- data/lib/utility.rb +16 -0
- metadata +487 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
#
|
2
|
+
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
3
|
+
# or more contributor license agreements. Licensed under the Elastic License;
|
4
|
+
# you may not use this file except in compliance with the Elastic License.
|
5
|
+
#
|
6
|
+
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require 'active_support'
|
10
|
+
require 'concurrent'
|
11
|
+
require 'connectors'
|
12
|
+
require 'core'
|
13
|
+
require 'utility'
|
14
|
+
require 'app/config'
|
15
|
+
|
16
|
+
module App
|
17
|
+
class Dispatcher
|
18
|
+
POLL_INTERVAL = (App::Config.poll_interval || 3).to_i
|
19
|
+
TERMINATION_TIMEOUT = (App::Config.termination_timeout || 60).to_i
|
20
|
+
HEARTBEAT_INTERVAL = (App::Config.heartbeat_interval || 60 * 30).to_i
|
21
|
+
MIN_THREADS = (App::Config.dig(:thread_pool, :min_threads) || 0).to_i
|
22
|
+
MAX_THREADS = (App::Config.dig(:thread_pool, :max_threads) || 5).to_i
|
23
|
+
MAX_QUEUE = (App::Config.dig(:thread_pool, :max_queue) || 100).to_i
|
24
|
+
|
25
|
+
@running = Concurrent::AtomicBoolean.new(false)
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def start!
|
29
|
+
running!
|
30
|
+
Utility::Logger.info("Starting connector service in #{App::Config.native_mode ? 'native' : 'non-native'} mode...")
|
31
|
+
start_polling_jobs!
|
32
|
+
end
|
33
|
+
|
34
|
+
def shutdown!
|
35
|
+
Utility::Logger.info("Shutting down connector service with pool [#{pool.class}]...")
|
36
|
+
running.make_false
|
37
|
+
scheduler.shutdown
|
38
|
+
pool.shutdown
|
39
|
+
pool.wait_for_termination(TERMINATION_TIMEOUT)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :running
|
45
|
+
|
46
|
+
def running!
|
47
|
+
raise 'connector service is already running!' unless running.make_true
|
48
|
+
end
|
49
|
+
|
50
|
+
def pool
|
51
|
+
@pool ||= Concurrent::ThreadPoolExecutor.new(
|
52
|
+
min_threads: MIN_THREADS,
|
53
|
+
max_threads: MAX_THREADS,
|
54
|
+
max_queue: MAX_QUEUE,
|
55
|
+
fallback_policy: :abort
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def scheduler
|
60
|
+
@scheduler ||= if App::Config.native_mode
|
61
|
+
Core::NativeScheduler.new(POLL_INTERVAL, HEARTBEAT_INTERVAL)
|
62
|
+
else
|
63
|
+
Core::SingleScheduler.new(App::Config.connector_id, POLL_INTERVAL, HEARTBEAT_INTERVAL)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def start_polling_jobs!
|
68
|
+
scheduler.when_triggered do |connector_settings, task|
|
69
|
+
case task
|
70
|
+
when :sync
|
71
|
+
start_sync_task(connector_settings)
|
72
|
+
when :heartbeat
|
73
|
+
start_heartbeat_task(connector_settings)
|
74
|
+
when :configuration
|
75
|
+
start_configuration_task(connector_settings)
|
76
|
+
else
|
77
|
+
Utility::Logger.error("Unknown task type: #{task}. Skipping...")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
rescue StandardError => e
|
81
|
+
Utility::ExceptionTracking.log_exception(e, 'The connector service failed due to unexpected error.')
|
82
|
+
end
|
83
|
+
|
84
|
+
def start_sync_task(connector_settings)
|
85
|
+
start_heartbeat_task(connector_settings)
|
86
|
+
pool.post do
|
87
|
+
Utility::Logger.info("Starting a sync job for #{connector_settings.formatted}...")
|
88
|
+
Core::ElasticConnectorActions.ensure_content_index_exists(connector_settings.index_name)
|
89
|
+
job_runner = Core::SyncJobRunner.new(connector_settings)
|
90
|
+
job_runner.execute
|
91
|
+
rescue StandardError => e
|
92
|
+
Utility::ExceptionTracking.log_exception(e, "Sync job for #{connector_settings.formatted} failed due to unexpected error.")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def start_heartbeat_task(connector_settings)
|
97
|
+
pool.post do
|
98
|
+
Utility::Logger.info("Sending heartbeat for #{connector_settings.formatted}...")
|
99
|
+
Core::Heartbeat.send(connector_settings)
|
100
|
+
rescue StandardError => e
|
101
|
+
Utility::ExceptionTracking.log_exception(e, "Heartbeat task for #{connector_settings.formatted} failed due to unexpected error.")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def start_configuration_task(connector_settings)
|
106
|
+
pool.post do
|
107
|
+
Utility::Logger.info("Updating configuration for #{connector_settings.formatted}...")
|
108
|
+
# when in non-native mode, populate the service type if it's not in connector settings
|
109
|
+
service_type = if !App::Config.native_mode && connector_settings.needs_service_type?
|
110
|
+
App::Config.service_type
|
111
|
+
else
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
Core::Configuration.update(connector_settings, service_type)
|
115
|
+
rescue StandardError => e
|
116
|
+
Utility::ExceptionTracking.log_exception(e, "Configuration task for #{connector_settings.formatted} failed due to unexpected error.")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/app/menu.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
#
|
2
|
+
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
3
|
+
# or more contributor license agreements. Licensed under the Elastic License;
|
4
|
+
# you may not use this file except in compliance with the Elastic License.
|
5
|
+
#
|
6
|
+
# frozen_string_literal: true
|
7
|
+
#
|
8
|
+
require 'remedy'
|
9
|
+
|
10
|
+
module App
|
11
|
+
class Menu
|
12
|
+
attr_reader :items
|
13
|
+
attr_reader :title
|
14
|
+
attr_reader :index
|
15
|
+
|
16
|
+
def initialize(title, items)
|
17
|
+
super()
|
18
|
+
@index = 0
|
19
|
+
@title = title
|
20
|
+
@items = items.map.with_index do |item, i|
|
21
|
+
item.is_a?(String) ? MenuItem.new(item, nil, i == 0) : MenuItem.new(item[:command], item[:hint], i == 0)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def select_item(index)
|
26
|
+
@index = index
|
27
|
+
@items.each_with_index { |item, i| item.selected = (i == index) }
|
28
|
+
display
|
29
|
+
end
|
30
|
+
|
31
|
+
def select_command
|
32
|
+
display
|
33
|
+
interaction = Remedy::Interaction.new
|
34
|
+
interaction.loop do |key|
|
35
|
+
if key.nil?
|
36
|
+
break
|
37
|
+
end
|
38
|
+
case key.to_s.to_sym
|
39
|
+
when :down
|
40
|
+
index = @index + 1
|
41
|
+
index = 0 if index >= @items.size
|
42
|
+
select_item(index)
|
43
|
+
when :up
|
44
|
+
index = @index - 1
|
45
|
+
index = 0 if index < 0
|
46
|
+
select_item(index)
|
47
|
+
when :control_m
|
48
|
+
return @items[@index].command
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def display
|
56
|
+
clear_screen
|
57
|
+
puts(title)
|
58
|
+
@items.each do |item|
|
59
|
+
print(item.selected ? '--> ' : ' ')
|
60
|
+
puts item.hint.present? ? "#{item.hint} (#{item.command})" : item.command
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def clear_screen
|
65
|
+
system('clear') || system('cls')
|
66
|
+
end
|
67
|
+
|
68
|
+
def read_char
|
69
|
+
STDIN.echo = false
|
70
|
+
STDIN.raw!
|
71
|
+
|
72
|
+
input = STDIN.getc
|
73
|
+
if input == "\e"
|
74
|
+
begin
|
75
|
+
input << STDIN.read_nonblock(3)
|
76
|
+
rescue StandardError
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
begin
|
80
|
+
input << STDIN.read_nonblock(2)
|
81
|
+
rescue StandardError
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
input
|
86
|
+
ensure
|
87
|
+
STDIN.echo = true
|
88
|
+
STDIN.cooked!
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class MenuItem
|
93
|
+
attr_reader :command
|
94
|
+
attr_reader :hint
|
95
|
+
attr_accessor :selected
|
96
|
+
|
97
|
+
def initialize(command, hint = nil, selected = false)
|
98
|
+
super()
|
99
|
+
@command = command
|
100
|
+
@hint = hint
|
101
|
+
@selected = selected
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
#
|
2
|
+
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
3
|
+
# or more contributor license agreements. Licensed under the Elastic License;
|
4
|
+
# you may not use this file except in compliance with the Elastic License.
|
5
|
+
#
|
6
|
+
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require 'app/version'
|
10
|
+
require 'utility'
|
11
|
+
require 'faraday'
|
12
|
+
|
13
|
+
module App
|
14
|
+
class PreflightCheck
|
15
|
+
class CheckFailure < StandardError; end
|
16
|
+
class UnhealthyCluster < StandardError; end
|
17
|
+
|
18
|
+
STARTUP_RETRY_INTERVAL = 5
|
19
|
+
STARTUP_RETRY_TIMEOUT = 600
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def run!
|
23
|
+
check_es_connection!
|
24
|
+
check_es_version!
|
25
|
+
check_system_indices!
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
#-------------------------------------------------------------------------------------------------
|
31
|
+
# Checks to make sure we can connect to Elasticsearch and make API requests to it
|
32
|
+
def check_es_connection!
|
33
|
+
check_es_connection_with_retries!(
|
34
|
+
:retry_interval => STARTUP_RETRY_INTERVAL,
|
35
|
+
:retry_timeout => STARTUP_RETRY_TIMEOUT
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
#-------------------------------------------------------------------------------------------------
|
40
|
+
# Ensures that the version of Elasticsearch is compatible with connector service
|
41
|
+
def check_es_version!
|
42
|
+
info = client.info
|
43
|
+
version = info.dig('version', 'number')
|
44
|
+
fail_check!("Cannot retrieve version from Elasticsearch response:\n#{info.to_json}") unless version
|
45
|
+
|
46
|
+
if match_es_version?(version)
|
47
|
+
Utility::Logger.info("Connector service version (#{App::VERSION}) matches Elasticsearch version (#{version}).")
|
48
|
+
else
|
49
|
+
fail_check!("Connector service (#{App::VERSION}) is required to run with the same major and minor version of Elasticsearch (#{version}).")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#-------------------------------------------------------------------------------------------------
|
54
|
+
# Ensures that the required system indices of connector service exist
|
55
|
+
def check_system_indices!
|
56
|
+
check_system_indices_with_retries!(
|
57
|
+
:retry_interval => STARTUP_RETRY_INTERVAL,
|
58
|
+
:retry_timeout => STARTUP_RETRY_TIMEOUT
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def check_es_connection_with_retries!(retry_interval:, retry_timeout:)
|
63
|
+
started_at = Time.now
|
64
|
+
|
65
|
+
begin
|
66
|
+
response = client.cluster.health
|
67
|
+
Utility::Logger.info('Successfully connected to Elasticsearch')
|
68
|
+
case response['status']
|
69
|
+
when 'green'
|
70
|
+
Utility::Logger.info('Elasticsearch is running and healthy.')
|
71
|
+
when 'yellow'
|
72
|
+
Utility::Logger.warn('Elasticsearch is running but the status is yellow.')
|
73
|
+
when 'red'
|
74
|
+
raise UnhealthyCluster, 'Elasticsearch is running but unhealthy.'
|
75
|
+
else
|
76
|
+
raise UnhealthyCluster, "Unexpected cluster status: #{response['status']}"
|
77
|
+
end
|
78
|
+
rescue *App::RETRYABLE_CONNECTION_ERRORS => e
|
79
|
+
Utility::Logger.warn('Could not connect to Elasticsearch. Make sure it is running and healthy.')
|
80
|
+
Utility::Logger.debug("Error: #{e.full_message}")
|
81
|
+
|
82
|
+
sleep(retry_interval)
|
83
|
+
time_elapsed = Time.now - started_at
|
84
|
+
retry if time_elapsed < retry_timeout
|
85
|
+
|
86
|
+
# If we ran out of time, there is not much we can do but shut down
|
87
|
+
fail_check!("Could not connect to Elasticsearch after #{time_elapsed.to_i} seconds. Terminating...")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def match_es_version?(es_version)
|
92
|
+
parse_minor_version(App::VERSION) == parse_minor_version(es_version)
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse_minor_version(version)
|
96
|
+
version.split('.').slice(0, 2).join('.')
|
97
|
+
end
|
98
|
+
|
99
|
+
def check_system_indices_with_retries!(retry_interval:, retry_timeout:)
|
100
|
+
started_at = Time.now
|
101
|
+
loop do
|
102
|
+
if client.indices.exists?(:index => Utility::Constants::CONNECTORS_INDEX) && client.indices.exists?(:index => Utility::Constants::JOB_INDEX)
|
103
|
+
Utility::Logger.info("Found system indices #{Utility::Constants::CONNECTORS_INDEX} and #{Utility::Constants::JOB_INDEX}.")
|
104
|
+
return
|
105
|
+
end
|
106
|
+
Utility::Logger.warn('Required system indices for connector service don\'t exist. Make sure to run Kibana first to create system indices.')
|
107
|
+
sleep(retry_interval)
|
108
|
+
time_elapsed = Time.now - started_at
|
109
|
+
if time_elapsed > retry_timeout
|
110
|
+
fail_check!("Could not find required system indices after #{time_elapsed.to_i} seconds. Terminating...")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def client
|
116
|
+
@client ||= Utility::EsClient.new(App::Config[:elasticsearch])
|
117
|
+
end
|
118
|
+
|
119
|
+
def fail_check!(message)
|
120
|
+
raise CheckFailure, message
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
RETRYABLE_CONNECTION_ERRORS = [
|
126
|
+
::Faraday::ConnectionFailed,
|
127
|
+
::Faraday::ClientError,
|
128
|
+
::Errno::ECONNREFUSED,
|
129
|
+
::SocketError,
|
130
|
+
::Errno::ECONNRESET,
|
131
|
+
App::PreflightCheck::UnhealthyCluster,
|
132
|
+
::HTTPClient::KeepAliveDisconnected
|
133
|
+
]
|
134
|
+
end
|
data/lib/app/version.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#
|
2
|
+
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
3
|
+
# or more contributor license agreements. Licensed under the Elastic License;
|
4
|
+
# you may not use this file except in compliance with the Elastic License.
|
5
|
+
#
|
6
|
+
require 'app/config'
|
7
|
+
|
8
|
+
module App
|
9
|
+
VERSION = App::Config[:version]
|
10
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
#
|
2
|
+
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
3
|
+
# or more contributor license agreements. Licensed under the Elastic License;
|
4
|
+
# you may not use this file except in compliance with the Elastic License.
|
5
|
+
#
|
6
|
+
|
7
|
+
require 'active_support/core_ext/array/wrap'
|
8
|
+
require 'active_support/core_ext/numeric/time'
|
9
|
+
require 'active_support/core_ext/object/deep_dup'
|
10
|
+
require 'active_support/core_ext/object/json'
|
11
|
+
require 'utility'
|
12
|
+
require 'utility/extension_mapping_util'
|
13
|
+
require 'date'
|
14
|
+
require 'json'
|
15
|
+
require 'mime-types'
|
16
|
+
|
17
|
+
module Connectors
|
18
|
+
module Base
|
19
|
+
class Adapter
|
20
|
+
def self.fields_to_preserve
|
21
|
+
@fields_to_preserve ||= ['body']
|
22
|
+
.concat(Utility::Constants::THUMBNAIL_FIELDS)
|
23
|
+
.concat(Utility::Constants::SUBEXTRACTOR_RESERVED_FIELDS)
|
24
|
+
.map(&:freeze)
|
25
|
+
.freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.generate_id_helpers(method_prefix, id_prefix)
|
29
|
+
define_singleton_method("#{method_prefix}_id_to_es_id") do |id|
|
30
|
+
"#{id_prefix}_#{id}"
|
31
|
+
end
|
32
|
+
|
33
|
+
define_singleton_method("es_id_is_#{method_prefix}_id?") do |es_id|
|
34
|
+
regex_match = /#{id_prefix}_(.+)$/.match(es_id)
|
35
|
+
regex_match.present? && regex_match.size == 2
|
36
|
+
end
|
37
|
+
|
38
|
+
define_singleton_method("es_id_to_#{method_prefix}_id") do |es_id|
|
39
|
+
regex_match = /#{id_prefix}_(.+)$/.match(es_id)
|
40
|
+
|
41
|
+
raise ArgumentError, "Invalid id #{es_id} for source with method prefix #{method_prefix}." if regex_match.nil? || regex_match.length != 2
|
42
|
+
regex_match[1]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.mime_type_for_file(file_name)
|
47
|
+
ruby_detected_type = MIME::Types.type_for(file_name)
|
48
|
+
return ruby_detected_type.first.simplified if ruby_detected_type.present?
|
49
|
+
extension = extension_for_file(file_name)
|
50
|
+
Utility::ExtensionMappingUtil.get_mime_types(extension)&.first
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.extension_for_file(file_name)
|
54
|
+
File.extname(file_name.downcase).delete_prefix!('.')
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.strip_file_extension(file_name)
|
58
|
+
File.basename(file_name, File.extname(file_name))
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.normalize_enum(enum)
|
62
|
+
enum&.to_s&.downcase
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.normalize_date(date)
|
66
|
+
return nil if date.blank?
|
67
|
+
|
68
|
+
case date
|
69
|
+
when Date, Time, DateTime, ActiveSupport::TimeWithZone
|
70
|
+
date.to_datetime.rfc3339
|
71
|
+
else
|
72
|
+
begin
|
73
|
+
Time.zone.parse(date).to_datetime.rfc3339
|
74
|
+
rescue ArgumentError, TypeError => e
|
75
|
+
Utility::ExceptionTracking.capture_exception(e)
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.normalize_path(path)
|
82
|
+
return nil if path.blank?
|
83
|
+
return path if path.start_with?('/')
|
84
|
+
"/#{path}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.url_to_path(url)
|
88
|
+
return nil if url.blank?
|
89
|
+
uri = URI(url)
|
90
|
+
return nil if uri.scheme.blank?
|
91
|
+
normalize_path(uri.path)
|
92
|
+
rescue URI::InvalidURIError, ArgumentError
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.es_document_from_configured_object_base(object_type:, object:, fields:)
|
97
|
+
object_as_json = object.as_json
|
98
|
+
|
99
|
+
adapted_object = {
|
100
|
+
:type => normalize_enum(object_type)
|
101
|
+
}
|
102
|
+
|
103
|
+
fields.each do |field_data|
|
104
|
+
remote_field_name = field_data.fetch(:remote)
|
105
|
+
|
106
|
+
value = object_as_json[remote_field_name]
|
107
|
+
value = object_as_json.dig(*remote_field_name.split('.')) if value.blank?
|
108
|
+
next if value.nil?
|
109
|
+
|
110
|
+
adapted_object[field_data.fetch(:target)] = value
|
111
|
+
end
|
112
|
+
|
113
|
+
adapted_object.symbolize_keys
|
114
|
+
end
|
115
|
+
|
116
|
+
delegate :normalize_enum, :normalize_date, :normalize_path, :to => :class
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#
|
2
|
+
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
3
|
+
# or more contributor license agreements. Licensed under the Elastic License;
|
4
|
+
# you may not use this file except in compliance with the Elastic License.
|
5
|
+
#
|
6
|
+
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require 'bson'
|
10
|
+
require 'core/output_sink'
|
11
|
+
require 'utility/exception_tracking'
|
12
|
+
require 'utility/errors'
|
13
|
+
require 'app/config'
|
14
|
+
|
15
|
+
module Connectors
|
16
|
+
module Base
|
17
|
+
class Connector
|
18
|
+
def self.display_name
|
19
|
+
raise 'Not implemented for this connector'
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.configurable_fields
|
23
|
+
{}
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.service_type
|
27
|
+
raise 'Not implemented for this connector'
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(configuration: {})
|
31
|
+
@configuration = configuration.dup || {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def yield_documents; end
|
35
|
+
|
36
|
+
def do_health_check
|
37
|
+
raise 'Not implemented for this connector'
|
38
|
+
end
|
39
|
+
|
40
|
+
def do_health_check!
|
41
|
+
do_health_check
|
42
|
+
rescue StandardError => e
|
43
|
+
Utility::ExceptionTracking.log_exception(e, "Connector for service #{self.class.service_type} failed the health check for 3rd-party service.")
|
44
|
+
raise Utility::HealthCheckFailedError.new, e.message
|
45
|
+
end
|
46
|
+
|
47
|
+
def is_healthy?
|
48
|
+
do_health_check
|
49
|
+
|
50
|
+
true
|
51
|
+
rescue StandardError => e
|
52
|
+
Utility::ExceptionTracking.log_exception(e, "Connector for service #{self.class.service_type} failed the health check for 3rd-party service.")
|
53
|
+
false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
#
|
2
|
+
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
3
|
+
# or more contributor license agreements. Licensed under the Elastic License;
|
4
|
+
# you may not use this file except in compliance with the Elastic License.
|
5
|
+
#
|
6
|
+
|
7
|
+
require 'faraday'
|
8
|
+
require 'httpclient'
|
9
|
+
require 'active_support/core_ext/array/wrap'
|
10
|
+
require 'active_support/core_ext/numeric/time'
|
11
|
+
require 'active_support/core_ext/object/deep_dup'
|
12
|
+
require 'utility'
|
13
|
+
require 'date'
|
14
|
+
|
15
|
+
module Connectors
|
16
|
+
module Base
|
17
|
+
class CustomClient
|
18
|
+
attr_reader :base_url, :middleware, :ensure_fresh_auth
|
19
|
+
|
20
|
+
MAX_RETRIES = 5
|
21
|
+
|
22
|
+
def initialize(base_url: nil, ensure_fresh_auth: nil)
|
23
|
+
@base_url = base_url
|
24
|
+
@ensure_fresh_auth = ensure_fresh_auth
|
25
|
+
middleware!
|
26
|
+
end
|
27
|
+
|
28
|
+
def middleware!
|
29
|
+
@middleware = Array.wrap(additional_middleware)
|
30
|
+
@middleware += Array.wrap(default_middleware)
|
31
|
+
@middleware.compact!
|
32
|
+
end
|
33
|
+
|
34
|
+
def additional_middleware
|
35
|
+
[] # define as needed in subclass
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_middleware
|
39
|
+
[[Faraday::Request::Retry, retry_config]]
|
40
|
+
end
|
41
|
+
|
42
|
+
def retry_config
|
43
|
+
{
|
44
|
+
:retry_statuses => [429],
|
45
|
+
:backoff_factor => 2,
|
46
|
+
:max => MAX_RETRIES,
|
47
|
+
:interval => 0.05
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
[
|
52
|
+
:delete,
|
53
|
+
:get,
|
54
|
+
:head,
|
55
|
+
:options,
|
56
|
+
:patch,
|
57
|
+
:post,
|
58
|
+
:put,
|
59
|
+
].each do |http_verb|
|
60
|
+
define_method http_verb do |*args, &block|
|
61
|
+
ensure_fresh_auth.call(self) if ensure_fresh_auth.present?
|
62
|
+
http_client.public_send(http_verb, *args, &block)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def http_client!
|
67
|
+
@http_client = nil
|
68
|
+
http_client
|
69
|
+
end
|
70
|
+
|
71
|
+
def http_client
|
72
|
+
@http_client ||= Faraday.new(base_url) do |faraday|
|
73
|
+
middleware.each do |middleware_config|
|
74
|
+
faraday.use(*middleware_config)
|
75
|
+
end
|
76
|
+
|
77
|
+
faraday.adapter :httpclient
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# https://github.com/lostisland/faraday/blob/b09c6db31591dd1a58fffcc0979b0c7d96b5388b/lib/faraday/connection.rb#L171
|
84
|
+
METHODS_WITH_BODY = [:post, :put, :patch].freeze
|
85
|
+
|
86
|
+
def send_body?(method)
|
87
|
+
METHODS_WITH_BODY.include?(method.to_sym)
|
88
|
+
end
|
89
|
+
|
90
|
+
def request_with_throttling(method, url, options = {})
|
91
|
+
response =
|
92
|
+
if send_body?(method)
|
93
|
+
public_send(method, url, options[:body], options[:headers])
|
94
|
+
else
|
95
|
+
public_send(method, url, options[:params], options[:headers])
|
96
|
+
end
|
97
|
+
|
98
|
+
if response.status == 429
|
99
|
+
retry_after = response.headers['Retry-After']
|
100
|
+
multiplier = options.fetch(:retry_mulitplier, 1)
|
101
|
+
retry_after_secs = (retry_after.is_a?(Array) ? retry_after.first.to_i : retry_after.to_i) * multiplier
|
102
|
+
retry_after_secs = 60 if retry_after_secs <= 0
|
103
|
+
Utility::Logger.warn("Exceeded #{self.class} request limits. Going to sleep for #{retry_after_secs} seconds")
|
104
|
+
raise Utility::ThrottlingError.new(:suspend_until => DateTime.now + retry_after_secs.seconds, :cursors => options[:cursors])
|
105
|
+
else
|
106
|
+
response
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#
|
2
|
+
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
3
|
+
# or more contributor license agreements. Licensed under the Elastic License;
|
4
|
+
# you may not use this file except in compliance with the Elastic License.
|
5
|
+
#
|
6
|
+
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
module Connectors
|
10
|
+
class ConnectorStatus
|
11
|
+
CREATED = 'created'
|
12
|
+
NEEDS_CONFIGURATION = 'needs_configuration'
|
13
|
+
CONFIGURED = 'configured'
|
14
|
+
CONNECTED = 'connected'
|
15
|
+
ERROR = 'error'
|
16
|
+
|
17
|
+
STATUSES = [
|
18
|
+
CREATED,
|
19
|
+
NEEDS_CONFIGURATION,
|
20
|
+
CONFIGURED,
|
21
|
+
CONNECTED,
|
22
|
+
ERROR
|
23
|
+
]
|
24
|
+
|
25
|
+
STATUSES_ALLOWING_SYNC = [
|
26
|
+
CONFIGURED,
|
27
|
+
CONNECTED,
|
28
|
+
ERROR
|
29
|
+
]
|
30
|
+
end
|
31
|
+
end
|