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.
- 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:
|