runner_spec.rb 14.5 KB
Newer Older
1 2
require 'spec_helper'

3
describe API::Runner do
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
  include ApiHelpers
  include StubGitlabCalls

  let(:registration_token) { 'abcdefg123456' }

  before do
    stub_gitlab_calls
    stub_application_setting(runners_registration_token: registration_token)
  end

  describe '/api/v4/runners' do
    describe 'POST /api/v4/runners' do
      context 'when no token is provided' do
        it 'returns 400 error' do
          post api('/runners')
          expect(response).to have_http_status 400
        end
      end

      context 'when invalid token is provided' do
        it 'returns 403 error' do
          post api('/runners'), token: 'invalid'
          expect(response).to have_http_status 403
        end
      end

      context 'when valid token is provided' do
        it 'creates runner with default values' do
          post api('/runners'), token: registration_token

          runner = Ci::Runner.first

          expect(response).to have_http_status 201
          expect(json_response['id']).to eq(runner.id)
          expect(json_response['token']).to eq(runner.token)
          expect(runner.run_untagged).to be true
        end

        context 'when project token is used' do
          let(:project) { create(:empty_project) }

          it 'creates runner' do
            post api('/runners'), token: project.runners_token

            expect(response).to have_http_status 201
            expect(project.runners.size).to eq(1)
          end
        end
      end

      context 'when runner description is provided' do
        it 'creates runner' do
          post api('/runners'), token: registration_token,
Tomasz Maczukin's avatar
Tomasz Maczukin committed
57
                                description: 'server.hostname'
58 59 60 61 62 63 64 65 66

          expect(response).to have_http_status 201
          expect(Ci::Runner.first.description).to eq('server.hostname')
        end
      end

      context 'when runner tags are provided' do
        it 'creates runner' do
          post api('/runners'), token: registration_token,
Tomasz Maczukin's avatar
Tomasz Maczukin committed
67
                                tag_list: 'tag1, tag2'
68 69 70 71 72 73 74 75 76 77

          expect(response).to have_http_status 201
          expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
        end
      end

      context 'when option for running untagged jobs is provided' do
        context 'when tags are provided' do
          it 'creates runner' do
            post api('/runners'), token: registration_token,
Tomasz Maczukin's avatar
Tomasz Maczukin committed
78 79
                                  run_untagged: false,
                                  tag_list: ['tag']
80 81 82 83 84 85 86 87 88 89

            expect(response).to have_http_status 201
            expect(Ci::Runner.first.run_untagged).to be false
            expect(Ci::Runner.first.tag_list.sort).to eq(['tag'])
          end
        end

        context 'when tags are not provided' do
          it 'returns 404 error' do
            post api('/runners'), token: registration_token,
Tomasz Maczukin's avatar
Tomasz Maczukin committed
90
                                  run_untagged: false
91 92 93 94 95 96 97 98 99

            expect(response).to have_http_status 404
          end
        end
      end

      context 'when option for locking Runner is provided' do
        it 'creates runner' do
          post api('/runners'), token: registration_token,
Tomasz Maczukin's avatar
Tomasz Maczukin committed
100
                                locked: true
101 102 103 104 105 106 107 108 109 110 111 112

          expect(response).to have_http_status 201
          expect(Ci::Runner.first.locked).to be true
        end
      end

      %w(name version revision platform architecture).each do |param|
        context "when info parameter '#{param}' info is present" do
          let(:value) { "#{param}_value" }

          it %q(updates provided Runner's parameter) do
            post api('/runners'), token: registration_token,
Tomasz Maczukin's avatar
Tomasz Maczukin committed
113
                                  info: { param => value }
114 115 116 117 118 119 120 121 122 123 124 125

            expect(response).to have_http_status 201
            expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value)
          end
        end
      end
    end

    describe 'DELETE /api/v4/runners' do
      context 'when no token is provided' do
        it 'returns 400 error' do
          delete api('/runners')
126

127 128 129 130 131 132 133
          expect(response).to have_http_status 400
        end
      end

      context 'when invalid token is provided' do
        it 'returns 403 error' do
          delete api('/runners'), token: 'invalid'
134

135 136 137 138 139 140 141 142 143
          expect(response).to have_http_status 403
        end
      end

      context 'when valid token is provided' do
        let(:runner) { create(:ci_runner) }

        it 'deletes Runner' do
          delete api('/runners'), token: runner.token
144 145

          expect(response).to have_http_status 204
146 147 148 149 150
          expect(Ci::Runner.count).to eq(0)
        end
      end
    end
  end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
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 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407

  describe '/api/v4/jobs' do
    let(:project) { create(:empty_project, shared_runners_enabled: false) }
    let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
    let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
    let(:runner) { create(:ci_runner) }

    before { project.runners << runner }

    describe 'POST /api/v4/jobs/request' do
      let!(:last_update) {}
      let!(:new_update) { }
      let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' }

      before { stub_container_registry_config(enabled: false) }

      shared_examples 'no jobs available' do
        before { request_job }

        context 'when runner sends version in User-Agent' do
          context 'for stable version' do
            it 'gives 204 and set X-GitLab-Last-Update' do
              expect(response).to have_http_status(204)
              expect(response.header).to have_key('X-GitLab-Last-Update')
            end
          end

          context 'when last_update is up-to-date' do
            let(:last_update) { runner.ensure_runner_queue_value }

            it 'gives 204 and set the same X-GitLab-Last-Update' do
              expect(response).to have_http_status(204)
              expect(response.header['X-GitLab-Last-Update']).to eq(last_update)
            end
          end

          context 'when last_update is outdated' do
            let(:last_update) { runner.ensure_runner_queue_value }
            let(:new_update) { runner.tick_runner_queue }

            it 'gives 204 and set a new X-GitLab-Last-Update' do
              expect(response).to have_http_status(204)
              expect(response.header['X-GitLab-Last-Update']).to eq(new_update)
            end
          end

          context 'for beta version' do
            let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' }
            it { expect(response).to have_http_status(204) }
          end

          context 'for pre-9-0 version' do
            let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' }
            it { expect(response).to have_http_status(204) }
          end

          context 'for pre-9-0 beta version' do
            let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' }
            it { expect(response).to have_http_status(204) }
          end
        end

        context %q(when runner doesn't send version in User-Agent) do
          let(:user_agent) { 'Go-http-client/1.1' }
          it { expect(response).to have_http_status(404) }
        end

        context %q(when runner doesn't have a User-Agent) do
          let(:user_agent) { nil }
          it { expect(response).to have_http_status(404) }
        end
      end

      context 'when no token is provided' do
        it 'returns 400 error' do
          post api('/jobs/request')
          expect(response).to have_http_status 400
        end
      end

      context 'when invalid token is provided' do
        it 'returns 403 error' do
          post api('/jobs/request'), token: 'invalid'
          expect(response).to have_http_status 403
        end
      end

      context 'when valid token is provided' do

        context 'when Runner is not active' do
          let(:runner) { create(:ci_runner, :inactive) }

          it 'returns 404 error' do
            request_job
            expect(response).to have_http_status 404
          end
        end

        context 'when jobs are finished' do
          before { job.success }
          it_behaves_like 'no jobs available'
        end

        context 'when other projects have pending jobs' do
          before do
            job.success
            create(:ci_build, :pending)
          end

          it_behaves_like 'no jobs available'
        end

        context 'when shared runner requests job for project without shared_runners_enabled' do
          let(:runner) { create(:ci_runner, :shared) }
          it_behaves_like 'no jobs available'
        end

        context 'when there is a pending job' do
          it 'starts a job' do
            request_job info: {platform: :darwin}

            expect(response).to have_http_status(201)
            expect(response.headers).not_to have_key('X-GitLab-Last-Update')
            expect(json_response['sha']).to eq(job.sha)
            expect(json_response['options']).to eq({'image' => 'ruby:2.1', 'services' => ['postgres']})
            expect(json_response['variables']).to include(
                                                      {'key' => 'CI_BUILD_NAME', 'value' => 'spinach', 'public' => true},
                                                      {'key' => 'CI_BUILD_STAGE', 'value' => 'test', 'public' => true},
                                                      {'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true}
                                                  )
            expect(runner.reload.platform).to eq('darwin')
          end

          it 'updates runner info' do
            expect { request_job }.to change { runner.reload.contacted_at }
          end

          %w(name version revision platform architecture).each do |param|
            context "when info parameter '#{param}' is present" do
              let(:value) { "#{param}_value" }

              it %q(updates provided Runner's parameter) do
                request_job info: {param => value}

                expect(response).to have_http_status(201)
                runner.reload
                expect(runner.read_attribute(param.to_sym)).to eq(value)
              end
            end
          end

          context 'when concurrently updating a job' do
            before do
              expect_any_instance_of(Ci::Build).to receive(:run!).
                  and_raise(ActiveRecord::StaleObjectError.new(nil, nil))
            end

            it 'returns a conflict' do
              request_job
              expect(response).to have_http_status(409)
              expect(response.headers).not_to have_key('X-GitLab-Last-Update')
            end
          end

          context 'when project and pipeline have multiple jobs' do
            let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }

            before { job.success }

            it 'returns dependent jobs' do
              request_job

              expect(response).to have_http_status(201)
              expect(json_response['id']).to eq(test_job.id)
              expect(json_response['depends_on_builds'].count).to eq(1)
              expect(json_response['depends_on_builds'][0]).to include('id' => job.id, 'name' => 'spinach')
            end
          end

          context 'when job has no tags' do
            before { job.update(tags: []) }

            context 'when runner is allowed to pick untagged jobs' do
              before { runner.update_column(:run_untagged, true) }

              it 'picks job' do
                request_job
                expect(response).to have_http_status 201
              end
            end

            context 'when runner is not allowed to pick untagged jobs' do
              before { runner.update_column(:run_untagged, false) }
              it_behaves_like 'no jobs available'
            end
          end

          context 'when triggered job is available' do
            before do
              trigger = create(:ci_trigger, project: project)
              create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [job], trigger: trigger)
              project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
            end

            it 'returns variables for triggers' do
              request_job

              expect(response).to have_http_status(201)
              expect(json_response['variables']).to include(
                                                        {'key' => 'CI_BUILD_NAME', 'value' => 'spinach', 'public' => true},
                                                        {'key' => 'CI_BUILD_STAGE', 'value' => 'test', 'public' => true},
                                                        {'key' => 'CI_BUILD_TRIGGERED', 'value' => 'true', 'public' => true},
                                                        {'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true},
                                                        {'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false},
                                                        {'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false},
                                                    )
            end
          end

          describe 'registry credentials support' do
            let(:registry_url) { 'registry.example.com:5005' }
            let(:registry_credentials) do
              {'type' => 'registry',
               'url' => registry_url,
               'username' => 'gitlab-ci-token',
               'password' => job.token}
            end

            context 'when registry is enabled' do
              before { stub_container_registry_config(enabled: true, host_port: registry_url) }

              it 'sends registry credentials key' do
                request_job
                expect(json_response).to have_key('credentials')
                expect(json_response['credentials']).to include(registry_credentials)
              end
            end

            context 'when registry is disabled' do
              before { stub_container_registry_config(enabled: false, host_port: registry_url) }

              it 'does not send registry credentials' do
                request_job
                expect(json_response).to have_key('credentials')
                expect(json_response['credentials']).not_to include(registry_credentials)
              end
            end
          end
        end

        def request_job(token = runner.token, **params)
          new_params = params.merge(token: token, last_update: last_update)
          post api('/jobs/request'), new_params, {'User-Agent' => user_agent}
        end
      end
    end
  end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
408
end