diffend-monitor 0.2.27

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ # Verifies voting verdicts for gems
5
+ module Voting
6
+ class << self
7
+ # Build verdict
8
+ #
9
+ # @param command [String] either install or update
10
+ # @param config [OpenStruct] diffend config
11
+ # @param definition [Bundler::Definition] definition for your source
12
+ def call(command, config, definition)
13
+ Versions::Remote
14
+ .call(command, config, definition)
15
+ .tap { |response| build_message(command, config, response) }
16
+ end
17
+
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)
22
+ if response.key?('error')
23
+ build_error(response)
24
+ elsif response.key?('action')
25
+ build_verdict(command, config, response)
26
+ else
27
+ Diffend::HandleErrors::Report.call(
28
+ config: config,
29
+ message: :unsupported_response,
30
+ payload: response,
31
+ report: true
32
+ )
33
+ end
34
+ end
35
+
36
+ # @param response [Hash] response from diffend API
37
+ def build_error(response)
38
+ build_error_message(response)
39
+ .tap(&Bundler.ui.method(:error))
40
+
41
+ raise Diffend::Errors::HandledException
42
+ end
43
+
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)
48
+ case response['action']
49
+ when 'allow'
50
+ build_allow_message(command, response)
51
+ .tap(&Bundler.ui.method(:confirm))
52
+ when 'warn'
53
+ build_warn_message(command, response)
54
+ .tap(&Bundler.ui.method(:warn))
55
+ when 'deny'
56
+ build_deny_message(command, response)
57
+ .tap(&Bundler.ui.method(:error))
58
+
59
+ exit 1
60
+ else
61
+ Diffend::HandleErrors::Report.call(
62
+ config: config,
63
+ message: :unsupported_verdict,
64
+ payload: response,
65
+ report: true
66
+ )
67
+ end
68
+ end
69
+
70
+ # @param response [Hash] response from diffend API
71
+ #
72
+ # @return [String]
73
+ def build_error_message(response)
74
+ <<~MSG
75
+ \nDiffend returned an error for your request.\n
76
+ #{response['error']}\n
77
+ MSG
78
+ end
79
+
80
+ # @param command [String] either install or update
81
+ # @param response [Hash] response from diffend API
82
+ #
83
+ # @return [String]
84
+ def build_allow_message(command, response)
85
+ <<~MSG
86
+ #{build_message_header('an allow', command)}
87
+ #{build_message_info(response)}\n
88
+ #{response['review_url']}\n
89
+ MSG
90
+ end
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]
108
+ def build_deny_message(command, response)
109
+ <<~MSG
110
+ #{build_message_header('a deny', command)}
111
+ #{build_message_info(response)} Please go to the url below and review the issues.\n
112
+ #{response['review_url']}\n
113
+ MSG
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
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module Voting
5
+ # Module responsible for handling both local and remote gem versions
6
+ module Versions
7
+ # Module responsible for preparing current or current/new versions of gems
8
+ class Local
9
+ # Definition of a local path, if it matches it means that we are the source
10
+ ME_PATH = '.'
11
+ # Sources that we expect to match ourselves too
12
+ ME_SOURCES = [
13
+ Bundler::Source::Gemspec,
14
+ Bundler::Source::Path
15
+ ].freeze
16
+ # List of dependency types
17
+ DEPENDENCIES_TYPES = {
18
+ direct: 0,
19
+ dependency: 1
20
+ }.freeze
21
+ # List of sources types
22
+ SOURCES_TYPES = {
23
+ valid: 0,
24
+ multiple_primary: 1
25
+ }.freeze
26
+ # List of gem sources types
27
+ GEM_SOURCES_TYPES = {
28
+ local: 0,
29
+ gemfile_source: 1,
30
+ gemfile_git: 2,
31
+ gemfile_path: 3
32
+ }.freeze
33
+
34
+ class << self
35
+ # @param command [String] either install or update
36
+ # @param definition [Bundler::Definition] definition for your source
37
+ def call(command, definition)
38
+ Bundler.ui.silence { definition.resolve_remotely! }
39
+
40
+ instance = new(definition)
41
+
42
+ case command
43
+ when Commands::INSTALL, Commands::EXEC then instance.build_install
44
+ when Commands::UPDATE then instance.build_update
45
+ else
46
+ raise ArgumentError, "invalid command: #{command}"
47
+ end
48
+ end
49
+ end
50
+
51
+ # @param definition [Bundler::Definition] definition for your source
52
+ #
53
+ # @return [Hash] local dependencies
54
+ def initialize(definition)
55
+ @definition = definition
56
+ @direct_dependencies = Hash[definition.dependencies.map { |val| [val.name, val] }]
57
+ # Support case without Gemfile.lock
58
+ @locked_specs = @definition.locked_gems ? @definition.locked_gems.specs : []
59
+ end
60
+
61
+ # Build install specification
62
+ #
63
+ # @return [Hash]
64
+ def build_install
65
+ hash = build_main
66
+
67
+ @definition.specs.each do |spec|
68
+ next if skip?(spec.source)
69
+
70
+ locked_spec = @locked_specs.find { |s| s.name == spec.name }
71
+
72
+ hash['dependencies'][spec.name] = {
73
+ 'platform' => build_spec_platform(spec, locked_spec),
74
+ 'source' => build_spec_source(spec),
75
+ 'type' => build_dependency_type(spec.name),
76
+ 'versions' => build_versions(spec, locked_spec)
77
+ }
78
+ end
79
+
80
+ hash
81
+ end
82
+
83
+ # Build update specification
84
+ #
85
+ # @return [Hash]
86
+ def build_update
87
+ hash = build_main
88
+
89
+ @definition.specs.each do |spec|
90
+ next if skip?(spec.source)
91
+
92
+ locked_spec = @locked_specs.find { |s| s.name == spec.name }
93
+
94
+ hash['dependencies'][spec.name] = {
95
+ 'platform' => build_spec_platform(spec, locked_spec),
96
+ 'source' => build_spec_source(spec),
97
+ 'type' => build_dependency_type(spec.name),
98
+ 'versions' => build_versions(spec, locked_spec)
99
+ }
100
+ end
101
+
102
+ hash
103
+ end
104
+
105
+ private
106
+
107
+ # Build default specification
108
+ #
109
+ # @return [Hash]
110
+ def build_main
111
+ {
112
+ 'dependencies' => {},
113
+ 'sources' => build_sources,
114
+ 'plugins' => {},
115
+ 'platforms' => @definition.platforms.map(&:to_s)
116
+ }
117
+ end
118
+
119
+ # Build gem versions
120
+ #
121
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
122
+ # @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass]
123
+ #
124
+ # @return [Array<String>]
125
+ def build_versions(spec, locked_spec = nil)
126
+ if locked_spec && locked_spec.version.to_s != spec.version.to_s
127
+ [locked_spec.version.to_s, spec.version.to_s]
128
+ else
129
+ [spec.version.to_s]
130
+ end
131
+ end
132
+
133
+ # @param specs [Array] specs that are direct dependencies
134
+ # @param name [String] spec name
135
+ #
136
+ # @return [Boolean] dependency type
137
+ def build_dependency_type(name)
138
+ if @direct_dependencies.key?(name)
139
+ DEPENDENCIES_TYPES[:direct]
140
+ else
141
+ DEPENDENCIES_TYPES[:dependency]
142
+ end
143
+ end
144
+
145
+ # Build gem platform
146
+ #
147
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
148
+ # @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass]
149
+ #
150
+ # @return [String]
151
+ def build_spec_platform(spec, locked_spec)
152
+ parse_platform(
153
+ spec.platform || locked_spec&.platform || spec.send(:generic_local_platform)
154
+ )
155
+ end
156
+
157
+ # Parse gem platform
158
+ #
159
+ # @param platform [String, Gem::Platform]
160
+ #
161
+ # @return [String]
162
+ def parse_platform(platform)
163
+ case platform
164
+ when String then platform
165
+ when Gem::Platform then platform.os
166
+ end
167
+ end
168
+
169
+ # Build gem source type
170
+ #
171
+ # @param source [Bundler::Source] gem source type
172
+ #
173
+ # @return [Integer] internal gem source type
174
+ def build_spec_gem_source_type(source)
175
+ case source
176
+ when Bundler::Source::Metadata
177
+ GEM_SOURCES_TYPES[:local]
178
+ when Bundler::Source::Rubygems, Bundler::Source::Rubygems::Remote
179
+ GEM_SOURCES_TYPES[:gemfile_source]
180
+ when Bundler::Source::Git
181
+ GEM_SOURCES_TYPES[:gemfile_git]
182
+ when Bundler::Source::Path
183
+ GEM_SOURCES_TYPES[:gemfile_path]
184
+ else
185
+ raise ArgumentError, "unknown source #{source.class}"
186
+ end
187
+ end
188
+
189
+ # Build gem source
190
+ #
191
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
192
+ #
193
+ # @return [Hash]
194
+ def build_spec_source(spec)
195
+ source = source_for_spec(spec)
196
+
197
+ {
198
+ 'type' => build_spec_gem_source_type(source),
199
+ 'value' => source_name_from_source(source)
200
+ }
201
+ end
202
+
203
+ # Figure out source for gem
204
+ #
205
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
206
+ #
207
+ # @return [Bundler::Source] gem source type
208
+ def source_for_spec(spec)
209
+ return spec.remote if spec.remote
210
+
211
+ case spec.source
212
+ when Bundler::Source::Rubygems
213
+ spec
214
+ .source
215
+ .send(:remote_specs)
216
+ .search(Bundler::Dependency.new(spec.name, spec.version))
217
+ .last
218
+ .remote
219
+ when Bundler::Source::Metadata, Bundler::Source::Git, Bundler::Source::Path
220
+ spec.source
221
+ else
222
+ raise ArgumentError, "unknown source #{spec.source.class}"
223
+ end
224
+ end
225
+
226
+ # Build gem source name
227
+ #
228
+ # @param source [Bundler::Source] gem source type
229
+ #
230
+ # @return [String]
231
+ def source_name_from_source(source)
232
+ case source
233
+ when Bundler::Source::Metadata
234
+ ''
235
+ when Bundler::Source::Rubygems::Remote
236
+ source_name(source.anonymized_uri)
237
+ when Bundler::Source::Git
238
+ source.instance_variable_get(:@safe_uri)
239
+ when Bundler::Source::Path
240
+ source.path
241
+ else
242
+ raise ArgumentError, "unknown source #{source.class}"
243
+ end
244
+ end
245
+
246
+ # @param uri [Bundler::URI]
247
+ #
248
+ # @return [String]
249
+ def source_name(uri)
250
+ uri.to_s[0...-1]
251
+ end
252
+
253
+ # Build sources used in the Gemfile
254
+ #
255
+ # @return [Array<Hash>]
256
+ def build_sources
257
+ sources = @definition.send(:sources).rubygems_sources
258
+ hash = {}
259
+
260
+ sources.each do |source|
261
+ type = build_source_type(source.remotes)
262
+
263
+ source.remotes.each do |src|
264
+ hash[source_name(src)] = type
265
+ end
266
+ end
267
+
268
+ hash.map { |name, type| { 'name' => name, 'type' => type } }
269
+ end
270
+
271
+ # Build gem source type
272
+ #
273
+ # @param remotes [Array<Bundler::URI>]
274
+ #
275
+ # @return [Integer] internal source type
276
+ def build_source_type(remotes)
277
+ remotes.count > 1 ? SOURCES_TYPES[:multiple_primary] : SOURCES_TYPES[:valid]
278
+ end
279
+
280
+ # Checks if we should skip a source
281
+ #
282
+ # @param source [Bundler::Source] gem source type
283
+ #
284
+ # @return [Boolean] true if we should skip this source, false otherwise
285
+ def skip?(source)
286
+ return true if me?(source)
287
+
288
+ false
289
+ end
290
+
291
+ # Checks if it's a self source, this happens for repositories that are a gem
292
+ #
293
+ # @param source [Bundler::Source] gem source type
294
+ #
295
+ # @return [Boolean] true if it's a self source, false otherwise
296
+ def me?(source)
297
+ return false unless ME_SOURCES.include?(source.class)
298
+
299
+ source.path.to_s == ME_PATH
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end