1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
|
# frozen_string_literal: true
require 'base64'
require 'cgi'
require 'fileutils'
require 'json'
require 'nokogiri'
require 'rest-client'
require 'securerandom'
require_relative './helpers'
require_relative './job'
module QA
module Vendor
module Jenkins
NetworkError = Class.new(StandardError)
NotParseableError = Class.new(StandardError)
class Client
include Helpers
attr_accessor :cookies
DEFAULT_SERVER_PORT = 8080
# @param host [String] the ip or hostname of the jenkins server
# @param user [String] the Jenkins admin user
# @param password [String] the Jenkins admin password
# @param port [Integer] the port that Jenkins is serving on
def initialize(host, user:, password:, port: nil)
@host = host
@user = user
@password = password
@port = port
@cookies = {}
end
def ready?
!!try_parse(RestClient.get(crumb_path, auth_headers).body)
end
# Creates a new job in Jenkins
#
# @param name [String] the name of the job
# @yieldparam job [Jenkins::Job] the job to be configured
# @return [Jenkins::Job] the created job in Jenkins
def create_job(name)
job = Job.new(name, self)
yield job if block_given?
job.create
job
end
# Is a given job running?
#
# @param name [String] the name of the job
# @return [Boolean] is the job running?
def job_running?(name)
res = execute <<~GROOVY
project = Jenkins.instance.getProjects().find{p -> p.getName().equals('#{name}')}
build = project.getBuilds().find{b -> b.getExecutor()}
return build ? build.getExecutor().isActive() : false
GROOVY
JSON.parse parse_result(res)
end
# Number of builds currently executing for a given job
#
# @param name [String] the name of the job
# @return [Integer] the number of builds currently running
def number_of_jobs_running(name)
res = execute <<~GROOVY
project = Jenkins.instance.getProjects().find{p -> p.getName().equals('#{name}')}
builds = project.getBuilds().findAll{b -> b.getExecutor()}
return builds.size
GROOVY
JSON.parse parse_result(res)&.to_i
end
# Latest build status for a job
#
# @param name [String] the name of the job
# @return [Symbol] the latest build status eg, (:success, :failure, etc)
def last_build_status(name)
res = execute <<~GROOVY
project = Jenkins.instance.getProjects().find{p -> p.getName().equals('#{name}')}
build = project.getBuilds()[-1]
return build.getResult()
GROOVY
parse_result(res)&.downcase&.to_sym
end
# Latest build id for a job
# Can be used to reference in other queries
#
# @param job_name [String] the name of the job
# @return [Integer] the latest build id
def last_build_id(job_name)
res = execute <<~GROOVY
project = Jenkins.instance.getProjects().find{p -> p.getName().equals('#{job_name}')}
build = project.getBuilds()[-1]
return build.getId()
GROOVY
parse_result(res)&.to_i
end
# Latest build log for a job
#
# @param job_name [String] the name of the job
# @param start [Integer] the log offset to return
# @return [String] the latest Jenkins log/output for this job
def last_build_log(job_name, start = 0)
get(
path: "/job/#{job_name}/#{last_build_id(job_name)}/logText/progressiveText",
params: { start: start }
).body
end
# Triggers a build for a given job
#
# @param name [String] the name of the job to trigger a build for
# @param [Hash] params the query parameters as a hash for the build endpoint
def build(name, params: {})
post(params, path: "/job/#{name}/build")
end
# Executes a Groovy script against the Jenkins instance
#
# @param script [String] the Groovy script to execute
def execute(script)
post("script=#{script}", path: '/scriptText')
end
# Sends XML to a given Jenkins endpoint
# This might be useful for filling in gaps in this lib
#
# @param xml [String] the xml to post
# @param params [Hash] the query parameters as a hash
# @param path [String] the path to post to ex: /job/<name>/build
# @return [Typhoeus::Response]
def post_xml(xml, params: {}, path: '')
post(xml, params: params, path: path, headers: { 'Content-Type' => 'text/xml' })
end
# Posts data to Jenkins
# This might be useful for filling in gaps in this lib
#
# @param data [String | Hash] the xml to post
# @param params [Hash] the query parameters as a hash
# @param path [String] the path to post to ex: /job/<name>/build
# @param headers [Hash] additional headers to send
# @return [Typhoeus::Response]
def post(data, params: {}, path: '', headers: {})
get_crumb
RestClient.post(
"#{api_path}#{path}?#{params_to_s(params)}",
data,
headers.merge(full_headers)
)
end
# Gets from a Jenkins endpoint
# This might be useful for filling in gaps in this lib
#
# @param path [String] the path to get from ex: /job/<name>/builds/<build_id>/logText/progressiveText
# @param params [Hash] the query parameters as a hash
# @return [Typhoeus::Response]
def get(path: '', params: {})
get_crumb
RestClient.get(
"#{api_path}#{path}?#{params_to_s(params)}",
full_headers
)
end
# configures the Jenkins GitLab plugin
#
# @param url [String] the url for the GitLab instance
# @param access_token [String] an access token for the GitLab instance
# @param secret_id [String] an secret id used for the Jenkins GitLab credentials
# @param hargs [Hash] extra keyword arguments to provide
# @option hargs [String] :connection_name the name to use for the gitlab connection
# @option hargs [Integer] :read_timeout the read timeout for GitLab Jenkins
# @option hargs [Integer] :connection_timeout the connection timeout for GitLab Jenkins
# @option hargs [Boolean] :ignore_ssl_errors whether GitLab Jenkins should ignore SSL errors
# @return [String] the execute response from Jenkins
def configure_gitlab_plugin(url, access_token:, secret_id: SecureRandom.hex(4), **hargs)
configure_secret(access_token, secret_id)
configure_gitlab(url, secret_id, **hargs)
end
private
def parse_result(res)
check_network_error(res)
res.body.scan(/Result: (.*)/)&.dig(0, 0)
end
def configure_gitlab(
url,
secret_id,
connection_name: 'default',
read_timeout: 10,
connection_timeout: 10,
ignore_ssl_errors: true
)
res = execute <<~GROOVY
import com.dabsquared.gitlabjenkins.connection.*;
conn = new GitLabConnection(
"#{connection_name}",
"#{url}",
"#{secret_id}",
#{ignore_ssl_errors},
#{connection_timeout},
#{read_timeout}
);
config = GitLabConnectionConfig.get();
config.setConnections([conn]);
GROOVY
res.body
end
def configure_secret(access_token, credential_id)
execute <<~GROOVY
import jenkins.model.Jenkins;
import com.cloudbees.plugins.credentials.domains.Domain;
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;
import com.cloudbees.plugins.credentials.CredentialsScope;
import hudson.util.Secret;
instance = Jenkins.instance;
domain = Domain.global();
store = instance.getExtensionList("com.cloudbees.plugins.credentials.SystemCredentialsProvider")[0].getStore();
secretText = new StringCredentialsImpl(
CredentialsScope.GLOBAL,
"#{credential_id}",
"GitLab API Token",
Secret.fromString("#{access_token}")
);
store.addCredentials(domain, secretText);
GROOVY
end
def get_crumb
return if @crumb
response = RestClient.get(crumb_path, auth_headers)
response_body = handle_json_response(response)
@crumb = response_body['crumb']
end
def params_to_s(params)
params.each_with_object([]) do |(k, v), memo|
memo << "#{k}=#{v}"
end.join('&')
end
def full_headers
crumb_headers
.merge(auth_headers)
.merge(cookie_headers)
end
def crumb_headers
{ 'Jenkins-Crumb' => @crumb }
end
def auth_headers
{ 'Authorization' => "Basic #{userpwd}" }
end
def cookie_headers
{ cookies: @cookies }
end
def userpwd
Base64.encode64("#{@user}:#{@password}")
end
def api_path
"http://#{@host}:#{port}"
end
def crumb_path
"#{api_path}/crumbIssuer/api/json"
end
def port
@port || DEFAULT_SERVER_PORT
end
end
end
end
end
|