diffend 0.2.19 → 0.2.27

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.
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module HandleErrors
5
+ # Module responsible for reporting errors to diffend
6
+ module Report
7
+ class << self
8
+ # Execute request to Diffend
9
+ #
10
+ # @param exception [Exception] expection that was raised
11
+ # @param payload [Hash] with versions to check
12
+ # @param config [OpenStruct] Diffend config
13
+ # @param message [Symbol] message that we want to display
14
+ # @param report [Boolean] if true we will report the issue to diffend
15
+ # @param raise_exception [Boolean] if true we will raise an exception
16
+ #
17
+ # @return [Net::HTTPResponse] response from Diffend
18
+ def call(config:, message:, exception: nil, payload: {}, report: false, raise_exception: true)
19
+ exception_payload = prepare_exception_payload(exception, payload)
20
+
21
+ Bundler.ui.error(Diffend::HandleErrors::Messages::PAYLOAD_DUMP)
22
+ Bundler.ui.error(Diffend::HandleErrors::Messages.const_get(message.to_s.upcase))
23
+
24
+ if report
25
+ Diffend::Request.call(
26
+ build_request_object(config, exception_payload)
27
+ )
28
+ end
29
+
30
+ raise Diffend::Errors::HandledException if raise_exception
31
+ end
32
+
33
+ # @param config [OpenStruct] diffend config
34
+ # @param payload [Hash]
35
+ #
36
+ # @return [Diffend::RequestObject]
37
+ def build_request_object(config, payload)
38
+ Diffend::RequestObject.new(
39
+ config: config,
40
+ url: errors_url(config.project_id),
41
+ payload: payload,
42
+ request_method: :post
43
+ )
44
+ end
45
+
46
+ # Prepare exception payload and display it to stdout
47
+ #
48
+ # @param exception [Exception] expection that was raised
49
+ # @param payload [Hash] with versions to check
50
+ #
51
+ # @return [Hash]
52
+ def prepare_exception_payload(exception, payload)
53
+ Diffend::HandleErrors::BuildExceptionPayload
54
+ .call(exception, payload)
55
+ .tap(&Diffend::HandleErrors::DisplayToStdout.method(:call))
56
+ end
57
+
58
+ # Provides diffend errors endpoint url
59
+ #
60
+ # @param project_id [String] diffend project_id
61
+ #
62
+ # @return [String] diffend endpoint
63
+ def errors_url(project_id)
64
+ return ENV['DIFFEND_ERROR_URL'] if ENV.key?('DIFFEND_ERROR_URL')
65
+
66
+ "https://my.diffend.io/api/projects/#{project_id}/errors"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ %w[
4
+ build_bundler_definition
5
+ errors
6
+ config/fetcher
7
+ config/file_finder
8
+ config/validator
9
+ commands
10
+ handle_errors/messages
11
+ handle_errors/build_exception_payload
12
+ handle_errors/display_to_stdout
13
+ handle_errors/report
14
+ request_object
15
+ request
16
+ voting
17
+ track
18
+ ].each { |file| require "diffend/#{file}" }
19
+
20
+ %w[
21
+ versions/local
22
+ versions/remote
23
+ ].each { |file| require "diffend/voting/#{file}" }
24
+
25
+ Thread.new do
26
+ track = Diffend::Track.new
27
+ track.start
28
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'openssl'
5
+ require 'json'
6
+
7
+ module Diffend
8
+ # Module responsible for doing request to Diffend
9
+ module Request
10
+ # Message displayed when connection issue occured and we will retry
11
+ CONNECTION_MESSAGE = 'We experienced a connection issue, retrying...'
12
+ # List of connection exceptions
13
+ CONNECTION_EXCEPTIONS = [
14
+ Errno::ECONNRESET,
15
+ Errno::ENETUNREACH,
16
+ Errno::EHOSTUNREACH,
17
+ Errno::ECONNREFUSED
18
+ ].freeze
19
+ # Message displayed when timeout occured and we will retry
20
+ TIMEOUT_MESSAGE = 'We experienced a connection issue, retrying...'
21
+ # List of timeout exceptions
22
+ TIMEOUT_EXCEPTIONS = [
23
+ Net::OpenTimeout,
24
+ Net::ReadTimeout
25
+ ].freeze
26
+ # Message displayed when server issue occured and we will retry
27
+ SERVER_ERROR_MESSAGE = 'We experienced a server-side issue, retrying...'
28
+ # List of server issues
29
+ #
30
+ # 500 - Internal Server Error
31
+ # 502 - Bad Gateway
32
+ # 503 - Service Unavailable
33
+ # 504 - Gateway Timeout
34
+ SERVER_ERRORS = [500, 502, 503, 504].freeze
35
+ # Number of retries
36
+ RETRIES = 3
37
+ # Request headers
38
+ HEADERS = { 'Content-Type': 'application/json' }.freeze
39
+
40
+ private_constant :HEADERS
41
+
42
+ class << self
43
+ # Execute request
44
+ #
45
+ # @param request_object [Diffend::RequestObject]
46
+ #
47
+ # @return [Net::HTTPResponse] response from Diffend
48
+ def call(request_object)
49
+ retry_count ||= -1
50
+
51
+ build_http(request_object.url) do |http, uri|
52
+ response = http.request(
53
+ build_request(
54
+ uri,
55
+ request_object.request_method,
56
+ request_object.config,
57
+ request_object.payload
58
+ )
59
+ )
60
+
61
+ if SERVER_ERRORS.include?(response.code.to_i)
62
+ raise Diffend::Errors::RequestServerError, response.code.to_i
63
+ end
64
+
65
+ response
66
+ end
67
+ rescue Diffend::Errors::RequestServerError => e
68
+ retry_count += 1
69
+
70
+ retry if handle_retry(SERVER_ERROR_MESSAGE, retry_count)
71
+
72
+ Diffend::HandleErrors::Report.call(
73
+ exception: e,
74
+ payload: request_object.payload,
75
+ config: request_object.config,
76
+ message: :request_error
77
+ )
78
+ rescue *CONNECTION_EXCEPTIONS => e
79
+ retry_count += 1
80
+
81
+ retry if handle_retry(CONNECTION_MESSAGE, retry_count)
82
+
83
+ Diffend::HandleErrors::Report.call(
84
+ exception: e,
85
+ payload: request_object.payload,
86
+ config: request_object.config,
87
+ message: :request_error
88
+ )
89
+ rescue *TIMEOUT_EXCEPTIONS => e
90
+ retry_count += 1
91
+
92
+ retry if handle_retry(TIMEOUT_MESSAGE, retry_count)
93
+
94
+ Diffend::HandleErrors::Report.call(
95
+ exception: e,
96
+ payload: request_object.payload,
97
+ config: request_object.config,
98
+ message: :request_error
99
+ )
100
+ end
101
+
102
+ # Handle retry
103
+ #
104
+ # @param message [String] message we want to display
105
+ # @param retry_count [Integer]
106
+ def handle_retry(message, retry_count)
107
+ return false if retry_count == RETRIES
108
+
109
+ Bundler.ui.error(message)
110
+ sleep(exponential_backoff(retry_count))
111
+
112
+ retry_count < RETRIES
113
+ end
114
+
115
+ # Builds http connection object
116
+ #
117
+ # @param url [String] command endpoint url
118
+ def build_http(url)
119
+ uri = URI(url)
120
+
121
+ Net::HTTP.start(
122
+ uri.host,
123
+ uri.port,
124
+ use_ssl: uri.scheme == 'https',
125
+ verify_mode: OpenSSL::SSL::VERIFY_NONE,
126
+ open_timeout: 5,
127
+ read_timeout: 5
128
+ ) { |http| yield(http, uri) }
129
+ end
130
+
131
+ # Build http post request and assigns headers and payload
132
+ #
133
+ # @param uri [URI::HTTPS]
134
+ # @param request_method [Symbol]
135
+ # @param config [OpenStruct] Diffend config
136
+ # @param payload [Hash] with versions to check
137
+ #
138
+ # @return [Net::HTTP::Post, Net::HTTP::Put]
139
+ def build_request(uri, request_method, config, payload)
140
+ pick_request_method(request_method)
141
+ .new(uri.request_uri, HEADERS)
142
+ .tap { |request| assign_auth(request, config) }
143
+ .tap { |request| assign_payload(request, payload) }
144
+ end
145
+
146
+ # Pick request method
147
+ #
148
+ # @param request_method [Symbol]
149
+ #
150
+ # @return [Net::HTTP::Post, Net::HTTP::Put]
151
+ def pick_request_method(request_method)
152
+ case request_method
153
+ when :post
154
+ Net::HTTP::Post
155
+ when :put
156
+ Net::HTTP::Put
157
+ end
158
+ end
159
+
160
+ # Assigns basic authorization if provided in the config
161
+ #
162
+ # @param request [Net::HTTP::Post] prepared http post
163
+ # @param config [OpenStruct] Diffend config
164
+ def assign_auth(request, config)
165
+ return unless config
166
+ return unless config.shareable_id
167
+ return unless config.shareable_key
168
+
169
+ request.basic_auth(config.shareable_id, config.shareable_key)
170
+ end
171
+
172
+ # Assigns payload as json
173
+ #
174
+ # @param request [Net::HTTP::Post] prepared http post
175
+ # @param payload [Hash] with versions to check
176
+ def assign_payload(request, payload)
177
+ request.body = JSON.dump(payload: payload)
178
+ end
179
+
180
+ def exponential_backoff(retry_count)
181
+ 2**(retry_count + 1)
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ # Class responsible for preparing diffend request object
5
+ RequestObject = Struct.new(:config, :url, :payload, :request_method, keyword_init: true)
6
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ # Track what is run in production
5
+ class Track
6
+ # Time that we want to wait between track requests
7
+ TRACK_SLEEP = 15
8
+ # Time that we want to wait before we retry
9
+ RETRY_SLEEP = 15
10
+
11
+ # Initialize tracking
12
+ def initialize
13
+ @mutex = Mutex.new
14
+ @config = fetch_config
15
+ end
16
+
17
+ # Start tracking
18
+ def start
19
+ response = exec_request
20
+
21
+ perform(response['id'])
22
+ rescue Diffend::Errors::HandledException
23
+ sleep(RETRY_SLEEP)
24
+
25
+ retry
26
+ rescue StandardError => e
27
+ Diffend::HandleErrors::Report.call(
28
+ exception: e,
29
+ config: @config,
30
+ message: :unhandled_exception,
31
+ report: true,
32
+ raise_exception: false
33
+ )
34
+
35
+ sleep(RETRY_SLEEP)
36
+
37
+ retry
38
+ end
39
+
40
+ # @param request_id [String]
41
+ def perform(request_id)
42
+ loop do
43
+ @mutex.synchronize do
44
+ track_request(request_id)
45
+ end
46
+
47
+ sleep(TRACK_SLEEP)
48
+ end
49
+ end
50
+
51
+ # Perform an exec request
52
+ def exec_request
53
+ Diffend::Voting.call(
54
+ Diffend::Commands::EXEC,
55
+ @config,
56
+ Diffend::BuildBundlerDefinition.call(
57
+ Diffend::Commands::EXEC,
58
+ Bundler.default_gemfile,
59
+ Bundler.default_lockfile
60
+ )
61
+ )
62
+ end
63
+
64
+ # Perform a track request
65
+ #
66
+ # @param request_id [String]
67
+ def track_request(request_id)
68
+ Diffend::Request.call(
69
+ build_request_object(request_id)
70
+ )
71
+ end
72
+
73
+ # @param request_id [String]
74
+ #
75
+ # @return [Diffend::RequestObject]
76
+ def build_request_object(request_id)
77
+ Diffend::RequestObject.new(
78
+ config: @config,
79
+ url: track_url(@config.project_id, request_id),
80
+ payload: { id: request_id }.freeze,
81
+ request_method: :put
82
+ ).freeze
83
+ end
84
+
85
+ # Fetch diffend config file
86
+ #
87
+ # @return [OpenStruct, nil] configuration object
88
+ #
89
+ # @raise [Errors::MissingConfigurationFile] when no config file
90
+ def fetch_config
91
+ Config::Fetcher.call(
92
+ File.expand_path('..', Bundler.bin_path)
93
+ )
94
+ end
95
+
96
+ # @param project_id [String] diffend project_id
97
+ # @param request_id [String]
98
+ #
99
+ # @return [String]
100
+ def track_url(project_id, request_id)
101
+ "https://my.diffend.io/api/projects/#{project_id}/bundle/#{request_id}/track"
102
+ end
103
+ end
104
+ end
@@ -7,45 +7,69 @@ module Diffend
7
7
  # Build verdict
8
8
  #
9
9
  # @param command [String] either install or update
10
+ # @param config [OpenStruct] diffend config
10
11
  # @param definition [Bundler::Definition] definition for your source
11
- def call(command, definition)
12
+ def call(command, config, definition)
12
13
  Versions::Remote
13
- .call(command, definition)
14
- .tap { |response| build_message(command, response) }
14
+ .call(command, config, definition)
15
+ .tap { |response| build_message(command, config, response) }
15
16
  end
16
17
 
17
- def build_message(command, response)
18
+ # @param command [String] either install or update
19
+ # @param config [OpenStruct] diffend config
20
+ # @param response [Hash] response from diffend API
21
+ def build_message(command, config, response)
18
22
  if response.key?('error')
19
23
  build_error(response)
20
24
  elsif response.key?('action')
21
- build_verdict(command, response)
25
+ build_verdict(command, config, response)
22
26
  else
23
- raise UnsupportedResponse, response['action']
27
+ Diffend::HandleErrors::Report.call(
28
+ config: config,
29
+ message: :unsupported_response,
30
+ payload: response,
31
+ report: true
32
+ )
24
33
  end
25
34
  end
26
35
 
36
+ # @param response [Hash] response from diffend API
27
37
  def build_error(response)
28
38
  build_error_message(response)
29
39
  .tap(&Bundler.ui.method(:error))
30
40
 
31
- exit 1
41
+ raise Diffend::Errors::HandledException
32
42
  end
33
43
 
34
- def build_verdict(command, response)
44
+ # @param command [String] either install or update
45
+ # @param config [OpenStruct] diffend config
46
+ # @param response [Hash] response from diffend API
47
+ def build_verdict(command, config, response)
35
48
  case response['action']
36
49
  when 'allow'
37
50
  build_allow_message(command, response)
38
51
  .tap(&Bundler.ui.method(:confirm))
52
+ when 'warn'
53
+ build_warn_message(command, response)
54
+ .tap(&Bundler.ui.method(:warn))
39
55
  when 'deny'
40
56
  build_deny_message(command, response)
41
57
  .tap(&Bundler.ui.method(:error))
42
58
 
43
59
  exit 1
44
60
  else
45
- raise UnsupportedAction, response['action']
61
+ Diffend::HandleErrors::Report.call(
62
+ config: config,
63
+ message: :unsupported_verdict,
64
+ payload: response,
65
+ report: true
66
+ )
46
67
  end
47
68
  end
48
69
 
70
+ # @param response [Hash] response from diffend API
71
+ #
72
+ # @return [String]
49
73
  def build_error_message(response)
50
74
  <<~MSG
51
75
  \nDiffend returned an error for your request.\n
@@ -53,21 +77,56 @@ module Diffend
53
77
  MSG
54
78
  end
55
79
 
80
+ # @param command [String] either install or update
81
+ # @param response [Hash] response from diffend API
82
+ #
83
+ # @return [String]
56
84
  def build_allow_message(command, response)
57
85
  <<~MSG
58
- \nDiffend reported an allow verdict for #{command} command for this project.\n
59
- All of our #{response['allows_count'] + response['denies_count']} checks succeeded.\n
86
+ #{build_message_header('an allow', command)}
87
+ #{build_message_info(response)}\n
60
88
  #{response['review_url']}\n
61
89
  MSG
62
90
  end
63
91
 
92
+ # @param command [String] either install or update
93
+ # @param response [Hash] response from diffend API
94
+ #
95
+ # @return [String]
96
+ def build_warn_message(command, response)
97
+ <<~MSG
98
+ #{build_message_header('a warn', command)}
99
+ #{build_message_info(response)} Please go to the url below and review the issues.\n
100
+ #{response['review_url']}\n
101
+ MSG
102
+ end
103
+
104
+ # @param command [String] either install or update
105
+ # @param response [Hash] response from diffend API
106
+ #
107
+ # @return [String]
64
108
  def build_deny_message(command, response)
65
109
  <<~MSG
66
- \nDiffend reported a deny verdict for #{command} command for this project.\n
67
- #{response['denies_count']} out of our #{response['allows_count'] + response['denies_count']} checks failed. Please go to the url below and review the issues.\n
110
+ #{build_message_header('a deny', command)}
111
+ #{build_message_info(response)} Please go to the url below and review the issues.\n
68
112
  #{response['review_url']}\n
69
113
  MSG
70
114
  end
115
+
116
+ # @param type [String] verdict type
117
+ # @param command [String] either install or update
118
+ #
119
+ # @return [String]
120
+ def build_message_header(type, command)
121
+ "\nDiffend reported #{type} verdict for #{command} command for this project."
122
+ end
123
+
124
+ # @param response [Hash] response from diffend API
125
+ #
126
+ # @return [String]
127
+ def build_message_info(response)
128
+ "\nQuality score: #{response['quality_score']}, allows: #{response['allows_count']}, warnings: #{response['warns_count']}, denies: #{response['denies_count']}."
129
+ end
71
130
  end
72
131
  end
73
132
  end