artifactory 2.2.1 → 2.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eb987fd18b2034340daf17e7f5d6d06b7289966f
4
- data.tar.gz: 5315895f5ab0badfee1351eecfd7d80a701d509c
3
+ metadata.gz: f36b33be0b821d20e678a3f104b7fd24a41ebd93
4
+ data.tar.gz: 144d1a14596dd383816dbb47158249a578cf31af
5
5
  SHA512:
6
- metadata.gz: 56040d7854457cbd0d86712e1ad7e096e539409012ea8e904fe4eda9b8d18ee7f4ad4d31f5f45ec12636c7f8f9c734b40fcbf9e45555d50660b9689db801f993
7
- data.tar.gz: 93bdb1b294a58cb73d6bfbcb118090828b25e1dd7ee0cf5e92a00850b6850f16bdf22248a900a62a6338e03b5e3425da78500b047a60157b61d1b106d4fa2402
6
+ metadata.gz: d85c333c34fa8fcb273ccf4a8fee6598f8e748c310c6f8de1b75c57c793994e5046ff98d37983ecd771a311d81c6c65e92df25b493a82e273a20e237cf7b1720
7
+ data.tar.gz: 9ea0a6be40e91885a8a1ea63b62a4fa2032c4e751f8536b325e60e7eea783464e2930fdd5821e37110de890952fc685f659ea13b800827dae4d2360ad2bc8cf0
@@ -3,6 +3,10 @@ Artifactory Client CHANGELOG
3
3
  This file is used to document the changes between releases of the Artifactory
4
4
  Ruby client.
5
5
 
6
+ v2.3.0 (08-04-2015)
7
+ -------------------
8
+ - Support for Build endpoints
9
+
6
10
  v2.2.1 (12-16-2014)
7
11
  -------------------
8
12
  - provide data to post in `Artifact#copy_or_move`
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  Artifactory Client
2
2
  ==================
3
- [![Build Status](https://secure.travis-ci.org/opscode/artifactory-client.png?branch=master)](http://travis-ci.org/opscode/artifactory-client)
3
+ [![Build Status](https://secure.travis-ci.org/chef/artifactory-client.png?branch=master)](http://travis-ci.org/chef/artifactory-client)
4
4
 
5
5
  A Ruby client and interface to the Artifactory API. **The majority of API endpoints are only exposed for Artifactory Pro customers!** As such, many of the resources and actions exposed by this gem also require Artifactory Pro.
6
6
 
@@ -121,8 +121,36 @@ artifact.delete #=> true
121
121
 
122
122
  #### Builds
123
123
  ```ruby
124
- # Show all builds
125
- Build.all #=> [#<Build ...>]
124
+ # Show all components
125
+ BuildComponent.all #=> [#<BuildComponent ...>]
126
+
127
+ # Show all builds for a components
128
+ Build.all('wicket') #=> [#<Build ...>]
129
+
130
+ # Find a build component by name
131
+ component = BuildComponent.find('wicket')
132
+
133
+ # Delete some builds for a component
134
+ component.delete(build_numbers: %w( 51 52)) #=> true
135
+
136
+ # Delete all builds for a component
137
+ component.delete(delete_all: true) #=> true
138
+
139
+ # Delete a component and all of its associated data (including artifacts)
140
+ component.delete(artifacts: true, delete_all: true) #=> true
141
+
142
+ # Get a list of all buld records for a component
143
+ component.builds #=> #=> [#<Artifactory::Resource::Build ...>, ...]
144
+
145
+ # Create a new build record
146
+ build = Build.new(name: 'fricket', number: '51', properties: {...}, modules: [...])
147
+ build.save
148
+
149
+ # Find a build
150
+ build = Build.find('wicket', '51')
151
+
152
+ # Promote a build
153
+ build.promote('libs-release-local', status: 'staged', comment: 'Tested on all target platforms.')
126
154
  ```
127
155
 
128
156
  #### Plugins
data/Rakefile CHANGED
@@ -1,8 +1,12 @@
1
1
  require 'bundler/gem_tasks'
2
2
 
3
3
  require 'rspec/core/rake_task'
4
- RSpec::Core::RakeTask.new(:integration)
5
- RSpec::Core::RakeTask.new(:unit)
4
+ RSpec::Core::RakeTask.new(:integration) do |t|
5
+ t.rspec_opts = "--tag integration"
6
+ end
7
+ RSpec::Core::RakeTask.new(:unit) do |t|
8
+ t.rspec_opts = "--tag ~integration"
9
+ end
6
10
 
7
11
  namespace :travis do
8
12
  desc 'Run tests on Travis'
@@ -27,6 +27,7 @@ module Artifactory
27
27
  module Collection
28
28
  autoload :Artifact, 'artifactory/collections/artifact'
29
29
  autoload :Base, 'artifactory/collections/base'
30
+ autoload :Build, 'artifactory/collections/build'
30
31
  end
31
32
 
32
33
  module Resource
@@ -34,6 +35,7 @@ module Artifactory
34
35
  autoload :Backup, 'artifactory/resources/backup'
35
36
  autoload :Base, 'artifactory/resources/base'
36
37
  autoload :Build, 'artifactory/resources/build'
38
+ autoload :BuildComponent, 'artifactory/resources/build_component'
37
39
  autoload :Group, 'artifactory/resources/group'
38
40
  autoload :Layout, 'artifactory/resources/layout'
39
41
  autoload :LDAPSetting, 'artifactory/resources/ldap_setting'
@@ -0,0 +1,28 @@
1
+ #
2
+ # Copyright 2015 Chef Software, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ module Artifactory
18
+ class Collection::Build < Collection::Base
19
+ #
20
+ # Create a new build collection.
21
+ #
22
+ # @param (see Collection::Base#initialize)
23
+ #
24
+ def initialize(parent, options = {}, &block)
25
+ super(Resource::Build, parent, options, &block)
26
+ end
27
+ end
28
+ end
@@ -40,5 +40,19 @@ module Artifactory
40
40
  "running an that your authentication information is correct."
41
41
  end
42
42
  end
43
+
44
+ # A general connection error with a more informative message
45
+ class InvalidBuildType < ArtifactoryError
46
+ def initialize(given_type)
47
+ super <<-EOH
48
+ '#{given_type}' is not a valid build type.
49
+
50
+ Valid build types include:
51
+
52
+ #{Resource::Build::BUILD_TYPES.join("\n ")}"
53
+
54
+ EOH
55
+ end
56
+ end
43
57
  end
44
58
  end
@@ -663,18 +663,7 @@ module Artifactory
663
663
  param[:dry] = 1 if options[:dry_run]
664
664
  end
665
665
 
666
- # Okay, seriously, WTF Artifactory? Are you fucking serious? You want me
667
- # to make a POST request, but you don't actually read the contents of the
668
- # POST request, you read the URL-params. Sigh, whoever claimed this was a
669
- # RESTful API should seriously consider a new occupation.
670
- params = params.map do |k, v|
671
- key = URI.escape(k.to_s)
672
- value = URI.escape(v.to_s)
673
-
674
- "#{key}=#{value}"
675
- end
676
-
677
- endpoint = File.join('/api', action.to_s, relative_path) + '?' + params.join('&')
666
+ endpoint = File.join('/api', action.to_s, relative_path) + "?#{to_query_string_parameters(params)}"
678
667
 
679
668
  client.post(endpoint, {})
680
669
  end
@@ -297,7 +297,7 @@ module Artifactory
297
297
  def to_hash
298
298
  attributes.inject({}) do |hash, (key, value)|
299
299
  unless Resource::Base.has_attribute?(key)
300
- hash[Util.camelize(key, true)] = value
300
+ hash[Util.camelize(key, true)] = send(key.to_sym)
301
301
  end
302
302
 
303
303
  hash
@@ -316,7 +316,7 @@ module Artifactory
316
316
  end
317
317
 
318
318
  #
319
- # Create URI-escaped string from matrix properties
319
+ # Create CGI-escaped string from matrix properties
320
320
  #
321
321
  # @see http://bit.ly/1qeVYQl
322
322
  #
@@ -335,6 +335,26 @@ module Artifactory
335
335
  end
336
336
  end
337
337
 
338
+ #
339
+ # Create URI-escaped querystring parameters
340
+ #
341
+ # @see http://bit.ly/1qeVYQl
342
+ #
343
+ def to_query_string_parameters(hash = {})
344
+ properties = hash.map do |k, v|
345
+ key = URI.escape(k.to_s)
346
+ value = URI.escape(v.to_s)
347
+
348
+ "#{key}=#{value}"
349
+ end
350
+
351
+ if properties.empty?
352
+ nil
353
+ else
354
+ properties.join('&')
355
+ end
356
+ end
357
+
338
358
  # @private
339
359
  def to_s
340
360
  "#<#{short_classname}>"
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2014 Chef Software, Inc.
2
+ # Copyright 2015 Chef Software, Inc.
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -14,12 +14,20 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
+ require 'time'
18
+
17
19
  module Artifactory
18
20
  class Resource::Build < Resource::Base
21
+ BUILD_SCHEMA_VERSION = '1.0.1'.freeze
22
+ # valid build types as dictated by the Artifactory API
23
+ BUILD_TYPES = %w( ANT IVY MAVEN GENERIC GRADLE )
24
+
19
25
  class << self
20
26
  #
21
27
  # Search for all builds in the system.
22
28
  #
29
+ # @param [String] name
30
+ # the name of the build component
23
31
  # @param [Hash] options
24
32
  # the list of options
25
33
  #
@@ -29,9 +37,13 @@ module Artifactory
29
37
  # @return [Array<Resource::Build>]
30
38
  # the list of builds
31
39
  #
32
- def all(options = {})
40
+ def all(name, options = {})
33
41
  client = extract_client!(options)
34
- client.get('/api/build')
42
+ client.get("/api/build/#{url_safe(name)}")['buildsNumbers'].map do |build_number|
43
+ # Remove the leading / from the `uri` value. Converts `/484` to `484`.
44
+ number = build_number['uri'].slice(1..-1)
45
+ find(name, number, client: client)
46
+ end.compact.flatten
35
47
  rescue Error::HTTPError => e
36
48
  # Artifactory returns a 404 instead of an empty list when there are no
37
49
  # builds. Whoever decided that was a good idea clearly doesn't
@@ -39,6 +51,179 @@ module Artifactory
39
51
  raise unless e.code == 404
40
52
  []
41
53
  end
54
+
55
+ #
56
+ # Find (fetch) data for a particular build of a component
57
+ #
58
+ # @example Find data for a build of a component
59
+ # Build.find('wicket', 25) #=> #<Build name: 'wicket' ...>
60
+ #
61
+ # @param [String] name
62
+ # the name of the build component
63
+ # @param [String] number
64
+ # the number of the build
65
+ # @param [Hash] options
66
+ # the list of options
67
+ #
68
+ # @option options [Artifactory::Client] :client
69
+ # the client object to make the request with
70
+ #
71
+ # @return [Resource::Build, nil]
72
+ # an instance of the build that matches the given name/number
73
+ # combination, or +nil+ if one does not exist
74
+ #
75
+ def find(name, number, options = {})
76
+ client = extract_client!(options)
77
+ response = client.get("/api/build/#{url_safe(name)}/#{url_safe(number)}")
78
+ from_hash(response['buildInfo'], client: client)
79
+ rescue Error::HTTPError => e
80
+ raise unless e.code == 404
81
+ nil
82
+ end
83
+
84
+ #
85
+ # @see Artifactory::Resource::Base.from_hash
86
+ #
87
+ def from_hash(hash, options = {})
88
+ super.tap do |instance|
89
+ instance.started = Time.parse(instance.started) rescue nil
90
+ instance.duration_millis = instance.duration_millis.to_i
91
+ end
92
+ end
93
+ end
94
+
95
+ # Based on https://github.com/JFrogDev/build-info/blob/master/README.md#build-info-json-format
96
+ attribute :properties, {}
97
+ attribute :version, BUILD_SCHEMA_VERSION
98
+ attribute :name, ->{ raise 'Build component missing!' }
99
+ attribute :number, ->{ raise 'Build number missing!' }
100
+ attribute :type, 'GENERIC'
101
+ attribute :build_agent, {}
102
+ attribute :agent, {}
103
+ attribute :started, Time.now.utc.iso8601(3)
104
+ attribute :duration_millis
105
+ attribute :artifactory_principal
106
+ attribute :url
107
+ attribute :vcs_revision
108
+ attribute :vcs_url
109
+ attribute :license_control, {}
110
+ attribute :build_retention, {}
111
+ attribute :modules, []
112
+ attribute :governance
113
+
114
+ #
115
+ # Compare a build artifacts/dependencies/environment with an older
116
+ # build to see what has changed (new artifacts added, old dependencies
117
+ # deleted etc).
118
+ #
119
+ # @example List all properties for an artifact
120
+ # build.diff(35) #=> { 'artifacts'=>{}, 'dependencies'=>{}, 'properties'=>{} }
121
+ #
122
+ # @param [String] previous_build_number
123
+ # the number of the previous build to compare against
124
+ #
125
+ # @return [Hash<String, Hash>]
126
+ # the list of properties
127
+ #
128
+ def diff(previous_build_number)
129
+ endpoint = api_path + '?' "diff=#{url_safe(previous_build_number)}"
130
+ client.get(endpoint, {})
131
+ end
132
+
133
+ #
134
+ # Move a build's artifacts to a new repository optionally moving or
135
+ # copying the build's dependencies to the target repository
136
+ # and setting properties on promoted artifacts.
137
+ #
138
+ # @example promote the build to 'omnibus-stable-local'
139
+ # build.promote('omnibus-stable-local')
140
+ # @example promote a build attaching some new properites
141
+ # build.promote('omnibus-stable-local'
142
+ # properties: {
143
+ # 'promoted_by' => 'hipchat:schisamo@chef.io'
144
+ # }
145
+ # )
146
+ #
147
+ # @param [String] target_repo
148
+ # repository to move or copy the build's artifacts and/or dependencies
149
+ # @param [Hash] options
150
+ # the list of options to pass
151
+ #
152
+ # @option options [String] :status (default: 'promoted')
153
+ # new build status (any string)
154
+ # @option options [String] :comment (default: '')
155
+ # an optional comment describing the reason for promotion
156
+ # @option options [String] :user (default: +Artifactory.username+)
157
+ # the user that invoked promotion
158
+ # @option options [Boolean] :dry_run (default: +false+)
159
+ # pretend to do the promotion
160
+ # @option options [Boolean] :copy (default: +false+)
161
+ # whether to copy instead of move
162
+ # @option options [Boolean] :dependencies (default: +false+)
163
+ # whether to move/copy the build's dependencies
164
+ # @option options [Array] :scopes (default: [])
165
+ # an array of dependency scopes to include when "dependencies" is true
166
+ # @option options [Hash<String, Array<String>>] :properties (default: [])
167
+ # a list of properties to attach to the build's artifacts
168
+ # @option options [Boolean] :fail_fast (default: +true+)
169
+ # fail and abort the operation upon receiving an error
170
+ #
171
+ # @return [Hash]
172
+ # the parsed JSON response from the server
173
+ #
174
+ def promote(target_repo, options = {})
175
+ request_body = {}.tap do |body|
176
+ body[:status] = options[:status] || 'promoted'
177
+ body[:comment] = options[:comment] || ''
178
+ body[:ciUser] = options[:user] || Artifactory.username
179
+ body[:dryRun] = options[:dry_run] || false
180
+ body[:targetRepo] = target_repo
181
+ body[:copy] = options[:copy] || false
182
+ body[:artifacts] = true # always move/copy the build's artifacts
183
+ body[:dependencies] = options[:dependencies] || false
184
+ body[:scopes] = options[:scopes] || []
185
+ body[:properties] = options[:properties] || {}
186
+ body[:failFast] = options[:fail_fast] || true
187
+ end
188
+
189
+ endpoint = "/api/build/promote/#{url_safe(name)}/#{url_safe(number)}"
190
+ client.post(endpoint, JSON.fast_generate(request_body),
191
+ 'Content-Type' => 'application/json'
192
+ )
193
+ end
194
+
195
+ #
196
+ # Creates data about a build.
197
+ #
198
+ # @return [Boolean]
199
+ #
200
+ def save
201
+ raise Error::InvalidBuildType.new(type) unless BUILD_TYPES.include?(type)
202
+
203
+ file = Tempfile.new("build.json")
204
+ file.write(to_json)
205
+ file.rewind
206
+
207
+ client.put('/api/build', file,
208
+ 'Content-Type' => 'application/json'
209
+ )
210
+ true
211
+ ensure
212
+ if file
213
+ file.close
214
+ file.unlink
215
+ end
216
+ end
217
+
218
+ private
219
+
220
+ #
221
+ # The path to this build on the server.
222
+ #
223
+ # @return [String]
224
+ #
225
+ def api_path
226
+ "/api/build/#{url_safe(name)}/#{url_safe(number)}"
42
227
  end
43
228
  end
44
229
  end
@@ -0,0 +1,160 @@
1
+ #
2
+ # Copyright 2015 Chef Software, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'time'
18
+
19
+ module Artifactory
20
+ class Resource::BuildComponent < Resource::Base
21
+ class << self
22
+ #
23
+ # Search for all compoenents for which build data exists.
24
+ #
25
+ # @param [Hash] options
26
+ # the list of options
27
+ #
28
+ # @option options [Artifactory::Client] :client
29
+ # the client object to make the request with
30
+ #
31
+ # @return [Array<Resource::BuildComponent>]
32
+ # the list of builds
33
+ #
34
+ def all(options = {})
35
+ client = extract_client!(options)
36
+ client.get('/api/build')['builds'].map do |component|
37
+ from_hash(component, client: client)
38
+ end.compact.flatten
39
+ rescue Error::HTTPError => e
40
+ # Artifactory returns a 404 instead of an empty list when there are no
41
+ # builds. Whoever decided that was a good idea clearly doesn't
42
+ # understand the point of REST interfaces...
43
+ raise unless e.code == 404
44
+ []
45
+ end
46
+
47
+ #
48
+ # Find (fetch) data for a particular build component
49
+ #
50
+ # @example Find a particular build component
51
+ # BuildComponent.find('wicket') #=> #<BuildComponent name: 'wicket' ...>
52
+ #
53
+ # @param [String] name
54
+ # the name of the build component
55
+ # @param [Hash] options
56
+ # the list of options
57
+ #
58
+ # @option options [Artifactory::Client] :client
59
+ # the client object to make the request with
60
+ #
61
+ # @return [Resource::BuildComponent, nil]
62
+ # an instance of the build component that matches the given name,
63
+ # or +nil+ if one does not exist
64
+ #
65
+ def find(name, options = {})
66
+ client = extract_client!(options)
67
+ all.find do |component|
68
+ component.name == name
69
+ end
70
+ end
71
+
72
+ #
73
+ # @see Artifactory::Resource::Base.from_hash
74
+ #
75
+ def from_hash(hash, options = {})
76
+ super.tap do |instance|
77
+ # Remove the leading / from the `uri` value. Converts `/foo` to `foo`.
78
+ instance.name = instance.uri.slice(1..-1)
79
+ instance.last_started = Time.parse(instance.last_started) rescue nil
80
+ end
81
+ end
82
+ end
83
+
84
+ attribute :uri
85
+ attribute :name, ->{ raise 'Name missing!' }
86
+ attribute :last_started
87
+
88
+ #
89
+ # The list of build data for this component.
90
+ #
91
+ # @example Get the list of artifacts for a repository
92
+ # component = BuildComponent.new(name: 'wicket')
93
+ # component.builds #=> [#<Resource::Build>, ...]
94
+ #
95
+ # @return [Collection::Build]
96
+ # the list of builds
97
+ #
98
+ def builds
99
+ @builds ||= Collection::Build.new(self, name: name) do
100
+ Resource::Build.all(name)
101
+ end
102
+ end
103
+
104
+ #
105
+ # Remove this component's build data stored in Artifactory
106
+ #
107
+ # @option options [Array<String>] :build_numbers (default: nil)
108
+ # an array of build numbers that should be deleted; if not given
109
+ # all builds (for this component) are deleted
110
+ # @option options [Boolean] :artifacts (default: +false+)
111
+ # if true the component's artifacts are also removed
112
+ # @option options [Boolean] :delete_all (default: +false+)
113
+ # if true the entire component is removed
114
+ #
115
+ # @return [Boolean]
116
+ # true if the object was deleted successfully, false otherwise
117
+ #
118
+ def delete(options = {})
119
+ params = {}.tap do |param|
120
+ param[:buildNumbers] = options[:build_numbers].join(',') if options[:build_numbers]
121
+ param[:artifacts] = 1 if options[:artifacts]
122
+ param[:deleteAll] = 1 if options[:delete_all]
123
+ end
124
+
125
+ endpoint = api_path + "?#{to_query_string_parameters(params)}"
126
+ client.delete(endpoint, {})
127
+ true
128
+ rescue Error::HTTPError => e
129
+ false
130
+ end
131
+
132
+ #
133
+ # Rename a build component.
134
+ #
135
+ # @param [String] new_name
136
+ # new name for the component
137
+ #
138
+ # @return [Boolean]
139
+ # true if the object was renamed successfully, false otherwise
140
+ #
141
+ def rename(new_name, options = {})
142
+ endpoint = "/api/build/rename/#{url_safe(name)}" + "?to=#{new_name}"
143
+ client.post(endpoint, {})
144
+ true
145
+ rescue Error::HTTPError => e
146
+ false
147
+ end
148
+
149
+ private
150
+
151
+ #
152
+ # The path to this build component on the server.
153
+ #
154
+ # @return [String]
155
+ #
156
+ def api_path
157
+ "/api/build/#{url_safe(name)}"
158
+ end
159
+ end
160
+ end