artifactory 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +11 -7
- data/CHANGELOG.md +18 -0
- data/README.md +25 -4
- data/Rakefile +5 -2
- data/artifactory.gemspec +0 -3
- data/lib/artifactory.rb +2 -4
- data/lib/artifactory/client.rb +210 -85
- data/lib/artifactory/configurable.rb +6 -1
- data/lib/artifactory/defaults.rb +52 -3
- data/lib/artifactory/errors.rb +21 -14
- data/lib/artifactory/resources/artifact.rb +132 -58
- data/lib/artifactory/resources/base.rb +120 -6
- data/lib/artifactory/resources/build.rb +2 -1
- data/lib/artifactory/resources/group.rb +5 -72
- data/lib/artifactory/resources/layout.rb +106 -0
- data/lib/artifactory/resources/repository.rb +62 -114
- data/lib/artifactory/resources/system.rb +5 -1
- data/lib/artifactory/resources/user.rb +5 -79
- data/lib/artifactory/util.rb +8 -2
- data/lib/artifactory/version.rb +1 -1
- data/spec/integration/resources/layout_spec.rb +22 -0
- data/spec/integration/resources/repository_spec.rb +7 -0
- data/spec/integration/resources/system_spec.rb +4 -4
- data/spec/support/api_server/repository_endpoints.rb +5 -0
- data/spec/support/api_server/system_endpoints.rb +18 -0
- data/spec/unit/client_spec.rb +17 -98
- data/spec/unit/resources/artifact_spec.rb +99 -13
- data/spec/unit/resources/build_spec.rb +1 -1
- data/spec/unit/resources/group_spec.rb +1 -1
- data/spec/unit/resources/layout_spec.rb +61 -0
- data/spec/unit/resources/repository_spec.rb +31 -47
- data/spec/unit/resources/system_spec.rb +4 -2
- data/spec/unit/resources/user_spec.rb +1 -1
- metadata +16 -40
- data/locales/en.yml +0 -27
data/lib/artifactory/defaults.rb
CHANGED
@@ -55,12 +55,61 @@ module Artifactory
|
|
55
55
|
end
|
56
56
|
|
57
57
|
#
|
58
|
-
# The HTTP Proxy
|
58
|
+
# The HTTP Proxy server address as a string
|
59
59
|
#
|
60
60
|
# @return [String, nil]
|
61
61
|
#
|
62
|
-
def
|
63
|
-
ENV['
|
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
|
data/lib/artifactory/errors.rb
CHANGED
@@ -1,22 +1,29 @@
|
|
1
1
|
module Artifactory
|
2
2
|
module Error
|
3
|
-
class
|
4
|
-
|
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
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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::
|
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::
|
256
|
+
rescue Error::HTTPError => e
|
257
|
+
raise unless e.code == 404
|
256
258
|
nil
|
257
259
|
end
|
258
260
|
|
259
261
|
#
|
260
|
-
#
|
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
|
-
|
293
|
-
|
294
|
-
|
295
|
-
instance.
|
296
|
-
instance.
|
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 :
|
274
|
+
attribute :uri, ->{ raise 'API path missing!' }
|
275
|
+
attribute :checksums
|
311
276
|
attribute :created
|
312
|
-
attribute :
|
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(
|
338
|
-
rescue Error::
|
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(
|
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(
|
387
|
+
filename = options[:filename] || File.basename(download_uri)
|
406
388
|
|
407
389
|
# Construct the full path for the file
|
408
|
-
destination = File.join(
|
390
|
+
destination = File.join(target, filename)
|
409
391
|
|
410
|
-
File.open(
|
411
|
-
file.write(
|
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 ||=
|
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
|
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
|
-
|
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(', ')}>"
|