jenkins_api_client 0.0.2 → 0.1.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.
@@ -0,0 +1,13 @@
1
+ = CHANGELOG
2
+
3
+ === v0.1.0 [26-OCT-2012]
4
+ * Improved performance
5
+ * Added job create feature, delete feature, chaining feature, and build feature
6
+ * Added exception handling mechanism
7
+
8
+ === v0.0.2 [16-OCT-2012]
9
+ * Added documentation
10
+ * Added some more smal features to Job class
11
+
12
+ === v0.0.1 [15-OCT-2012]
13
+ * Initial Release
data/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2012 Kannan Manickam <arangamani.kannan@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -1,14 +1,145 @@
1
- = jenkins_api_client
1
+ = Jenkins API Client
2
2
 
3
- Client libraries for communicating with a Jenkins CI server
3
+ Client libraries for communicating with a Jenkins CI server and programatically managing jobs.
4
4
 
5
- == Sample Usage:
5
+ == OVERVIEW:
6
+ This project is a simple API client for interacting with Jenkins Continuous Integration server.
7
+ Jenkins provides three kinds of remote access API. 1. XML API, 2. JSON API, and 3. Python API.
8
+ This project aims at consuming the JSON API and provides some useful functions for controlling
9
+ jobs on the Jenkins programatically. Even though Jenkins provides an awesome UI for controlling
10
+ jobs, it would be nice and helpful to have a programmable interface so we can dynamically and
11
+ automatically manage jobs and other artifacts.
12
+
13
+ == DETAILS:
14
+ This projects currently only provides functionality for the <tt>jobs</tt> interface. This is
15
+ still a work-in-progress project. I mainly use the functionality of this project for my autmation
16
+ work and the functionality mainly focussed on my usage and I believe others might find it useful
17
+ too. I would love to add more features to it and I will continue working on improving existing
18
+ features and add more interfaces such as nodes, views, build queue, etc,.
19
+
20
+ == USAGE:
21
+
22
+ === Installation
6
23
 
7
24
  Install jenkins_api_client by <tt>sudo gem install jenkins_api_client</tt>
25
+ Include this gem in your code as a require statement.
8
26
 
9
27
  require 'jenkins_api_client'
10
28
 
11
- client = JenkinsApi::Client.new(:server_ip => '0.0.0.0',
29
+ === Using with IRB
30
+
31
+ If you want to just play with it and not actually want to write a script, you can just use the
32
+ irb launcher script which is available in <tt>scripts/login_with_irb.rb</tt>. But make sure that
33
+ you have your credentials available in the correct location. By default the script assumes that
34
+ you have your credentials file in <tt>~/.jenkins_api_client/login.yml</tt>. If you don't prefer this
35
+ location and would like to use a different location, just modify that script to point to the
36
+ location where the credentials file exists.
37
+
38
+ ruby scripts/login_with_irb.rb
39
+
40
+ You will see the that it entered IRB session and you can play with the API client with the
41
+ <tt>@client</tt> object that it has returned.
42
+
43
+ === Authentication
44
+
45
+ This project supports two types of password-based authentication. You can just you the plain
46
+ password by using <tt>password</tt> parameter. If you don't prefer leaving plain passwords in the
47
+ credentials file, you can encode your password in base64 format and use <tt>password_base64</tt>
48
+ parameter to specify the password either in the arguments or in the credentials file.
49
+
50
+ === Basic Usage
51
+
52
+ As discussed earlier, you can either specify all the credentials and server information as
53
+ parameters to the Client or have a credentials file and just parse the yaml file and pass it in.
54
+ The following call just passes the information as parameters
55
+
56
+ @client = JenkinsApi::Client.new(:server_ip => '0.0.0.0',
12
57
  :username => 'somename', :password => 'secret password')
13
58
  # The following call will return all jobs matching 'Testjob'
14
- puts client.job.list("^Testjob")
59
+ puts @client.job.list("^Testjob")
60
+
61
+ The following example passes the YAML file contents. An example yaml file is located in
62
+ <tt>config/login.yml.example</tt>.
63
+
64
+ @client = JenkinsApi::Client.new(YAML.Load_file(File.expand_path('~/.jenkins_api_client/login.yml', __FILE__)))
65
+ # The following call lists all jobs
66
+ puts @client.job.list_all
67
+
68
+ === Chaining and Building Jobs
69
+
70
+ Sometimes we want certain jobs to be added as downstream projects and run them sequencially.
71
+ The following example will explain how this could be done.
72
+
73
+ require 'jenkins_api_client'
74
+
75
+ # We want to filter all jobs that start with 'test_job'
76
+ # Just write a regex to match all the jobs that start with 'test_job'
77
+ jobs_to_filter = "^test_job.*"
78
+
79
+ # Create an instance to jenkins_api_client
80
+ @client = JenkinsApi::Client.new(YAML.load_file(File.expand_path('~/.jenkins_api_client/login.yml', __FILE__)))
81
+
82
+ # Get a filtered list of jobs from the server
83
+ jobs = @client.job.list(jobs_to_filter)
84
+
85
+ # Chain all the jobs with 'success' as the threshold
86
+ # The chain method will return the jobs that is in the head of the sequence
87
+ # This method will also remove any existing chaining
88
+ initial_jobs = @client.job.chain(jobs, 'success', ["all"])
89
+
90
+ # Now that we have the initial job(s) we can build them
91
+ # The build function returns a code from the API which should be 302 if the build was successful
92
+ code = @client.job.build(initial_jobs[0])
93
+ raise "Could not build the job specified" unless code == 302
94
+
95
+ In the above example, you might have noticed that the chain method returns an array instead of a
96
+ single job. There is a reason behind it. In simple chain, such as the one in the example above, all
97
+ jobs specified are chained one by one. But in some cases they might not be dependent on the previous
98
+ jobs and we might want to run some jobs parallelly. We just have to specify that as a parameter.
99
+
100
+ For example: <tt>parallel = 3</tt> in the parameter list to the <tt>chain</tt> method will take the first three
101
+ jobs and chain them with the next three jobs and so forth till it reaches the end of the list.
102
+
103
+ There is another filter option you can specify for the method to take only jobs that are in a
104
+ particular state. In case if we want to build only jobs that are failed or unstable, we can achieve
105
+ that by passing in the states in the third parameter. In the example above, we wanted build all jobs.
106
+ If we just want to build failed and unstable jobs, just pass <tt>["failure", "unstable"]</tt>. Also if you
107
+ pass in an empty array, it will assume that you want to consider all jobs and no filtering will be
108
+ performed.
109
+
110
+ There is another parameter called <tt>threshold</tt> you can specify for the chaining and this is used
111
+ to decide whether to move forward with the next job in the chain or not. A <tt>success</tt> will move to
112
+ the next job only if the current build succeeds, <tt>failure</tt> will move to the next job even if the build
113
+ fails, and <tt>unstable</tt> will move to the job even if the build is unstable.
114
+
115
+ The following call to the <tt>chain</tt> method will consider only failed and unstable jobs, chain then
116
+ with 'failure' as the threshold, and also chain three jobs in parallel.
117
+
118
+ initial_jobs = @client.job.chain(jobs, 'failure', ["failure", "unstable"], 3)
119
+ # We will receive three jobs as a result and we can build them all
120
+ initial_jobs.each { |job|
121
+ code = @client.job.build(job)
122
+ raise "Unable to build job: #{job}" unless code == 302
123
+ }
124
+
125
+ === Debug
126
+
127
+ The call to client initialization accepts a debug parameter. If it is set to <tt>true</tt> it will print
128
+ some debug information to the console. By default it is set to false.
129
+
130
+ == CONTRIBUTING:
131
+
132
+ If you would like to contribute to this project, just do the following:
133
+
134
+ 1. Fork the repo on Github.
135
+ 2. Add your features and make commits to your forked repo.
136
+ 3. Make a pull request to this repo.
137
+ 4. Review will be done and changes will be requested.
138
+ 5. Once changes are done or no changes are required, pull request will be merged.
139
+ 6. The next release will have your changes in it.
140
+
141
+ == FEATURE REQUEST:
142
+
143
+ If you use this gem for your project and you think it would be nice to have a particular feature
144
+ that is presently not implemented, I would love to hear that and consider working on it.
145
+ Just open an issue in Github as a feature request.
@@ -0,0 +1,19 @@
1
+ # The Jenkins server and login information can be stored in a YAML file like this
2
+ # so we don't have to pass in the parameters every we login to the server
3
+ # through this API client.
4
+
5
+ # This file contains the following four parameters
6
+
7
+ # The IP address of the Jenkins CI Server
8
+
9
+ :server_ip: 0.0.0.0
10
+
11
+ # The port number on which the Jenkins listens on. The default is 8080
12
+
13
+ :server_port: 8080
14
+
15
+ # The username and password to authenticate to the server
16
+
17
+ :username: my_username
18
+
19
+ :password: my_password
@@ -1,9 +1,32 @@
1
+ #
2
+ # Copyright (c) 2012 Kannan Manickam <arangamani.kannan@gmail.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+
1
23
  require 'rubygems'
2
24
  require 'json'
3
25
  require 'net/http'
4
26
  require 'nokogiri'
5
27
  require 'active_support/core_ext'
6
28
  require 'active_support/builder'
29
+ require 'base64'
7
30
 
8
31
  require File.expand_path('../version', __FILE__)
9
32
  require File.expand_path('../exceptions', __FILE__)
@@ -11,9 +34,10 @@ require File.expand_path('../job', __FILE__)
11
34
 
12
35
  module JenkinsApi
13
36
  class Client
14
-
37
+ attr_accessor :debug
38
+ @debug = false
15
39
  DEFAULT_SERVER_PORT = 8080
16
- VALID_PARAMS = %w(server_ip server_port username password)
40
+ VALID_PARAMS = %w(server_ip server_port username password debug)
17
41
 
18
42
  # Initialize a Client object with Jenkins CI server information and credentials
19
43
  #
@@ -28,8 +52,13 @@ module JenkinsApi
28
52
  instance_variable_set("@#{key}", value) if value
29
53
  } if args.is_a? Hash
30
54
  raise "Server IP is required to connect to Jenkins Server" unless @server_ip
31
- raise "Credentials are required to connect to te Jenkins Server" unless @username && @password
55
+ raise "Credentials are required to connect to te Jenkins Server" unless @username && (@password || @password_base64)
32
56
  @server_port = DEFAULT_SERVER_PORT unless @server_port
57
+
58
+ # Base64 decode inserts a newline character at the end. As a workaround added chomp
59
+ # to remove newline characters. I hope nobody uses newline characters at the end of
60
+ # their passwords :)
61
+ @password = Base64.decode64(@password_base64).chomp if @password_base64
33
62
  end
34
63
 
35
64
  # Creates an instance to the Job object by passing a reference to self
@@ -48,12 +77,24 @@ module JenkinsApi
48
77
  #
49
78
  # @param [String] url_prefix
50
79
  #
51
- def api_get_request(url_prefix)
80
+ def api_get_request(url_prefix, tree = nil)
52
81
  http = Net::HTTP.start(@server_ip, @server_port)
53
82
  request = Net::HTTP::Get.new("#{url_prefix}/api/json")
83
+ request = Net::HTTP::Get.new("#{url_prefix}/api/json?#{tree}") if tree
54
84
  request.basic_auth @username, @password
55
85
  response = http.request(request)
56
- JSON.parse(response.body)
86
+ case response.code.to_i
87
+ when 200
88
+ return JSON.parse(response.body)
89
+ when 401
90
+ raise Exceptions::UnautherizedException.new("HTTP Code: #{response.code.to_s}, Response Body: #{response.body}")
91
+ when 404
92
+ raise Exceptions::NotFoundException.new("HTTP Code: #{response.code.to_s}, Response Body: #{response.body}")
93
+ when 500
94
+ raise Exceptions::InternelServerErrorException.new("HTTP Code: #{response.code.to_s}, Response Body: #{response.body}")
95
+ else
96
+ raise Exceptions::ApiException.new("HTTP Code: #{response.code.to_s}, Response body: #{response.body}")
97
+ end
57
98
  end
58
99
 
59
100
  # Sends a POST message to the Jenkins CI server with the specified URL
@@ -65,6 +106,16 @@ module JenkinsApi
65
106
  request = Net::HTTP::Post.new("#{url_prefix}")
66
107
  request.basic_auth @username, @password
67
108
  response = http.request(request)
109
+ case response.code.to_i
110
+ when 200, 302
111
+ return response.code
112
+ when 404
113
+ raise Exceptions::NotFoundException.new("HTTP Code: #{response.code.to_s}, Response Body: #{response.body}")
114
+ when 500
115
+ raise Exceptions::InternelServerErrorException.new("HTTP Code: #{response.code.to_s}, Response Body: #{response.body}")
116
+ else
117
+ raise Exceptions::ApiException.new("HTTP Code: #{response.code.to_s}, Response body: #{response.body}")
118
+ end
68
119
  end
69
120
 
70
121
  # Obtains the configuration of a component from the Jenkins CI server
@@ -86,9 +137,10 @@ module JenkinsApi
86
137
  #
87
138
  def post_config(url_prefix, xml)
88
139
  http = Net::HTTP.start(@server_ip, @server_port)
89
- request = Net::HTTP::Post.new("#{url_prefix}/config.xml")
140
+ request = Net::HTTP::Post.new("#{url_prefix}")
90
141
  request.basic_auth @username, @password
91
142
  request.body = xml
143
+ request.content_type = 'application/xml'
92
144
  response = http.request(request)
93
145
  response.code
94
146
  end
@@ -1,11 +1,49 @@
1
+ #
2
+ # Copyright (c) 2012 Kannan Manickam <arangamani.kannan@gmail.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+
1
23
  module JenkinsApi
2
24
  module Exceptions
3
25
  class ApiException < RuntimeError
4
-
5
- def initialize(message="")
26
+ def initialize(message = "")
6
27
  super("Error: #{message}")
7
28
  end
29
+ end
30
+
31
+ class UnautherizedException < ApiException
32
+ def initialize(message = "")
33
+ super("Invalid credentials are provided. #{message}")
34
+ end
35
+ end
8
36
 
37
+ class NotFoundException < ApiException
38
+ def initialize(message = "")
39
+ super("Requested page not found on the Jenkins CI server. #{message}")
40
+ end
41
+ end
42
+
43
+ class InternelServerErrorException < ApiException
44
+ def initialize(message = "")
45
+ super("Internel Server Error. #{message}")
46
+ end
9
47
  end
10
48
  end
11
49
  end
@@ -1,3 +1,25 @@
1
+ #
2
+ # Copyright (c) 2012 Kannan Manickam <arangamani.kannan@gmail.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+
1
23
  module JenkinsApi
2
24
  class Client
3
25
  class Job
@@ -8,6 +30,29 @@ module JenkinsApi
8
30
  @client = client
9
31
  end
10
32
 
33
+ # Return a string representation of the object
34
+ #
35
+ def to_s
36
+ "#<JenkinsApi::Client::Job>"
37
+ end
38
+
39
+ # Create a job with the name specified and the xml given
40
+ #
41
+ # @param [String] job_name
42
+ # @param [XML] xml
43
+ #
44
+ def create(job_name, xml)
45
+ @client.post_config("/createItem?name=#{job_name}", xml)
46
+ end
47
+
48
+ # Delete a job given the name
49
+ #
50
+ # @param [String] job_name
51
+ #
52
+ def delete(job_name)
53
+ @client.api_post_request("/job/#{job_name}/doDelete")
54
+ end
55
+
11
56
  # List all jobs on the Jenkins CI server
12
57
  #
13
58
  def list_all
@@ -22,12 +67,17 @@ module JenkinsApi
22
67
  # List all jobs that match the given regex
23
68
  #
24
69
  # @param [String] filter - a regex
70
+ # @param [Boolean] ignorecase
25
71
  #
26
- def list(filter)
72
+ def list(filter, ignorecase = true)
27
73
  response_json = @client.api_get_request("")
28
74
  jobs = []
29
75
  response_json["jobs"].each { |job|
30
- jobs << job["name"] if job["name"] =~ /#{filter}/i
76
+ if ignorecase
77
+ jobs << job["name"] if job["name"] =~ /#{filter}/i
78
+ else
79
+ jobs << job["name"] if job["name"] =~ /#{filter}/
80
+ end
31
81
  }
32
82
  jobs
33
83
  end
@@ -101,10 +151,10 @@ module JenkinsApi
101
151
  # This functions lists all jobs that are currently running on the Jenkins CI server
102
152
  #
103
153
  def list_running
104
- jobs = list_all
154
+ xml_response = @client.api_get_request("", "tree=jobs[name,color]")
105
155
  running_jobs = []
106
- jobs.each { |job|
107
- running_jobs << job if get_current_build_status(job) == "running"
156
+ xml_response["jobs"].each { |job|
157
+ running_jobs << job["name"] if job["color"] =~ /anime/
108
158
  }
109
159
  running_jobs
110
160
  end
@@ -131,7 +181,7 @@ module JenkinsApi
131
181
  # @param [String] xml
132
182
  #
133
183
  def post_config(job_name, xml)
134
- @client.post_config("/job/#{job_name}", xml)
184
+ @client.post_config("/job/#{job_name}/config.xml", xml)
135
185
  end
136
186
 
137
187
  # Change the description of a specific job
@@ -154,8 +204,9 @@ module JenkinsApi
154
204
  # @param [String] job_name
155
205
  # @param [String] downstream_projects
156
206
  # @param [String] threshold - failure, success, or unstable
207
+ # @param [Bool] overwrite - true or false
157
208
  #
158
- def add_downstream_projects(job_name, downstream_projects, threshold = 'success')
209
+ def add_downstream_projects(job_name, downstream_projects, threshold, overwrite = false)
159
210
  case threshold
160
211
  when 'success'
161
212
  name = 'SUCCESS'
@@ -174,7 +225,11 @@ module JenkinsApi
174
225
  n_xml = Nokogiri::XML(xml)
175
226
  child_projects_node = n_xml.xpath("//childProjects").first
176
227
  if child_projects_node
177
- child_projects_node.content = child_projects_node.content + ", #{downstream_projects}"
228
+ if overwrite
229
+ child_projects_node.content = "#{downstream_projects}"
230
+ else
231
+ child_projects_node.content = child_projects_node.content + ", #{downstream_projects}"
232
+ end
178
233
  else
179
234
  publisher_node = n_xml.xpath("//publishers").first
180
235
  build_trigger_node = publisher_node.add_child("<hudson.tasks.BuildTrigger/>")
@@ -237,6 +292,39 @@ module JenkinsApi
237
292
  post_config(job_name, xml_modified)
238
293
  end
239
294
 
295
+ # Chain the jobs given based on specified criteria
296
+ #
297
+ # @param [Array] job_names Array of job names to be chained
298
+ # @param [String] threshold what should be the threshold for running the next job
299
+ # @param [Array] criteria criteria which should be applied for picking the jobs for the chain
300
+ # @param [Integer] parallel Number of jobs that should be considered for parallel run
301
+ #
302
+ def chain(job_names, threshold, criteria, parallel = 1)
303
+ raise "Parallel jobs should be at least 1" if parallel < 1
304
+
305
+ job_names.each { |job|
306
+ puts "INFO: Removing downstream projects for <#{job}>" if @client.debug
307
+ @client.job.remove_downstream_projects(job)
308
+ }
309
+
310
+ filtered_job_names = []
311
+ if criteria.include?("all") || criteria.empty?
312
+ filtered_job_names = job_names
313
+ else
314
+ puts "INFO: Criteria is specified. Filtering jobs..." if @client.debug
315
+ job_names.each { |job|
316
+ filtered_job_names << job if criteria.include?(@client.job.get_current_build_status(job))
317
+ }
318
+ end
319
+ filtered_job_names.each_with_index { |job_name, index|
320
+ break if index >= (filtered_job_names.length - parallel)
321
+ puts "INFO: Adding <#{filtered_job_names[index+1]}> as a downstream project to <#{job_name}> with <#{threshold}> as the threshold" if @client.debug
322
+ @client.job.add_downstream_projects(job_name, filtered_job_names[index + parallel], threshold, true)
323
+ }
324
+ parallel = filtered_job_names.length if parallel > filtered_job_names.length
325
+ filtered_job_names[0..parallel-1]
326
+ end
327
+
240
328
  end
241
329
  end
242
330
  end
@@ -1,6 +1,27 @@
1
+ #
2
+ # Copyright (c) 2012 Kannan Manickam <arangamani.kannan@gmail.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
1
22
 
2
23
  module JenkinsApi
3
24
  class Client
4
- VERSION = "0.0.2"
25
+ VERSION = "0.1.0"
5
26
  end
6
27
  end
@@ -0,0 +1,14 @@
1
+ # This script provides an easier way to login to Jenkins server API.
2
+ # It logs you in with the credentials and server details you proided and then starts an IRB
3
+ # session so you can interactively play with the API.
4
+
5
+ require File.expand_path('../../lib/jenkins_api_client', __FILE__)
6
+ require 'yaml'
7
+ require 'irb'
8
+
9
+ begin
10
+ @client = JenkinsApi::Client.new(YAML.load_file(File.expand_path('~/.jenkins_api_client/login.yml', __FILE__)))
11
+ puts "logged-in to the Jenkins API, use the '@client' variable to use the client"
12
+ end
13
+
14
+ IRB.start
@@ -0,0 +1,47 @@
1
+ #
2
+ # Specifying JenkinsApi::Client class capabilities
3
+ # Author: Kannan Manickam <arangamani.kannan@gmail.com>
4
+ #
5
+
6
+ require File.expand_path('../spec_helper', __FILE__)
7
+ require 'yaml'
8
+
9
+ describe JenkinsApi::Client do
10
+ context "Given valid credentials and server information in the ~/.jenkins_api_client/login.yml" do
11
+ before(:all) do
12
+ @creds_file = '~/.jenkins_api_client/login.yml'
13
+ # Grabbing just the server IP in a variable so we can check for wrong credentials
14
+ @server_ip = YAML.load_file(File.expand_path(@creds_file, __FILE__))[:server_ip]
15
+ begin
16
+ @client = JenkinsApi::Client.new(YAML.load_file(File.expand_path(@creds_file, __FILE__)))
17
+ rescue Exception => e
18
+ puts "WARNING: Credentials are not set properly."
19
+ puts e.message
20
+ end
21
+ end
22
+
23
+ it "Should be able to initialize with valid credentials" do
24
+ client1 = JenkinsApi::Client.new(YAML.load_file(File.expand_path(@creds_file, __FILE__)))
25
+ client1.class.should == JenkinsApi::Client
26
+ end
27
+
28
+ it "Should fail if wrong credentials are given" do
29
+ begin
30
+ client2 = JenkinsApi::Client.new(:server_ip => @server_ip, :username => 'stranger', :password => 'hacked')
31
+ client2.job.list_all
32
+ rescue Exception => e
33
+ e.class.should == JenkinsApi::Exceptions::UnautherizedException
34
+ end
35
+ end
36
+
37
+ it "Should return a job object on call to job function" do
38
+ @client.job.class.should == JenkinsApi::Client::Job
39
+ end
40
+
41
+ it "Should accept a YAML argument when creating a new client" do
42
+ client3 = JenkinsApi::Client.new(YAML.load_file(File.expand_path(@creds_file, __FILE__)))
43
+ client3.class.should == JenkinsApi::Client
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,129 @@
1
+ #
2
+ # Specifying JenkinsApi::Client::Job class capabilities
3
+ # Author: Kannan Manickam <arangamani.kannan@gmail.com>
4
+ #
5
+
6
+ require File.expand_path('../spec_helper', __FILE__)
7
+ require 'yaml'
8
+
9
+ describe JenkinsApi::Client::Job do
10
+ context "With properly initialized client" do
11
+ before(:all) do
12
+ @helper = JenkinsApiSpecHelper::Helper.new
13
+ @creds_file = '~/.jenkins_api_client/login.yml'
14
+ @job_name_prefix = 'awesome_rspec_test_job'
15
+ @filter = "^#{@job_name_prefix}.*"
16
+ @job_name = ''
17
+ begin
18
+ @client = JenkinsApi::Client.new(YAML.load_file(File.expand_path(@creds_file, __FILE__)))
19
+ rescue Exception => e
20
+ puts "WARNING: Credentials are not set properly."
21
+ puts e.message
22
+ end
23
+ # Creating 10 jobs to run the spec tests on
24
+ begin
25
+ 10.times do |num|
26
+ xml = @helper.create_job_xml
27
+ job = "#{@job_name_prefix}_#{num}"
28
+ @job_name = job if num == 0
29
+ @client.job.create(job, xml).to_i.should == 200
30
+ end
31
+ rescue Exception => e
32
+ puts "WARNING: Can't create jobs for preparing to spec tests"
33
+ end
34
+ end
35
+
36
+ it "Should be able to create a job" do
37
+ xml = @helper.create_job_xml
38
+ @client.job.create("some_random_nonexistent_job", xml).to_i.should == 200
39
+ end
40
+
41
+ it "Should be able to change the description of a job" do
42
+ @client.job.change_description("some_random_nonexistent_job", "The description has been changed by the spec test").to_i.should == 200
43
+ end
44
+
45
+ it "Should be able to delete a job" do
46
+ @client.job.delete("some_random_nonexistent_job").to_i.should == 302
47
+ end
48
+
49
+ it "Should list all jobs" do
50
+ @client.job.list_all.class.should == Array
51
+ end
52
+
53
+ it "Should return job names based on the filter" do
54
+ names = @client.job.list(@filter)
55
+ names.class.should == Array
56
+ names.each { |name|
57
+ name.should match /#{@filter}/i
58
+ }
59
+ end
60
+
61
+ it "Should return all job names with details" do
62
+ @client.job.list_all_with_details.class.should == Array
63
+ end
64
+
65
+ it "Should list details of a particular job" do
66
+ job_name = @client.job.list(@filter)[0]
67
+ job_name.class.should == String
68
+ @client.job.list_details(job_name).class.should == Hash
69
+ end
70
+
71
+ it "Should list upstream projects of the specified job" do
72
+ @client.job.get_upstream_projects(@job_name).class.should == Array
73
+ end
74
+
75
+ it "Should list downstream projects of the specified job" do
76
+ @client.job.get_downstream_projects(@job_name).class.should == Array
77
+ end
78
+
79
+ it "Should get builds of a specified job" do
80
+ @client.job.get_builds(@job_name).class.should == Array
81
+ end
82
+
83
+ it "Should obtain the current build status for the specified job" do
84
+ build_status = @client.job.get_current_build_status(@job_name)
85
+ build_status.class.should == String
86
+ valid_build_status = ["not run", "aborted", "success", "failure", "unstable", "running"]
87
+ valid_build_status.include?(build_status).should be_true
88
+ end
89
+
90
+ it "Should list all running jobs" do
91
+ @client.job.list_running.class.should == Array
92
+ end
93
+
94
+ it "Should build the specified job" do
95
+ @client.job.get_current_build_status(@job_name).should_not == "running"
96
+ response = @client.job.build(@job_name)
97
+ response.to_i.should == 302
98
+ end
99
+
100
+ it "Should be able to restrict a job to a node" do
101
+ @client.job.restrict_to_node(@job_name, 'master').to_i.should == 200
102
+ # Run it again to make sure that the replace existing node works
103
+ @client.job.restrict_to_node(@job_name, 'master').to_i.should == 200
104
+ end
105
+
106
+ it "Should be able to chain jobs" do
107
+ #
108
+ jobs = @client.job.list(@filter)
109
+ jobs.class.should == Array
110
+ start_jobs = @client.job.chain(jobs, 'success', ["all"])
111
+ start_jobs.class.should == Array
112
+ start_jobs.length.should == 1
113
+
114
+ #
115
+ #
116
+ start_jobs = @client.job.chain(jobs, 'failure', ["not run", "aborted", 'failure'], 3)
117
+ start_jobs.class.should == Array
118
+ start_jobs.length.should == 3
119
+ end
120
+
121
+ after(:all) do
122
+ job_names = @client.job.list(@filter)
123
+ job_names.each { |job_name|
124
+ @client.job.delete(job_name)
125
+ }
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,47 @@
1
+ #
2
+ # Helper functions for Ruby specifications
3
+ # Author: Kannan Manickam <arangamani.kannan@gmail.com>
4
+ #
5
+
6
+ require 'simplecov'
7
+ SimpleCov.start if ENV["COVERAGE"]
8
+ require File.expand_path('../../lib/jenkins_api_client', __FILE__)
9
+ require 'pp'
10
+ require 'yaml'
11
+ require 'nokogiri'
12
+
13
+ RSpec.configure do |config|
14
+ config.mock_with :flexmock
15
+ end
16
+
17
+ module JenkinsApiSpecHelper
18
+ class Helper
19
+
20
+ def create_job_xml
21
+ builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') { |xml|
22
+ xml.project {
23
+ xml.actions
24
+ xml.description
25
+ xml.keepDependencies "false"
26
+ xml.properties
27
+ xml.scm(:class => "hudson.scm.NullSCM")
28
+ xml.canRoam "true"
29
+ xml.disabled "false"
30
+ xml.blockBuildWhenDownstreamBuilding "false"
31
+ xml.blockBuildWhenUpstreamBuilding "false"
32
+ xml.triggers.vector
33
+ xml.concurrentBuild "false"
34
+ xml.builders {
35
+ xml.send("hudson.tasks.Shell") {
36
+ xml.command "\necho 'done'\necho 'done again'"
37
+ }
38
+ }
39
+ xml.publishers
40
+ xml.buildWrappers
41
+ }
42
+ }
43
+ builder.to_xml
44
+ end
45
+
46
+ end
47
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jenkins_api_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-16 00:00:00.000000000 Z
12
+ date: 2012-10-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: nokogiri
@@ -167,14 +167,21 @@ extensions: []
167
167
  extra_rdoc_files:
168
168
  - README.rdoc
169
169
  files:
170
+ - CHANGELOG.rdoc
170
171
  - Gemfile
172
+ - LICENCE
171
173
  - README.rdoc
172
174
  - Rakefile
175
+ - config/login.yml.example
173
176
  - lib/jenkins_api_client.rb
174
177
  - lib/jenkins_api_client/client.rb
175
178
  - lib/jenkins_api_client/exceptions.rb
176
179
  - lib/jenkins_api_client/job.rb
177
180
  - lib/jenkins_api_client/version.rb
181
+ - scripts/login_with_irb.rb
182
+ - spec/client_spec.rb
183
+ - spec/job_spec.rb
184
+ - spec/spec_helper.rb
178
185
  homepage: https://github.com/arangamani/jenkins_api_client
179
186
  licenses: []
180
187
  post_install_message:
@@ -189,7 +196,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
189
196
  version: '0'
190
197
  segments:
191
198
  - 0
192
- hash: -746284255980224518
199
+ hash: 2928616763588687630
193
200
  required_rubygems_version: !ruby/object:Gem::Requirement
194
201
  none: false
195
202
  requirements: