artifactory 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -16
  3. data/.travis.yml +8 -0
  4. data/CHANGELOG.md +9 -0
  5. data/Gemfile +6 -2
  6. data/LICENSE +202 -0
  7. data/README.md +216 -17
  8. data/Rakefile +6 -1
  9. data/artifactory.gemspec +15 -10
  10. data/lib/artifactory.rb +74 -2
  11. data/lib/artifactory/client.rb +222 -0
  12. data/lib/artifactory/collections/artifact.rb +12 -0
  13. data/lib/artifactory/collections/base.rb +49 -0
  14. data/lib/artifactory/configurable.rb +73 -0
  15. data/lib/artifactory/defaults.rb +67 -0
  16. data/lib/artifactory/errors.rb +22 -0
  17. data/lib/artifactory/resources/artifact.rb +481 -0
  18. data/lib/artifactory/resources/base.rb +145 -0
  19. data/lib/artifactory/resources/build.rb +27 -0
  20. data/lib/artifactory/resources/plugin.rb +22 -0
  21. data/lib/artifactory/resources/repository.rb +251 -0
  22. data/lib/artifactory/resources/system.rb +114 -0
  23. data/lib/artifactory/resources/user.rb +57 -0
  24. data/lib/artifactory/util.rb +93 -0
  25. data/lib/artifactory/version.rb +1 -1
  26. data/locales/en.yml +27 -0
  27. data/spec/integration/resources/artifact_spec.rb +73 -0
  28. data/spec/integration/resources/build_spec.rb +11 -0
  29. data/spec/integration/resources/repository_spec.rb +13 -0
  30. data/spec/integration/resources/system_spec.rb +59 -0
  31. data/spec/spec_helper.rb +40 -0
  32. data/spec/support/api_server.rb +41 -0
  33. data/spec/support/api_server/artifact_endpoints.rb +122 -0
  34. data/spec/support/api_server/build_endpoints.rb +22 -0
  35. data/spec/support/api_server/repository_endpoints.rb +75 -0
  36. data/spec/support/api_server/status_endpoints.rb +11 -0
  37. data/spec/support/api_server/system_endpoints.rb +44 -0
  38. data/spec/unit/artifactory_spec.rb +73 -0
  39. data/spec/unit/client_spec.rb +176 -0
  40. data/spec/unit/resources/artifact_spec.rb +377 -0
  41. data/spec/unit/resources/base_spec.rb +140 -0
  42. data/spec/unit/resources/build_spec.rb +34 -0
  43. data/spec/unit/resources/plugin_spec.rb +25 -0
  44. data/spec/unit/resources/repository_spec.rb +180 -0
  45. data/spec/unit/resources/system_spec.rb +88 -0
  46. metadata +106 -36
  47. data/LICENSE.txt +0 -22
@@ -0,0 +1,67 @@
1
+ require 'artifactory/version'
2
+
3
+ module Artifactory
4
+ module Defaults
5
+ # Default API endpoint
6
+ ENDPOINT = 'http://localhost:8080/artifactory'.freeze
7
+
8
+ # Default User Agent header string
9
+ USER_AGENT = "Artifactory Ruby Gem #{Artifactory::VERSION}".freeze
10
+
11
+ class << self
12
+ #
13
+ # The list of calculated default options for the configuration.
14
+ #
15
+ # @return [Hash]
16
+ #
17
+ def options
18
+ Hash[Configurable.keys.map { |key| [key, send(key)] }]
19
+ end
20
+
21
+ #
22
+ # The endpoint where artifactory lives
23
+ #
24
+ # @return [String]
25
+ #
26
+ def endpoint
27
+ ENV['ARTIFACTORY_ENDPOINT'] || ENDPOINT
28
+ end
29
+
30
+ #
31
+ # The User Agent header to send along
32
+ #
33
+ # @return [String]
34
+ #
35
+ def user_agent
36
+ ENV['ARTIFACTORY_USER_AGENT'] || USER_AGENT
37
+ end
38
+
39
+ #
40
+ # The HTTP Basic Authentication username
41
+ #
42
+ # @return [String, nil]
43
+ #
44
+ def username
45
+ ENV['ARTIFACTORY_USERNAME']
46
+ end
47
+
48
+ #
49
+ # The HTTP Basic Authentication password
50
+ #
51
+ # @return [String, nil]
52
+ #
53
+ def password
54
+ ENV['ARTIFACTORY_PASSWORD']
55
+ end
56
+
57
+ #
58
+ # The HTTP Proxy information as a string
59
+ #
60
+ # @return [String, nil]
61
+ #
62
+ def proxy
63
+ ENV['ARTIFACTORY_PROXY']
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,22 @@
1
+ module Artifactory
2
+ module Error
3
+ class ArtifactoryError < StandardError
4
+ def initialize(options = {})
5
+ class_name = self.class.to_s.split('::').last
6
+ error_key = Util.underscore(class_name)
7
+
8
+ super I18n.t("artifactory.errors.#{error_key}", options)
9
+ end
10
+ end
11
+
12
+ #
13
+ # Client errors
14
+ # ------------------------------
15
+ class ConnectionError < ArtifactoryError; end
16
+ class BadRequest < ConnectionError; end
17
+ class Forbidden < ConnectionError; end
18
+ class MethodNotAllowed < ConnectionError; end
19
+ class NotFound < ConnectionError; end
20
+ class Unauthorized < ConnectionError; end
21
+ end
22
+ end
@@ -0,0 +1,481 @@
1
+ module Artifactory
2
+ class Resource::Artifact < Resource::Base
3
+ class << self
4
+ #
5
+ # Search for an artifact by the full or partial filename.
6
+ #
7
+ # @example Search for all repositories with the name "artifact"
8
+ # Artifact.search(name: 'artifact')
9
+ #
10
+ # @example Search for all artifacts named "artifact" in a specific repo
11
+ # Artifact.search(name: 'artifact', repos: 'libs-release-local')
12
+ #
13
+ # @param [Hash] options
14
+ # the list of options to search with
15
+ #
16
+ # @option options [Artifactory::Client] :client
17
+ # the client object to make the request with
18
+ # @option options [String] :name
19
+ # the name of the artifact to search (it can be a regular expression)
20
+ # @option options [String, Array<String>] :repos
21
+ # the list of repos to search
22
+ #
23
+ # @return [Array<Resource::Artifact>]
24
+ # a list of artifacts that match the query
25
+ #
26
+ def search(options = {})
27
+ client = extract_client!(options)
28
+ params = Util.slice(options, :name, :repos)
29
+ format_repos!(params)
30
+
31
+ client.get('/api/search/artifact', params)['results'].map do |artifact|
32
+ from_url(artifact['uri'], client: client)
33
+ end
34
+ end
35
+
36
+ #
37
+ # Search for an artifact by Maven coordinates: +Group ID+, +Artifact ID+,
38
+ # +Version+ and +Classifier+.
39
+ #
40
+ # @example Search for all repositories with the given gavc
41
+ # Artifact.gavc_search(
42
+ # group: 'org.acme',
43
+ # name: 'artifact',
44
+ # version: '1.0',
45
+ # classifier: 'sources',
46
+ # )
47
+ #
48
+ # @example Search for all artifacts with the given gavc in a specific repo
49
+ # Artifact.gavc_search(
50
+ # group: 'org.acme',
51
+ # name: 'artifact',
52
+ # version: '1.0',
53
+ # classifier: 'sources',
54
+ # repos: 'libs-release-local',
55
+ # )
56
+ #
57
+ # @param [Hash] options
58
+ # the list of options to search with
59
+ #
60
+ # @option options [Artifactory::Client] :client
61
+ # the client object to make the request with
62
+ # @option options [String] :group
63
+ # the group id to search for
64
+ # @option options [String] :name
65
+ # the artifact id to search for
66
+ # @option options [String] :version
67
+ # the version of the artifact to search for
68
+ # @option options [String] :classifier
69
+ # the classifer to search for
70
+ # @option options [String, Array<String>] :repos
71
+ # the list of repos to search
72
+ #
73
+ # @return [Array<Resource::Artifact>]
74
+ # a list of artifacts that match the query
75
+ #
76
+ def gavc_search(options = {})
77
+ client = extract_client!(options)
78
+ options = Util.rename_keys(options,
79
+ :group => :g,
80
+ :name => :a,
81
+ :version => :v,
82
+ :classifier => :c,
83
+ )
84
+ params = Util.slice(options, :g, :a, :v, :c, :repos)
85
+ format_repos!(params)
86
+
87
+ client.get('/api/search/gavc', params)['results'].map do |artifact|
88
+ from_url(artifact['uri'], client: client)
89
+ end
90
+ end
91
+
92
+ #
93
+ # Search for an artifact by the given properties. These are arbitrary
94
+ # properties defined by the user on artifact, so the search uses a free-
95
+ # form schema.
96
+ #
97
+ # @example Search for all repositories with the given properties
98
+ # Artifact.property_search(
99
+ # branch: 'master',
100
+ # author: 'sethvargo',
101
+ # )
102
+ #
103
+ # @example Search for all artifacts with the given gavc in a specific repo
104
+ # Artifact.property_search(
105
+ # branch: 'master',
106
+ # author: 'sethvargo',
107
+ # repos: 'libs-release-local',
108
+ # )
109
+ #
110
+ # @param [Hash] options
111
+ # the free-form list of options to search with
112
+ #
113
+ # @option options [Artifactory::Client] :client
114
+ # the client object to make the request with
115
+ # @option options [String, Array<String>] :repos
116
+ # the list of repos to search
117
+ #
118
+ # @return [Array<Resource::Artifact>]
119
+ # a list of artifacts that match the query
120
+ #
121
+ def property_search(options = {})
122
+ client = extract_client!(options)
123
+ params = options.dup
124
+ format_repos!(params)
125
+
126
+ client.get('/api/search/prop', params)['results'].map do |artifact|
127
+ from_url(artifact['uri'], client: client)
128
+ end
129
+ end
130
+
131
+ #
132
+ # Search for an artifact by its checksum
133
+ #
134
+ # @example Search for all repositories with the given MD5 checksum
135
+ # Artifact.checksum_search(
136
+ # md5: 'abcd1234...',
137
+ # )
138
+ #
139
+ # @example Search for all artifacts with the given SHA1 checksum in a repo
140
+ # Artifact.checksum_search(
141
+ # sha1: 'abcdef123456....',
142
+ # repos: 'libs-release-local',
143
+ # )
144
+ #
145
+ # @param [Hash] options
146
+ # the list of options to search with
147
+ #
148
+ # @option options [Artifactory::Client] :client
149
+ # the client object to make the request with
150
+ # @option options [String] :md5
151
+ # the MD5 checksum of the artifact to search for
152
+ # @option options [String] :sha1
153
+ # the SHA1 checksum of the artifact to search for
154
+ # @option options [String, Array<String>] :repos
155
+ # the list of repos to search
156
+ #
157
+ # @return [Array<Resource::Artifact>]
158
+ # a list of artifacts that match the query
159
+ #
160
+ def checksum_search(options = {})
161
+ client = extract_client!(options)
162
+ params = Util.slice(options, :md5, :sha1, :repos)
163
+ format_repos!(params)
164
+
165
+ client.get('/api/search/checksum', params)['results'].map do |artifact|
166
+ from_url(artifact['uri'], client: client)
167
+ end
168
+ end
169
+
170
+ #
171
+ # Get all versions of an artifact.
172
+ #
173
+ # @example Get all versions of a given artifact
174
+ # Artifact.versions(name: 'artifact')
175
+ # @example Get all versions of a given artifact in a specific repo
176
+ # Artifact.versions(name: 'artifact', repos: 'libs-release-local')
177
+ #
178
+ # @param [Hash] options
179
+ # the list of options to search with
180
+ #
181
+ # @option options [Artifactory::Client] :client
182
+ # the client object to make the request with
183
+ # @option options [String] :group
184
+ # the
185
+ # @option options [String] :sha1
186
+ # the SHA1 checksum of the artifact to search for
187
+ # @option options [String, Array<String>] :repos
188
+ # the list of repos to search
189
+ #
190
+ def versions(options = {})
191
+ client = extract_client!(options)
192
+ options = Util.rename_keys(options,
193
+ :group => :g,
194
+ :name => :a,
195
+ :version => :v,
196
+ )
197
+ params = Util.slice(options, :g, :a, :v, :repos)
198
+ format_repos!(params)
199
+
200
+ client.get('/api/search/versions', params)['results']
201
+ rescue Error::NotFound
202
+ []
203
+ end
204
+
205
+ #
206
+ # Get the latest version of an artifact.
207
+ #
208
+ # @example Find the latest version of an artifact
209
+ # Artifact.latest_version(name: 'artifact')
210
+ # @example Find the latest version of an artifact in a repo
211
+ # Artifact.latest_version(
212
+ # name: 'artifact',
213
+ # repo: 'libs-release-local',
214
+ # )
215
+ # @example Find the latest snapshot version of an artifact
216
+ # Artifact.latest_version(name: 'artifact', version: '1.0-SNAPSHOT')
217
+ # @example Find the latest version of an artifact in a group
218
+ # Artifact.latest_version(name: 'artifact', group: 'org.acme')
219
+ #
220
+ # @param [Hash] options
221
+ # the list of options to search with
222
+ #
223
+ # @option options [Artifactory::Client] :client
224
+ # the client object to make the request with
225
+ # @option options [String] :group
226
+ # the group id to search for
227
+ # @option options [String] :name
228
+ # the artifact id to search for
229
+ # @option options [String] :version
230
+ # the version of the artifact to search for
231
+ # @option options [Boolean] :remote
232
+ # search remote repos (default: +false+)
233
+ # @option options [String, Array<String>] :repos
234
+ # the list of repos to search
235
+ #
236
+ # @return [String, nil]
237
+ # the latest version as a string (e.g. +1.0-201203131455-2+), or +nil+
238
+ # if no artifact matches the given query
239
+ #
240
+ def latest_version(options = {})
241
+ client = extract_client!(options)
242
+ options = Util.rename_keys(options,
243
+ :group => :g,
244
+ :name => :a,
245
+ :version => :v,
246
+ )
247
+ params = Util.slice(options, :g, :a, :v, :repos, :remote)
248
+ format_repos!(params)
249
+
250
+ # For whatever reason, Artifactory won't accept "true" - they want a
251
+ # literal "1"...
252
+ params[:remote] = 1 if options[:remote]
253
+
254
+ client.get('/api/search/latestVersion', params)
255
+ rescue Error::NotFound
256
+ nil
257
+ end
258
+
259
+ #
260
+ # Construct an artifact from the given URL.
261
+ #
262
+ # @example Create an artifact object from the given URL
263
+ # Artifact.from_url('/path/to/some.deb') #=> #<Resource::Artifact>
264
+ #
265
+ # @param [Artifactory::Client] client
266
+ # the client object to make the request with
267
+ # @param [String] url
268
+ # the URL to find the artifact from
269
+ #
270
+ # @return [Resource::Artifact]
271
+ #
272
+ def from_url(url, options = {})
273
+ client = extract_client!(options)
274
+ from_hash(client.get(url), client: client)
275
+ end
276
+
277
+ #
278
+ # Create a instance from the given Hash. This method extracts the "safe"
279
+ # information from the hash and adds them to the instance.
280
+ #
281
+ # @example Create a new resource from a hash
282
+ # Artifact.from_hash('downloadUri' => '...', 'size' => '...')
283
+ #
284
+ # @param [Artifactory::Client] client
285
+ # the client object to make the request with
286
+ # @param [Hash] hash
287
+ # the hash to create the instance from
288
+ #
289
+ # @return [Resource::Artifact]
290
+ #
291
+ def from_hash(hash, options = {})
292
+ client = extract_client!(options)
293
+
294
+ new.tap do |instance|
295
+ instance.api_path = hash['uri']
296
+ instance.client = client
297
+ instance.created = Time.parse(hash['created'])
298
+ instance.download_path = hash['downloadUri']
299
+ instance.last_modified = Time.parse(hash['lastModified'])
300
+ instance.last_updated = Time.parse(hash['lastUpdated'])
301
+ instance.md5 = hash['checksums']['md5']
302
+ instance.mime_type = hash['mimeType']
303
+ instance.repo = hash['repo']
304
+ instance.sha1 = hash['checksums']['sha1']
305
+ instance.size = hash['size'].to_i
306
+ end
307
+ end
308
+ end
309
+
310
+ attribute :api_path, ->{ raise 'API path missing!' }
311
+ attribute :created
312
+ attribute :download_path, ->{ raise 'Download path missing!' }
313
+ attribute :last_modified
314
+ attribute :last_updated
315
+ attribute :local_path, ->{ raise 'Local destination missing!' }
316
+ attribute :mime_type
317
+ attribute :md5
318
+ attribute :repo
319
+ attribute :sha1
320
+ attribute :size
321
+
322
+ #
323
+ # @see Artifact#copy_or_move
324
+ #
325
+ def copy(destination, options = {})
326
+ copy_or_move(:copy, destination, options)
327
+ end
328
+
329
+ #
330
+ # Delete this artifact from repository, suppressing any +ResourceNotFound+
331
+ # exceptions might occur.
332
+ #
333
+ # @return [Boolean]
334
+ # true if the object was deleted successfully, false otherwise
335
+ #
336
+ def delete
337
+ !!client.delete(download_path)
338
+ rescue Error::NotFound
339
+ false
340
+ end
341
+
342
+ #
343
+ # @see {Artifact#copy_or_move}
344
+ #
345
+ def move(destination, options = {})
346
+ copy_or_move(:move, destination, options)
347
+ end
348
+
349
+ #
350
+ # The list of properties for this object.
351
+ #
352
+ # @example List all properties for an artifact
353
+ # artifact.properties #=> { 'artifactory.licenses'=>['Apache-2.0'] }
354
+ #
355
+ # @return [Hash<String, Object>]
356
+ # the list of properties
357
+ #
358
+ def properties
359
+ @properties ||= client.get(api_path, properties: nil)['properties']
360
+ end
361
+
362
+ #
363
+ # Get compliance info for a given artifact path. The result includes
364
+ # license and vulnerabilities, if any.
365
+ #
366
+ # **This requires the Black Duck addon to be enabled!**
367
+ #
368
+ # @example Get compliance info for an artifact
369
+ # artifact.compliance #=> { 'licenses' => [{ 'name' => 'LGPL v3' }] }
370
+ #
371
+ # @return [Hash<String, Array<Hash>>]
372
+ #
373
+ def compliance
374
+ @compliance ||= client.get(File.join('/api/compliance', relative_path))
375
+ end
376
+
377
+ #
378
+ # Download the artifact onto the local disk.
379
+ #
380
+ # @example Download an artifact
381
+ # artifact.download #=> /tmp/cache/000adad0-bac/artifact.deb
382
+ #
383
+ # @example Download a remote artifact into a specific target
384
+ # artifact.download('~/Desktop') #=> ~/Desktop/artifact.deb
385
+ #
386
+ # @param [String] target
387
+ # the target directory where the artifact should be downloaded to
388
+ # (defaults to a temporary directory). **It is the user's responsibility
389
+ # to cleanup the temporary directory when finished!**
390
+ # @param [Hash] options
391
+ # @option options [String] filename
392
+ # the name of the file when downloaded to disk (defaults to the basename
393
+ # of the file on the server)
394
+ #
395
+ # @return [String]
396
+ # the path where the file was downloaded on disk
397
+ #
398
+ def download(target = Dir.mktmpdir, options = {})
399
+ target = File.expand_path(target)
400
+
401
+ # Make the directory if it doesn't yet exist
402
+ FileUtils.mkdir_p(target) unless File.exists?(target)
403
+
404
+ # Use the server artifact's filename if one wasn't given
405
+ filename = options[:filename] || File.basename(download_path)
406
+
407
+ # Construct the full path for the file
408
+ destination = File.join(targer, filename)
409
+
410
+ File.open(File.join(destination, filename), 'wb') do |file|
411
+ file.write(_get(download_path))
412
+ end
413
+
414
+ destination
415
+ end
416
+
417
+ private
418
+
419
+ #
420
+ # Helper method for extracting the relative (repo) path, since it's not
421
+ # returned as part of the API.
422
+ #
423
+ # @example Get the relative URI from the resource
424
+ # /libs-release-local/org/acme/artifact.deb
425
+ #
426
+ # @return [String]
427
+ #
428
+ def relative_path
429
+ @relative_path ||= api_path.split('/api/storage', 2).last
430
+ end
431
+
432
+ #
433
+ # Copy or move current artifact to a new destination.
434
+ #
435
+ # @example Move the current artifact to +ext-releases-local+
436
+ # artifact.move(to: '/ext-releaes-local/org/acme')
437
+ # @example Copy the current artifact to +ext-releases-local+
438
+ # artifact.move(to: '/ext-releaes-local/org/acme')
439
+ #
440
+ # @param [Symbol] action
441
+ # the action (+:move+ or +:copy+)
442
+ # @param [String] destination
443
+ # the server-side destination to move or copy the artifact
444
+ # @param [Hash] options
445
+ # the list of options to pass
446
+ #
447
+ # @option options [Boolean] :fail_fast (default: +false+)
448
+ # fail on the first failure
449
+ # @option options [Boolean] :suppress_layouts (default: +false+)
450
+ # suppress cross-layout module path translation during copying or moving
451
+ # @option options [Boolean] :dry_run (default: +false+)
452
+ # pretend to do the copy or move
453
+ #
454
+ # @return [Hash]
455
+ # the parsed JSON response from the server
456
+ #
457
+ def copy_or_move(action, destination, options = {})
458
+ params = {}.tap do |param|
459
+ param[:to] = destination
460
+ param[:failFast] = 1 if options[:fail_fast]
461
+ param[:suppressLayouts] = 1 if options[:suppress_layouts]
462
+ param[:dry] = 1 if options[:dry_run]
463
+ end
464
+
465
+ # Okay, seriously, WTF Artifactory? Are you fucking serious? You want me
466
+ # to make a POST request, but you don't actually read the contents of the
467
+ # POST request, you read the URL-params. Sigh, whoever claimed this was a
468
+ # RESTful API should seriously consider a new occupation.
469
+ params = params.map do |k, v|
470
+ key = URI.escape(k.to_s)
471
+ value = URI.escape(v.to_s)
472
+
473
+ "#{key}=#{value}"
474
+ end
475
+
476
+ endpoint = File.join('/api', action.to_s, relative_path) + '?' + params.join('&')
477
+
478
+ client.post(endpoint)
479
+ end
480
+ end
481
+ end