artifactory 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +11 -7
  3. data/CHANGELOG.md +18 -0
  4. data/README.md +25 -4
  5. data/Rakefile +5 -2
  6. data/artifactory.gemspec +0 -3
  7. data/lib/artifactory.rb +2 -4
  8. data/lib/artifactory/client.rb +210 -85
  9. data/lib/artifactory/configurable.rb +6 -1
  10. data/lib/artifactory/defaults.rb +52 -3
  11. data/lib/artifactory/errors.rb +21 -14
  12. data/lib/artifactory/resources/artifact.rb +132 -58
  13. data/lib/artifactory/resources/base.rb +120 -6
  14. data/lib/artifactory/resources/build.rb +2 -1
  15. data/lib/artifactory/resources/group.rb +5 -72
  16. data/lib/artifactory/resources/layout.rb +106 -0
  17. data/lib/artifactory/resources/repository.rb +62 -114
  18. data/lib/artifactory/resources/system.rb +5 -1
  19. data/lib/artifactory/resources/user.rb +5 -79
  20. data/lib/artifactory/util.rb +8 -2
  21. data/lib/artifactory/version.rb +1 -1
  22. data/spec/integration/resources/layout_spec.rb +22 -0
  23. data/spec/integration/resources/repository_spec.rb +7 -0
  24. data/spec/integration/resources/system_spec.rb +4 -4
  25. data/spec/support/api_server/repository_endpoints.rb +5 -0
  26. data/spec/support/api_server/system_endpoints.rb +18 -0
  27. data/spec/unit/client_spec.rb +17 -98
  28. data/spec/unit/resources/artifact_spec.rb +99 -13
  29. data/spec/unit/resources/build_spec.rb +1 -1
  30. data/spec/unit/resources/group_spec.rb +1 -1
  31. data/spec/unit/resources/layout_spec.rb +61 -0
  32. data/spec/unit/resources/repository_spec.rb +31 -47
  33. data/spec/unit/resources/system_spec.rb +4 -2
  34. data/spec/unit/resources/user_spec.rb +1 -1
  35. metadata +16 -40
  36. data/locales/en.yml +0 -27
@@ -15,7 +15,12 @@ module Artifactory
15
15
  :endpoint,
16
16
  :username,
17
17
  :password,
18
- :proxy,
18
+ :proxy_address,
19
+ :proxy_password,
20
+ :proxy_port,
21
+ :proxy_username,
22
+ :ssl_pem_file,
23
+ :ssl_verify,
19
24
  :user_agent,
20
25
  ]
21
26
  end
@@ -55,12 +55,61 @@ module Artifactory
55
55
  end
56
56
 
57
57
  #
58
- # The HTTP Proxy information as a string
58
+ # The HTTP Proxy server address as a string
59
59
  #
60
60
  # @return [String, nil]
61
61
  #
62
- def proxy
63
- ENV['ARTIFACTORY_PROXY']
62
+ def proxy_address
63
+ ENV['ARTIFACTORY_PROXY_ADDRESS']
64
+ end
65
+
66
+ #
67
+ # The HTTP Proxy user password as a string
68
+ #
69
+ # @return [String, nil]
70
+ #
71
+ def proxy_password
72
+ ENV['ARTIFACTORY_PROXY_PASSWORD']
73
+ end
74
+
75
+ #
76
+ # The HTTP Proxy server port as a string
77
+ #
78
+ # @return [String, nil]
79
+ #
80
+ def proxy_port
81
+ ENV['ARTIFACTORY_PROXY_PORT']
82
+ end
83
+
84
+ #
85
+ # The HTTP Proxy server username as a string
86
+ #
87
+ # @return [String, nil]
88
+ #
89
+ def proxy_username
90
+ ENV['ARTIFACTORY_PROXY_USERNAME']
91
+ end
92
+
93
+ #
94
+ # The path to a pem file on disk for use with a custom SSL verification
95
+ #
96
+ # @return [String, nil]
97
+ #
98
+ def ssl_pem_file
99
+ ENV['ARTIFACTORY_SSL_PEM_FILE']
100
+ end
101
+
102
+ #
103
+ # Verify SSL requests (default: true)
104
+ #
105
+ # @return [true, false]
106
+ #
107
+ def ssl_verify
108
+ if ENV['ARTIFACTORY_SSL_VERIFY'].nil?
109
+ true
110
+ else
111
+ %w[t y].include?(ENV['ARTIFACTORY_SSL_VERIFY'].downcase[0])
112
+ end
64
113
  end
65
114
  end
66
115
  end
@@ -1,22 +1,29 @@
1
1
  module Artifactory
2
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)
3
+ # Base class for all errors
4
+ class ArtifactoryError < StandardError; end
7
5
 
8
- super I18n.t("artifactory.errors.#{error_key}", options)
6
+ # Class for all HTTP errors
7
+ class HTTPError < ArtifactoryError
8
+ attr_reader :code
9
+ attr_reader :message
10
+
11
+ def initialize(hash = {})
12
+ @code = hash['status'].to_i
13
+ @http = hash['message'].to_s
14
+
15
+ super "The Artifactory server responded with an HTTP Error "\
16
+ "#{@code}: `#{@http}'"
9
17
  end
10
18
  end
11
19
 
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
20
+ # A general connection error with a more informative message
21
+ class ConnectionError < ArtifactoryError
22
+ def initialize(endpoint)
23
+ super "The Artifactory server at `#{endpoint}' is not currently " \
24
+ "accepting connections. Please ensure that the server is " \
25
+ "running an that your authentication information is correct."
26
+ end
27
+ end
21
28
  end
22
29
  end
@@ -198,7 +198,8 @@ module Artifactory
198
198
  format_repos!(params)
199
199
 
200
200
  client.get('/api/search/versions', params)['results']
201
- rescue Error::NotFound
201
+ rescue Error::HTTPError => e
202
+ raise unless e.code == 404
202
203
  []
203
204
  end
204
205
 
@@ -252,73 +253,54 @@ module Artifactory
252
253
  params[:remote] = 1 if options[:remote]
253
254
 
254
255
  client.get('/api/search/latestVersion', params)
255
- rescue Error::NotFound
256
+ rescue Error::HTTPError => e
257
+ raise unless e.code == 404
256
258
  nil
257
259
  end
258
260
 
259
261
  #
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]
262
+ # @see Artifactory::Resource::Base.from_hash
290
263
  #
291
264
  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
265
+ super.tap do |instance|
266
+ instance.created = Time.parse(instance.created) rescue nil
267
+ instance.last_modified = Time.parse(instance.last_modified) rescue nil
268
+ instance.last_updated = Time.parse(instance.last_updated) rescue nil
269
+ instance.size = instance.size.to_i
306
270
  end
307
271
  end
308
272
  end
309
273
 
310
- attribute :api_path, ->{ raise 'API path missing!' }
274
+ attribute :uri, ->{ raise 'API path missing!' }
275
+ attribute :checksums
311
276
  attribute :created
312
- attribute :download_path, ->{ raise 'Download path missing!' }
277
+ attribute :download_uri, ->{ raise 'Download URI missing!' }
278
+ attribute :key
313
279
  attribute :last_modified
314
280
  attribute :last_updated
315
281
  attribute :local_path, ->{ raise 'Local destination missing!' }
316
282
  attribute :mime_type
317
- attribute :md5
318
283
  attribute :repo
319
- attribute :sha1
320
284
  attribute :size
321
285
 
286
+ #
287
+ # The SHA of this artifact.
288
+ #
289
+ # @return [String]
290
+ #
291
+ def sha1
292
+ checksums && checksums['sha1']
293
+ end
294
+
295
+ #
296
+ # The MD5 of this artifact.
297
+ #
298
+ # @return [String]
299
+ #
300
+ def md5
301
+ checksums && checksums['md5']
302
+ end
303
+
322
304
  #
323
305
  # @see Artifact#copy_or_move
324
306
  #
@@ -334,8 +316,8 @@ module Artifactory
334
316
  # true if the object was deleted successfully, false otherwise
335
317
  #
336
318
  def delete
337
- !!client.delete(download_path)
338
- rescue Error::NotFound
319
+ !!client.delete(download_uri)
320
+ rescue Error::HTTPError
339
321
  false
340
322
  end
341
323
 
@@ -356,7 +338,7 @@ module Artifactory
356
338
  # the list of properties
357
339
  #
358
340
  def properties
359
- @properties ||= client.get(api_path, properties: nil)['properties']
341
+ @properties ||= client.get(uri, properties: nil)['properties']
360
342
  end
361
343
 
362
344
  #
@@ -402,18 +384,110 @@ module Artifactory
402
384
  FileUtils.mkdir_p(target) unless File.exists?(target)
403
385
 
404
386
  # Use the server artifact's filename if one wasn't given
405
- filename = options[:filename] || File.basename(download_path)
387
+ filename = options[:filename] || File.basename(download_uri)
406
388
 
407
389
  # Construct the full path for the file
408
- destination = File.join(targer, filename)
390
+ destination = File.join(target, filename)
409
391
 
410
- File.open(File.join(destination, filename), 'wb') do |file|
411
- file.write(_get(download_path))
392
+ File.open(destination, 'wb') do |file|
393
+ file.write(client.get(download_uri))
412
394
  end
413
395
 
414
396
  destination
415
397
  end
416
398
 
399
+ #
400
+ # Upload an artifact into the repository. If the first parameter is a File
401
+ # object, that file descriptor is passed to the uploader. If the first
402
+ # parameter is a string, it is assumed to be the path to a local file on
403
+ # disk. This method will automatically construct the File object from the
404
+ # given path.
405
+ #
406
+ # @see bit.ly/1dhJRMO Artifactory Matrix Properties
407
+ #
408
+ # @example Upload an artifact from a File instance
409
+ # file = File.new('/local/path/to/file.deb')
410
+ # artifact = Artifact.new
411
+ # artifact.upload(file, 'libs-release-local', file.deb')
412
+ #
413
+ # @example Upload an artifact from a path
414
+ # artifact.upload('/local/path/to/file.deb', 'libs-release-local', 'file.deb')
415
+ #
416
+ # @example Upload an artifact with matrix properties
417
+ # artifact.upload('/local/path/to/file.deb', 'libs-release-local', file.deb', {
418
+ # status: 'DEV',
419
+ # rating: 5,
420
+ # branch: 'master'
421
+ # })
422
+ #
423
+ # @param [String] key
424
+ # the key of the repository to which to upload the file
425
+ # @param [String, File] path_or_io
426
+ # the file or path to the file to upload
427
+ # @param [String] path
428
+ # the path where this resource will live in the remote artifactory
429
+ # repository, relative to the repository key
430
+ # @param [Hash] headers
431
+ # the list of headers to send with the request
432
+ # @param [Hash] properties
433
+ # a list of matrix properties
434
+ #
435
+ # @return [Resource::Artifact]
436
+ #
437
+ def upload(key, path_or_io, path, properties = {}, headers = {})
438
+ file = if respond_to?(:read)
439
+ path_or_io
440
+ else
441
+ File.new(File.expand_path(path_or_io))
442
+ end
443
+
444
+ matrix = to_matrix_properties(properties)
445
+ endpoint = File.join("#{url_safe(key)}#{matrix}", path)
446
+
447
+ response = client.put(endpoint, file, headers)
448
+ self.class.from_hash(response)
449
+ end
450
+
451
+ #
452
+ # Upload an artifact with the given SHA checksum. Consult the artifactory
453
+ # documentation for the possible responses when the checksums fail to
454
+ # match.
455
+ #
456
+ # @see Artifact#upload More syntax examples
457
+ #
458
+ # @example Upload an artifact with a checksum
459
+ # artifact = Artifact.new
460
+ # artifact.upload_with_checksum('/local/file', 'libs-release-local', /remote/path', 'ABCD1234')
461
+ #
462
+ # @param (see Artifact#upload)
463
+ # @param [String] checksum
464
+ # the SHA1 checksum of the artifact to upload
465
+ #
466
+ def upload_with_checksum(key, path_or_io, path, checksum, properties = {})
467
+ upload(key, path_or_io, path, properties,
468
+ 'X-Checksum-Deploy' => true,
469
+ 'X-Checksum-Sha1' => checksum,
470
+ )
471
+ end
472
+
473
+ #
474
+ # Upload an artifact with the given archive. Consult the artifactory
475
+ # documentation for the format of the archive to upload.
476
+ #
477
+ # @see Artifact#upload More syntax examples
478
+ #
479
+ # @example Upload an artifact with a checksum
480
+ # artifact = Artifact.new('libs-release-local')
481
+ # artifact.upload_from_archive('/local/archive', '/remote/path')#
482
+ #
483
+ # @param (see Repository#upload)
484
+ #
485
+ def upload_from_archive(key, path_or_io, path, properties = {})
486
+ upload(key, path_or_io, path, properties,
487
+ 'X-Explode-Archive' => true,
488
+ )
489
+ end
490
+
417
491
  private
418
492
 
419
493
  #
@@ -426,7 +500,7 @@ module Artifactory
426
500
  # @return [String]
427
501
  #
428
502
  def relative_path
429
- @relative_path ||= api_path.split('/api/storage', 2).last
503
+ @relative_path ||= uri.split('/api/storage', 2).last
430
504
  end
431
505
 
432
506
  #
@@ -27,7 +27,7 @@ module Artifactory
27
27
  def attribute(key, default = nil)
28
28
  key = key.to_sym unless key.is_a?(Symbol)
29
29
 
30
- # Set the key on the top attributes, mostly for asthetic purposes
30
+ # Set this attribute in the top-level hash
31
31
  attributes[key] = nil
32
32
 
33
33
  define_method(key) do
@@ -52,6 +52,73 @@ module Artifactory
52
52
  end
53
53
  end
54
54
 
55
+ #
56
+ # The list of attributes defined by this class.
57
+ #
58
+ # @return [Array<Symbol>]
59
+ #
60
+ def attributes
61
+ @attributes ||= {}
62
+ end
63
+
64
+ #
65
+ # Determine if this class has a given attribute.
66
+ #
67
+ # @param [#to_sym] key
68
+ # the key to check as an attribute
69
+ #
70
+ # @return [true, false]
71
+ #
72
+ def has_attribute?(key)
73
+ attributes.has_key?(key.to_sym)
74
+ end
75
+
76
+ #
77
+ # Construct a new object from the given URL.
78
+ #
79
+ # @param [String] url
80
+ # the URL to find the user from
81
+ # @param [Hash] options
82
+ # the list of options
83
+ #
84
+ # @option options [Artifactory::Client] :client
85
+ # the client object to make the request with
86
+ #
87
+ # @return [~Resource::Base]
88
+ #
89
+ def from_url(url, options = {})
90
+ client = extract_client!(options)
91
+ from_hash(client.get(url), client: client)
92
+ end
93
+
94
+ #
95
+ # Construct a new object from the hash.
96
+ #
97
+ # @param [Hash] hash
98
+ # the hash to create the object with
99
+ # @param [Hash] options
100
+ # the list options
101
+ #
102
+ # @option options [Artifactory::Client] :client
103
+ # the client object to make the request with
104
+ #
105
+ # @return [~Resource::Base]
106
+ #
107
+ def from_hash(hash, options = {})
108
+ instance = new
109
+ instance.client = extract_client!(options)
110
+
111
+ hash.inject(instance) do |instance, (key, value)|
112
+ method = :"#{Util.underscore(key)}="
113
+
114
+ if instance.respond_to?(method)
115
+ instance.send(method, value)
116
+ end
117
+
118
+ instance
119
+ end
120
+ end
121
+
55
122
  #
56
123
  # Get the client (connection) object from the given options. If the
57
124
  # +:client+ key is preset in the hash, it is assumed to contain the
@@ -103,10 +170,6 @@ module Artifactory
103
170
  def url_safe(value)
104
171
  URI.escape(value.to_s)
105
172
  end
106
-
107
- def attributes
108
- @attributes ||= {}
109
- end
110
173
  end
111
174
 
112
175
  attribute :client, ->{ Artifactory.client }
@@ -159,6 +222,55 @@ module Artifactory
159
222
  self.class.url_safe(value)
160
223
  end
161
224
 
225
+ #
226
+ # The hash representation
227
+ #
228
+ # @example An example hash response
229
+ # { 'key' => 'local-repo1', 'includesPattern' => '**/*' }
230
+ #
231
+ # @return [Hash]
232
+ #
233
+ def to_hash
234
+ attributes.inject({}) do |hash, (key, value)|
235
+ unless Resource::Base.has_attribute?(key)
236
+ hash[Util.camelize(key, true)] = value
237
+ end
238
+
239
+ hash
240
+ end
241
+ end
242
+
243
+ #
244
+ # The JSON representation of this object.
245
+ #
246
+ # @see Artifactory::Resource::Base#to_json
247
+ #
248
+ # @return [String]
249
+ #
250
+ def to_json
251
+ JSON.fast_generate(to_hash)
252
+ end
253
+
254
+ #
255
+ # Create URI-escaped string from matrix properties
256
+ #
257
+ # @see http://bit.ly/1qeVYQl
258
+ #
259
+ def to_matrix_properties(hash = {})
260
+ properties = hash.map do |k, v|
261
+ key = URI.escape(k.to_s)
262
+ value = URI.escape(v.to_s)
263
+
264
+ "#{key}=#{value}"
265
+ end
266
+
267
+ if properties.empty?
268
+ nil
269
+ else
270
+ ";#{properties.join(';')}"
271
+ end
272
+ end
273
+
162
274
  # @private
163
275
  def to_s
164
276
  "#<#{short_classname}>"
@@ -167,7 +279,9 @@ module Artifactory
167
279
  # @private
168
280
  def inspect
169
281
  list = attributes.collect do |key, value|
170
- "#{key}: #{value.inspect}" unless key == :client
282
+ unless Resource::Base.has_attribute?(key)
283
+ "#{key}: #{value.inspect}"
284
+ end
171
285
  end.compact
172
286
 
173
287
  "#<#{short_classname} #{list.join(', ')}>"