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.
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