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.
- data/CHANGELOG.rdoc +13 -0
- data/LICENCE +21 -0
- data/README.rdoc +136 -5
- data/config/login.yml.example +19 -0
- data/lib/jenkins_api_client/client.rb +58 -6
- data/lib/jenkins_api_client/exceptions.rb +40 -2
- data/lib/jenkins_api_client/job.rb +96 -8
- data/lib/jenkins_api_client/version.rb +22 -1
- data/scripts/login_with_irb.rb +14 -0
- data/spec/client_spec.rb +47 -0
- data/spec/job_spec.rb +129 -0
- data/spec/spec_helper.rb +47 -0
- metadata +10 -3
data/CHANGELOG.rdoc
ADDED
@@ -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.
|
data/README.rdoc
CHANGED
@@ -1,14 +1,145 @@
|
|
1
|
-
=
|
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
|
-
==
|
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
|
-
|
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
|
-
|
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}
|
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
|
-
|
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
|
-
|
154
|
+
xml_response = @client.api_get_request("", "tree=jobs[name,color]")
|
105
155
|
running_jobs = []
|
106
|
-
jobs.each { |job|
|
107
|
-
running_jobs << job if
|
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 =
|
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
|
-
|
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
|
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
|
data/spec/client_spec.rb
ADDED
@@ -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
|
data/spec/job_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
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-
|
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:
|
199
|
+
hash: 2928616763588687630
|
193
200
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
194
201
|
none: false
|
195
202
|
requirements:
|