Welcome to mirror list, hosted at ThFree Co, Russian Federation.

client.rb « jenkins « vendor « qa « qa - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: d766625501018177958c7e688cb422d2a7d2a300 (plain)
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