connectors_service 8.5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +93 -0
  3. data/NOTICE.txt +2 -0
  4. data/bin/connectors_service +4 -0
  5. data/bin/list_connectors +4 -0
  6. data/config/connectors.yml +25 -0
  7. data/lib/app/app.rb +25 -0
  8. data/lib/app/config.rb +132 -0
  9. data/lib/app/console_app.rb +278 -0
  10. data/lib/app/dispatcher.rb +121 -0
  11. data/lib/app/menu.rb +104 -0
  12. data/lib/app/preflight_check.rb +134 -0
  13. data/lib/app/version.rb +10 -0
  14. data/lib/connectors/base/adapter.rb +119 -0
  15. data/lib/connectors/base/connector.rb +57 -0
  16. data/lib/connectors/base/custom_client.rb +111 -0
  17. data/lib/connectors/connector_status.rb +31 -0
  18. data/lib/connectors/crawler/scheduler.rb +32 -0
  19. data/lib/connectors/example/connector.rb +57 -0
  20. data/lib/connectors/example/example_attachments/first_attachment.txt +1 -0
  21. data/lib/connectors/example/example_attachments/second_attachment.txt +1 -0
  22. data/lib/connectors/example/example_attachments/third_attachment.txt +1 -0
  23. data/lib/connectors/gitlab/adapter.rb +50 -0
  24. data/lib/connectors/gitlab/connector.rb +67 -0
  25. data/lib/connectors/gitlab/custom_client.rb +44 -0
  26. data/lib/connectors/gitlab/extractor.rb +69 -0
  27. data/lib/connectors/mongodb/connector.rb +138 -0
  28. data/lib/connectors/registry.rb +52 -0
  29. data/lib/connectors/sync_status.rb +21 -0
  30. data/lib/connectors.rb +16 -0
  31. data/lib/connectors_app/// +13 -0
  32. data/lib/connectors_service.rb +24 -0
  33. data/lib/connectors_utility.rb +16 -0
  34. data/lib/core/configuration.rb +48 -0
  35. data/lib/core/connector_settings.rb +142 -0
  36. data/lib/core/elastic_connector_actions.rb +269 -0
  37. data/lib/core/heartbeat.rb +32 -0
  38. data/lib/core/native_scheduler.rb +24 -0
  39. data/lib/core/output_sink/base_sink.rb +33 -0
  40. data/lib/core/output_sink/combined_sink.rb +38 -0
  41. data/lib/core/output_sink/console_sink.rb +51 -0
  42. data/lib/core/output_sink/es_sink.rb +74 -0
  43. data/lib/core/output_sink.rb +13 -0
  44. data/lib/core/scheduler.rb +158 -0
  45. data/lib/core/single_scheduler.rb +29 -0
  46. data/lib/core/sync_job_runner.rb +111 -0
  47. data/lib/core.rb +16 -0
  48. data/lib/list_connectors.rb +22 -0
  49. data/lib/stubs/app_config.rb +35 -0
  50. data/lib/stubs/connectors/stats.rb +35 -0
  51. data/lib/stubs/service_type.rb +13 -0
  52. data/lib/utility/constants.rb +20 -0
  53. data/lib/utility/cron.rb +81 -0
  54. data/lib/utility/elasticsearch/index/language_data.yml +111 -0
  55. data/lib/utility/elasticsearch/index/mappings.rb +104 -0
  56. data/lib/utility/elasticsearch/index/text_analysis_settings.rb +226 -0
  57. data/lib/utility/environment.rb +33 -0
  58. data/lib/utility/errors.rb +132 -0
  59. data/lib/utility/es_client.rb +84 -0
  60. data/lib/utility/exception_tracking.rb +64 -0
  61. data/lib/utility/extension_mapping_util.rb +123 -0
  62. data/lib/utility/logger.rb +84 -0
  63. data/lib/utility/middleware/basic_auth.rb +27 -0
  64. data/lib/utility/middleware/bearer_auth.rb +27 -0
  65. data/lib/utility/middleware/restrict_hostnames.rb +73 -0
  66. data/lib/utility.rb +16 -0
  67. 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
@@ -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