jenkins_api_client 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: