diffend-monitor 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,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