connectors_service 8.5.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|