runner_spec.rb 41.6 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
  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')
18

19
          expect(response).to have_gitlab_http_status 400
20 21 22 23 24 25
        end
      end

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

27
          expect(response).to have_gitlab_http_status 403
28 29 30 31 32 33 34 35 36
        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

37
          expect(response).to have_gitlab_http_status 201
38 39 40
          expect(json_response['id']).to eq(runner.id)
          expect(json_response['token']).to eq(runner.token)
          expect(runner.run_untagged).to be true
41
          expect(runner.token).not_to eq(registration_token)
42 43 44
        end

        context 'when project token is used' do
45
          let(:project) { create(:project) }
46 47 48 49

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

50
            expect(response).to have_gitlab_http_status 201
51
            expect(project.runners.size).to eq(1)
52 53
            expect(Ci::Runner.first.token).not_to eq(registration_token)
            expect(Ci::Runner.first.token).not_to eq(project.runners_token)
54 55 56 57 58 59 60
          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
61
                                description: 'server.hostname'
62

63
          expect(response).to have_gitlab_http_status 201
64 65 66 67 68 69 70
          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
71
                                tag_list: 'tag1, tag2'
72

73
          expect(response).to have_gitlab_http_status 201
74 75 76 77 78 79 80 81
          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
82 83
                                  run_untagged: false,
                                  tag_list: ['tag']
84

85
            expect(response).to have_gitlab_http_status 201
86 87 88 89 90 91 92 93
            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
94
                                  run_untagged: false
95

96
            expect(response).to have_gitlab_http_status 404
97 98 99 100 101 102 103
          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
104
                                locked: true
105

106
          expect(response).to have_gitlab_http_status 201
107 108 109 110 111 112 113 114
          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" }

115
          it "updates provided Runner's parameter" do
116
            post api('/runners'), token: registration_token,
Tomasz Maczukin's avatar
Tomasz Maczukin committed
117
                                  info: { param => value }
118

119
            expect(response).to have_gitlab_http_status 201
120 121 122 123 124 125 126 127 128 129
            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')
130

131
          expect(response).to have_gitlab_http_status 400
132 133 134 135 136 137
        end
      end

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

139
          expect(response).to have_gitlab_http_status 403
140 141 142 143 144 145 146 147
        end
      end

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

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

149
          expect(response).to have_gitlab_http_status 204
150 151
          expect(Ci::Runner.count).to eq(0)
        end
152 153 154 155 156

        it_behaves_like '412 response' do
          let(:request) { api('/runners') }
          let(:params) { { token: runner.token } }
        end
157 158
      end
    end
159 160 161 162 163 164 165 166

    describe 'POST /api/v4/runners/verify' do
      let(:runner) { create(:ci_runner) }

      context 'when no token is provided' do
        it 'returns 400 error' do
          post api('/runners/verify')

167
          expect(response).to have_gitlab_http_status :bad_request
168 169 170 171 172 173 174
        end
      end

      context 'when invalid token is provided' do
        it 'returns 403 error' do
          post api('/runners/verify'), token: 'invalid-token'

175
          expect(response).to have_gitlab_http_status 403
176 177 178 179
        end
      end

      context 'when valid token is provided' do
180
        it 'verifies Runner credentials' do
181 182
          post api('/runners/verify'), token: runner.token

183
          expect(response).to have_gitlab_http_status 200
184 185 186
        end
      end
    end
187
  end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
188 189

  describe '/api/v4/jobs' do
190
    let(:project) { create(:project, shared_runners_enabled: false) }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
191 192
    let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
    let(:runner) { create(:ci_runner) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
193
    let(:job) do
Tomasz Maczukin's avatar
Tomasz Maczukin committed
194 195 196
      create(:ci_build, :artifacts, :extended_options,
             pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate")
    end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
197

198
    before do
199 200
      stub_artifacts_object_storage
      job
201 202
      project.runners << runner
    end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
203 204 205 206 207 208

    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)' }

Kamil Trzcinski's avatar
Kamil Trzcinski committed
209 210 211
      before do
        stub_container_registry_config(enabled: false)
      end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
212 213

      shared_examples 'no jobs available' do
214 215 216
        before do
          request_job
        end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
217 218 219 220

        context 'when runner sends version in User-Agent' do
          context 'for stable version' do
            it 'gives 204 and set X-GitLab-Last-Update' do
221
              expect(response).to have_gitlab_http_status(204)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
222 223 224 225 226 227 228 229
              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
230
              expect(response).to have_gitlab_http_status(204)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
231 232 233 234 235 236 237 238 239
              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
240
              expect(response).to have_gitlab_http_status(204)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
241 242 243 244
              expect(response.header['X-GitLab-Last-Update']).to eq(new_update)
            end
          end

245
          context 'when beta version is sent' do
Tomasz Maczukin's avatar
Tomasz Maczukin committed
246
            let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' }
247

248
            it { expect(response).to have_gitlab_http_status(204) }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
249 250
          end

251
          context 'when pre-9-0 version is sent' do
Tomasz Maczukin's avatar
Tomasz Maczukin committed
252
            let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' }
253

254
            it { expect(response).to have_gitlab_http_status(204) }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
255 256
          end

257
          context 'when pre-9-0 beta version is sent' do
Tomasz Maczukin's avatar
Tomasz Maczukin committed
258
            let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' }
259

260
            it { expect(response).to have_gitlab_http_status(204) }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
261 262 263 264 265 266 267
          end
        end
      end

      context 'when no token is provided' do
        it 'returns 400 error' do
          post api('/jobs/request')
268

269
          expect(response).to have_gitlab_http_status 400
Tomasz Maczukin's avatar
Tomasz Maczukin committed
270 271 272 273 274 275
        end
      end

      context 'when invalid token is provided' do
        it 'returns 403 error' do
          post api('/jobs/request'), token: 'invalid'
276

277
          expect(response).to have_gitlab_http_status 403
Tomasz Maczukin's avatar
Tomasz Maczukin committed
278 279 280 281 282 283 284
        end
      end

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

285
          it 'returns 204 error' do
Tomasz Maczukin's avatar
Tomasz Maczukin committed
286
            request_job
287

288
            expect(response).to have_gitlab_http_status 204
Tomasz Maczukin's avatar
Tomasz Maczukin committed
289 290 291 292
          end
        end

        context 'when jobs are finished' do
293 294 295
          before do
            job.success
          end
296

Tomasz Maczukin's avatar
Tomasz Maczukin committed
297 298 299 300 301 302 303 304 305 306 307 308 309 310
          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) }
311

Tomasz Maczukin's avatar
Tomasz Maczukin committed
312 313 314 315
          it_behaves_like 'no jobs available'
        end

        context 'when there is a pending job' do
316 317 318 319 320 321
          let(:expected_job_info) do
            { 'name' => job.name,
              'stage' => job.stage,
              'project_id' => job.project.id,
              'project_name' => job.project.name }
          end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
322

323 324 325 326 327
          let(:expected_git_info) do
            { 'repo_url' => job.repo_url,
              'ref' => job.ref,
              'sha' => job.sha,
              'before_sha' => job.before_sha,
Tomasz Maczukin's avatar
Tomasz Maczukin committed
328
              'ref_type' => 'branch' }
329
          end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
330

331 332 333 334 335 336 337 338 339 340 341 342
          let(:expected_steps) do
            [{ 'name' => 'script',
               'script' => %w(ls date),
               'timeout' => job.timeout,
               'when' => 'on_success',
               'allow_failure' => false },
             { 'name' => 'after_script',
               'script' => %w(ls date),
               'timeout' => job.timeout,
               'when' => 'always',
               'allow_failure' => true }]
          end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
343

344
          let(:expected_variables) do
345 346
            [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
             { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
347 348
             { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }]
          end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
349

350
          let(:expected_artifacts) do
351 352 353 354 355 356
            [{ 'name' => 'artifacts_file',
               'untracked' => false,
               'paths' => %w(out/),
               'when' => 'always',
               'expire_in' => '7d' }]
          end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
357

358 359 360
          let(:expected_cache) do
            [{ 'key' => 'cache_key',
               'untracked' => false,
361 362
               'paths' => ['vendor/*'],
               'policy' => 'pull-push' }]
363 364
          end

365 366
          let(:expected_features) { { 'trace_sections' => true } }

Tomasz Maczukin's avatar
Tomasz Maczukin committed
367
          it 'picks a job' do
Tomasz Maczukin's avatar
Tomasz Maczukin committed
368
            request_job info: { platform: :darwin }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
369

370
            expect(response).to have_gitlab_http_status(201)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
371 372
            expect(response.headers).not_to have_key('X-GitLab-Last-Update')
            expect(runner.reload.platform).to eq('darwin')
373 374
            expect(json_response['id']).to eq(job.id)
            expect(json_response['token']).to eq(job.token)
375 376
            expect(json_response['job_info']).to eq(expected_job_info)
            expect(json_response['git_info']).to eq(expected_git_info)
377 378 379 380 381
            expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' })
            expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
                                                       'alias' => nil, 'command' => nil },
                                                     { 'name' => 'docker:dind', 'entrypoint' => '/bin/sh',
                                                       'alias' => 'docker', 'command' => 'sleep 30' }])
382 383
            expect(json_response['steps']).to eq(expected_steps)
            expect(json_response['artifacts']).to eq(expected_artifacts)
384
            expect(json_response['cache']).to eq(expected_cache)
385
            expect(json_response['variables']).to include(*expected_variables)
386
            expect(json_response['features']).to eq(expected_features)
387 388 389
          end

          context 'when job is made for tag' do
390
            let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
391 392 393

            it 'sets branch as ref_type' do
              request_job
394

395
              expect(response).to have_gitlab_http_status(201)
396 397 398 399 400 401 402
              expect(json_response['git_info']['ref_type']).to eq('tag')
            end
          end

          context 'when job is made for branch' do
            it 'sets tag as ref_type' do
              request_job
403

404
              expect(response).to have_gitlab_http_status(201)
405 406
              expect(json_response['git_info']['ref_type']).to eq('branch')
            end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
407 408 409 410 411 412 413 414 415 416
          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" }

417
              it "updates provided Runner's parameter" do
Tomasz Maczukin's avatar
Tomasz Maczukin committed
418
                request_job info: { param => value }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
419

420
                expect(response).to have_gitlab_http_status(201)
421
                expect(runner.reload.read_attribute(param.to_sym)).to eq(value)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
422 423 424 425 426 427
              end
            end
          end

          context 'when concurrently updating a job' do
            before do
428 429
              expect_any_instance_of(Ci::Build).to receive(:run!)
                  .and_raise(ActiveRecord::StaleObjectError.new(nil, nil))
Tomasz Maczukin's avatar
Tomasz Maczukin committed
430 431 432 433
            end

            it 'returns a conflict' do
              request_job
434

435
              expect(response).to have_gitlab_http_status(409)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
436 437 438 439 440
              expect(response.headers).not_to have_key('X-GitLab-Last-Update')
            end
          end

          context 'when project and pipeline have multiple jobs' do
441 442
            let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
            let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
443
            let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
444

445 446 447 448 449 450 451 452
            before do
              job.success
              job2.success
            end

            it 'returns dependent jobs' do
              request_job

453
              expect(response).to have_gitlab_http_status(201)
454 455
              expect(json_response['id']).to eq(test_job.id)
              expect(json_response['dependencies'].count).to eq(2)
456 457 458 459 460 461 462
              expect(json_response['dependencies']).to include(
                { 'id' => job.id, 'name' => job.name, 'token' => job.token },
                { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token })
            end
          end

          context 'when pipeline have jobs with artifacts' do
463
            let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
464 465 466 467 468 469 470 471 472
            let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }

            before do
              job.success
            end

            it 'returns dependent jobs' do
              request_job

473
              expect(response).to have_gitlab_http_status(201)
474 475 476 477 478
              expect(json_response['id']).to eq(test_job.id)
              expect(json_response['dependencies'].count).to eq(1)
              expect(json_response['dependencies']).to include(
                { 'id' => job.id, 'name' => job.name, 'token' => job.token,
                  'artifacts_file' => { 'filename' => 'ci_build_artifacts.zip', 'size' => 106365 } })
479 480 481 482
            end
          end

          context 'when explicit dependencies are defined' do
483 484
            let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
            let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
485 486 487 488 489 490 491 492 493 494
            let!(:test_job) do
              create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
                                stage: 'deploy', stage_idx: 1,
                                options: { dependencies: [job2.name] })
            end

            before do
              job.success
              job2.success
            end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
495 496 497 498

            it 'returns dependent jobs' do
              request_job

499
              expect(response).to have_gitlab_http_status(201)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
500
              expect(json_response['id']).to eq(test_job.id)
501
              expect(json_response['dependencies'].count).to eq(1)
502
              expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
503 504 505
            end
          end

506
          context 'when dependencies is an empty array' do
507 508
            let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
            let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
509 510 511 512 513 514 515 516 517 518 519 520 521 522
            let!(:empty_dependencies_job) do
              create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
                                stage: 'deploy', stage_idx: 1,
                                options: { dependencies: [] })
            end

            before do
              job.success
              job2.success
            end

            it 'returns an empty array' do
              request_job

523
              expect(response).to have_gitlab_http_status(201)
524 525 526 527 528
              expect(json_response['id']).to eq(empty_dependencies_job.id)
              expect(json_response['dependencies'].count).to eq(0)
            end
          end

Tomasz Maczukin's avatar
Tomasz Maczukin committed
529
          context 'when job has no tags' do
530 531 532
            before do
              job.update(tags: [])
            end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
533 534

            context 'when runner is allowed to pick untagged jobs' do
535 536 537
              before do
                runner.update_column(:run_untagged, true)
              end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
538 539 540

              it 'picks job' do
                request_job
541

542
                expect(response).to have_gitlab_http_status 201
Tomasz Maczukin's avatar
Tomasz Maczukin committed
543 544 545 546
              end
            end

            context 'when runner is not allowed to pick untagged jobs' do
547 548 549
              before do
                runner.update_column(:run_untagged, false)
              end
550

Tomasz Maczukin's avatar
Tomasz Maczukin committed
551 552 553 554 555
              it_behaves_like 'no jobs available'
            end
          end

          context 'when triggered job is available' do
556
            let(:expected_variables) do
557 558 559
              [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
               { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
               { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true },
560 561 562 563 564
               { '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

565 566 567
            let(:trigger) { create(:ci_trigger, project: project) }
            let!(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, builds: [job], trigger: trigger) }

Tomasz Maczukin's avatar
Tomasz Maczukin committed
568 569 570 571
            before do
              project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
            end

572 573 574
            shared_examples 'expected variables behavior' do
              it 'returns variables for triggers' do
                request_job
Tomasz Maczukin's avatar
Tomasz Maczukin committed
575

576
                expect(response).to have_gitlab_http_status(201)
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
                expect(json_response['variables']).to include(*expected_variables)
              end
            end

            context 'when variables are stored in trigger_request' do
              before do
                trigger_request.update_attribute(:variables, { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } )
              end

              it_behaves_like 'expected variables behavior'
            end

            context 'when variables are stored in pipeline_variables' do
              before do
                create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1')
              end

              it_behaves_like 'expected variables behavior'
Tomasz Maczukin's avatar
Tomasz Maczukin committed
595 596 597 598 599 600
            end
          end

          describe 'registry credentials support' do
            let(:registry_url) { 'registry.example.com:5005' }
            let(:registry_credentials) do
Tomasz Maczukin's avatar
Tomasz Maczukin committed
601 602 603 604
              { 'type' => 'registry',
                'url' => registry_url,
                'username' => 'gitlab-ci-token',
                'password' => job.token }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
605 606 607
            end

            context 'when registry is enabled' do
608 609 610
              before do
                stub_container_registry_config(enabled: true, host_port: registry_url)
              end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
611 612 613

              it 'sends registry credentials key' do
                request_job
614

Tomasz Maczukin's avatar
Tomasz Maczukin committed
615 616 617 618 619 620
                expect(json_response).to have_key('credentials')
                expect(json_response['credentials']).to include(registry_credentials)
              end
            end

            context 'when registry is disabled' do
621 622 623
              before do
                stub_container_registry_config(enabled: false, host_port: registry_url)
              end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
624 625 626

              it 'does not send registry credentials' do
                request_job
627

Tomasz Maczukin's avatar
Tomasz Maczukin committed
628 629 630 631 632 633 634 635 636
                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)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
637
          post api('/jobs/request'), new_params, { 'User-Agent' => user_agent }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
638 639 640
        end
      end
    end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
641 642 643 644

    describe 'PUT /api/v4/jobs/:id' do
      let(:job) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }

645 646 647
      before do
        job.run!
      end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
648 649 650 651

      context 'when status is given' do
        it 'mark job as succeeded' do
          update_job(state: 'success')
652

653 654
          job.reload
          expect(job).to be_success
Tomasz Maczukin's avatar
Tomasz Maczukin committed
655 656 657 658
        end

        it 'mark job as failed' do
          update_job(state: 'failed')
659

660 661 662
          job.reload
          expect(job).to be_failed
          expect(job).to be_unknown_failure
Tomasz Maczukin's avatar
Tomasz Maczukin committed
663
        end
664

665 666 667 668 669 670 671 672
        context 'when failure_reason is script_failure' do
          before do
            update_job(state: 'failed', failure_reason: 'script_failure')
            job.reload
          end

          it { expect(job).to be_script_failure }
        end
673

674 675 676 677
        context 'when failure_reason is runner_system_failure' do
          before do
            update_job(state: 'failed', failure_reason: 'runner_system_failure')
            job.reload
678
          end
679 680

          it { expect(job).to be_runner_system_failure }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
681 682 683 684 685 686 687
        end
      end

      context 'when tace is given' do
        it 'updates a running build' do
          update_job(trace: 'BUILD TRACE UPDATED')

688
          expect(response).to have_gitlab_http_status(200)
689
          expect(job.reload.trace.raw).to eq 'BUILD TRACE UPDATED'
Tomasz Maczukin's avatar
Tomasz Maczukin committed
690 691 692 693 694 695
        end
      end

      context 'when no trace is given' do
        it 'does not override trace information' do
          update_job
696

697
          expect(job.reload.trace.raw).to eq 'BUILD TRACE'
Tomasz Maczukin's avatar
Tomasz Maczukin committed
698 699 700 701 702 703 704 705
        end
      end

      context 'when job has been erased' do
        let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }

        it 'responds with forbidden' do
          update_job
706

707
          expect(response).to have_gitlab_http_status(403)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
708 709 710 711 712 713 714 715
        end
      end

      def update_job(token = job.token, **params)
        new_params = params.merge(token: token)
        put api("/jobs/#{job.id}"), new_params
      end
    end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
716 717 718 719 720 721 722

    describe 'PATCH /api/v4/jobs/:id/trace' do
      let(:job) { create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) }
      let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } }
      let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
      let(:update_interval) { 10.seconds.to_i }

723 724 725
      before do
        initial_patch_the_trace
      end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
726 727 728 729

      context 'when request is valid' do
        it 'gets correct response' do
          expect(response.status).to eq 202
730
          expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
Tomasz Maczukin's avatar
Tomasz Maczukin committed
731 732 733 734 735
          expect(response.header).to have_key 'Range'
          expect(response.header).to have_key 'Job-Status'
        end

        context 'when job has been updated recently' do
736
          it { expect { patch_the_trace }.not_to change { job.updated_at }}
Tomasz Maczukin's avatar
Tomasz Maczukin committed
737 738 739

          it "changes the job's trace" do
            patch_the_trace
740

741
            expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
Tomasz Maczukin's avatar
Tomasz Maczukin committed
742 743 744
          end

          context 'when Runner makes a force-patch' do
745
            it { expect { force_patch_the_trace }.not_to change { job.updated_at }}
Tomasz Maczukin's avatar
Tomasz Maczukin committed
746 747 748

            it "doesn't change the build.trace" do
              force_patch_the_trace
749

750
              expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
Tomasz Maczukin's avatar
Tomasz Maczukin committed
751 752 753 754 755 756 757 758 759 760 761
            end
          end
        end

        context 'when job was not updated recently' do
          let(:update_interval) { 15.minutes.to_i }

          it { expect { patch_the_trace }.to change { job.updated_at } }

          it 'changes the job.trace' do
            patch_the_trace
762

763
            expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
Tomasz Maczukin's avatar
Tomasz Maczukin committed
764 765 766 767 768 769 770
          end

          context 'when Runner makes a force-patch' do
            it { expect { force_patch_the_trace }.to change { job.updated_at } }

            it "doesn't change the job.trace" do
              force_patch_the_trace
771

772
              expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
Tomasz Maczukin's avatar
Tomasz Maczukin committed
773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796
            end
          end
        end

        context 'when project for the build has been deleted' do
          let(:job) do
            create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) do |job|
              job.project.update(pending_delete: true)
            end
          end

          it 'responds with forbidden' do
            expect(response.status).to eq(403)
          end
        end
      end

      context 'when Runner makes a force-patch' do
        before do
          force_patch_the_trace
        end

        it 'gets correct response' do
          expect(response.status).to eq 202
797
          expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
Tomasz Maczukin's avatar
Tomasz Maczukin committed
798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836
          expect(response.header).to have_key 'Range'
          expect(response.header).to have_key 'Job-Status'
        end
      end

      context 'when content-range start is too big' do
        let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }

        it 'gets 416 error response with range headers' do
          expect(response.status).to eq 416
          expect(response.header).to have_key 'Range'
          expect(response.header['Range']).to eq '0-11'
        end
      end

      context 'when content-range start is too small' do
        let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }

        it 'gets 416 error response with range headers' do
          expect(response.status).to eq 416
          expect(response.header).to have_key 'Range'
          expect(response.header['Range']).to eq '0-11'
        end
      end

      context 'when Content-Range header is missing' do
        let(:headers_with_range) { headers }

        it { expect(response.status).to eq 400 }
      end

      context 'when job has been errased' do
        let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }

        it { expect(response.status).to eq 403 }
      end

      def patch_the_trace(content = ' appended', request_headers = nil)
        unless request_headers
837 838 839 840 841
          job.trace.read do |stream|
            offset = stream.size
            limit = offset + content.length - 1
            request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
          end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857
        end

        Timecop.travel(job.updated_at + update_interval) do
          patch api("/jobs/#{job.id}/trace"), content, request_headers
          job.reload
        end
      end

      def initial_patch_the_trace
        patch_the_trace(' appended', headers_with_range)
      end

      def force_patch_the_trace
        2.times { patch_the_trace('') }
      end
    end
858 859

    describe 'artifacts' do
860
      let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
861 862 863
      let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
      let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
      let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) }
864 865
      let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
      let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
866

Kamil Trzcinski's avatar
Kamil Trzcinski committed
867 868 869 870
      before do
        stub_artifacts_object_storage
        job.run!
      end
871 872 873 874 875 876

      describe 'POST /api/v4/jobs/:id/artifacts/authorize' do
        context 'when using token as parameter' do
          it 'authorizes posting artifacts to running job' do
            authorize_artifacts_with_token_in_params

877
            expect(response).to have_gitlab_http_status(200)
878 879 880 881 882 883
            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
            expect(json_response['TempPath']).not_to be_nil
          end

          it 'fails to post too large artifact' do
            stub_application_setting(max_artifacts_size: 0)
884

885 886
            authorize_artifacts_with_token_in_params(filesize: 100)

887
            expect(response).to have_gitlab_http_status(413)
888 889 890 891 892 893 894
          end
        end

        context 'when using token as header' do
          it 'authorizes posting artifacts to running job' do
            authorize_artifacts_with_token_in_headers

895
            expect(response).to have_gitlab_http_status(200)
896 897 898 899 900 901
            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
            expect(json_response['TempPath']).not_to be_nil
          end

          it 'fails to post too large artifact' do
            stub_application_setting(max_artifacts_size: 0)
902

903 904
            authorize_artifacts_with_token_in_headers(filesize: 100)

905
            expect(response).to have_gitlab_http_status(413)
906 907 908 909 910 911
          end
        end

        context 'when using runners token' do
          it 'fails to authorize artifacts posting' do
            authorize_artifacts(token: job.project.runners_token)
912

913
            expect(response).to have_gitlab_http_status(403)
914 915 916 917 918
          end
        end

        it 'reject requests that did not go through gitlab-workhorse' do
          headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
919

920
          authorize_artifacts
921

922
          expect(response).to have_gitlab_http_status(500)
923 924 925 926 927
        end

        context 'authorization token is invalid' do
          it 'responds with forbidden' do
            authorize_artifacts(token: 'invalid', filesize: 100 )
928

929
            expect(response).to have_gitlab_http_status(403)
930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945
          end
        end

        def authorize_artifacts(params = {}, request_headers = headers)
          post api("/jobs/#{job.id}/artifacts/authorize"), params, request_headers
        end

        def authorize_artifacts_with_token_in_params(params = {}, request_headers = headers)
          params = params.merge(token: job.token)
          authorize_artifacts(params, request_headers)
        end

        def authorize_artifacts_with_token_in_headers(params = {}, request_headers = headers_with_token)
          authorize_artifacts(params, request_headers)
        end
      end
946 947 948 949 950

      describe 'POST /api/v4/jobs/:id/artifacts' do
        context 'when artifacts are being stored inside of tmp path' do
          before do
            # by configuring this path we allow to pass temp file from any path
951
            allow(JobArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
952 953 954 955 956 957 958 959 960 961 962
          end

          context 'when job has been erased' do
            let(:job) { create(:ci_build, erased_at: Time.now) }

            before do
              upload_artifacts(file_upload, headers_with_token)
            end

            it 'responds with forbidden' do
              upload_artifacts(file_upload, headers_with_token)
963

964
              expect(response).to have_gitlab_http_status(403)
965 966 967 968 969 970
            end
          end

          context 'when job is running' do
            shared_examples 'successful artifacts upload' do
              it 'updates successfully' do
971
                expect(response).to have_gitlab_http_status(201)
972 973 974 975
              end
            end

            context 'when uses regular file post' do
976 977 978
              before do
                upload_artifacts(file_upload, headers_with_token, false)
              end
979 980 981 982 983

              it_behaves_like 'successful artifacts upload'
            end

            context 'when uses accelerated file post' do
984 985 986
              before do
                upload_artifacts(file_upload, headers_with_token, true)
              end
987 988 989 990 991 992 993

              it_behaves_like 'successful artifacts upload'
            end

            context 'when using runners token' do
              it 'responds with forbidden' do
                upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token))
994

995
                expect(response).to have_gitlab_http_status(403)
996 997 998 999 1000 1001 1002
              end
            end
          end

          context 'when artifacts file is too large' do
            it 'fails to post too large artifact' do
              stub_application_setting(max_artifacts_size: 0)
1003

1004
              upload_artifacts(file_upload, headers_with_token)
1005

1006
              expect(response).to have_gitlab_http_status(413)
1007 1008 1009 1010 1011 1012
            end
          end

          context 'when artifacts post request does not contain file' do
            it 'fails to post artifacts without file' do
              post api("/jobs/#{job.id}/artifacts"), {}, headers_with_token
1013

1014
              expect(response).to have_gitlab_http_status(400)
1015 1016 1017 1018 1019 1020
            end
          end

          context 'GitLab Workhorse is not configured' do
            it 'fails to post artifacts without GitLab-Workhorse' do
              post api("/jobs/#{job.id}/artifacts"), { token: job.token }, {}
1021

1022
              expect(response).to have_gitlab_http_status(403)
1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035
            end
          end

          context 'when setting an expire date' do
            let(:default_artifacts_expire_in) {}
            let(:post_data) do
              { 'file.path' => file_upload.path,
                'file.name' => file_upload.original_filename,
                'expire_in' => expire_in }
            end

            before do
              stub_application_setting(default_artifacts_expire_in: default_artifacts_expire_in)
1036

1037 1038 1039 1040 1041 1042 1043
              post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token)
            end

            context 'when an expire_in is given' do
              let(:expire_in) { '7 days' }

              it 'updates when specified' do
1044
                expect(response).to have_gitlab_http_status(201)
1045
                expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now)
1046 1047 1048 1049 1050 1051 1052
              end
            end

            context 'when no expire_in is given' do
              let(:expire_in) { nil }

              it 'ignores if not specified' do
1053
                expect(response).to have_gitlab_http_status(201)
1054
                expect(job.reload.artifacts_expire_at).to be_nil
1055 1056 1057 1058 1059 1060 1061
              end

              context 'with application default' do
                context 'when default is 5 days' do
                  let(:default_artifacts_expire_in) { '5 days' }

                  it 'sets to application default' do
1062
                    expect(response).to have_gitlab_http_status(201)
1063
                    expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now)
1064 1065 1066 1067 1068 1069 1070
                  end
                end

                context 'when default is 0' do
                  let(:default_artifacts_expire_in) { '0' }

                  it 'does not set expire_in' do
1071
                    expect(response).to have_gitlab_http_status(201)
1072
                    expect(job.reload.artifacts_expire_at).to be_nil
1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099
                  end
                end
              end
            end
          end

          context 'posts artifacts file and metadata file' do
            let!(:artifacts) { file_upload }
            let!(:metadata) { file_upload2 }

            let(:stored_artifacts_file) { job.reload.artifacts_file.file }
            let(:stored_metadata_file) { job.reload.artifacts_metadata.file }
            let(:stored_artifacts_size) { job.reload.artifacts_size }

            before do
              post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token)
            end

            context 'when posts data accelerated by workhorse is correct' do
              let(:post_data) do
                { 'file.path' => artifacts.path,
                  'file.name' => artifacts.original_filename,
                  'metadata.path' => metadata.path,
                  'metadata.name' => metadata.original_filename }
              end

              it 'stores artifacts and artifacts metadata' do
1100
                expect(response).to have_gitlab_http_status(201)
1101 1102
                expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
                expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
1103
                expect(stored_artifacts_size).to eq(72821)
1104 1105 1106 1107 1108 1109 1110 1111 1112
              end
            end

            context 'when there is no artifacts file in post data' do
              let(:post_data) do
                { 'metadata' => metadata }
              end

              it 'is expected to respond with bad request' do
1113
                expect(response).to have_gitlab_http_status(400)
1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127
              end

              it 'does not store metadata' do
                expect(stored_metadata_file).to be_nil
              end
            end
          end
        end

        context 'when artifacts are being stored outside of tmp path' do
          before do
            # by configuring this path we allow to pass file from @tmpdir only
            # but all temporary files are stored in system tmp directory
            @tmpdir = Dir.mktmpdir
1128
            allow(JobArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
1129 1130
          end

1131 1132 1133
          after do
            FileUtils.remove_entry @tmpdir
          end
1134 1135 1136

          it' "fails to post artifacts for outside of tmp path"' do
            upload_artifacts(file_upload, headers_with_token)
1137

1138
            expect(response).to have_gitlab_http_status(400)
1139 1140 1141 1142
          end
        end

        def upload_artifacts(file, headers = {}, accelerated = true)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
1143 1144 1145
          params = if accelerated
                     { 'file.path' => file.path, 'file.name' => file.original_filename }
                   else
1146
                     { 'file' => file }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
1147
                   end
1148 1149 1150
          post api("/jobs/#{job.id}/artifacts"), params, headers
        end
      end
1151 1152 1153 1154 1155

      describe 'GET /api/v4/jobs/:id/artifacts' do
        let(:token) { job.token }

        context 'when job has artifacts' do
1156
          let(:job) { create(:ci_build) }
Micaël Bergeron's avatar
Micaël Bergeron committed
1157
          let(:store) { JobArtifactUploader::Store::LOCAL }
1158 1159

          before do
1160
            create(:ci_job_artifact, :archive, file_store: store, job: job)
1161 1162 1163

            download_artifact
          end
1164 1165

          context 'when using job token' do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1166 1167 1168 1169 1170
            context 'when artifacts are stored locally' do
              let(:download_headers) do
                { 'Content-Transfer-Encoding' => 'binary',
                  'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
              end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1171

Kamil Trzcinski's avatar
Kamil Trzcinski committed
1172
              it 'download artifacts' do
1173
                expect(response).to have_gitlab_http_status(200)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1174 1175 1176 1177 1178
                expect(response.headers).to include download_headers
              end
            end

            context 'when artifacts are stored remotely' do
Micaël Bergeron's avatar
Micaël Bergeron committed
1179
              let(:store) { JobArtifactUploader::Store::REMOTE }
1180
              let!(:job) { create(:ci_build) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1181

Kamil Trzcinski's avatar
Kamil Trzcinski committed
1182
              it 'download artifacts' do
1183
                expect(response).to have_gitlab_http_status(302)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1184
              end
1185 1186 1187 1188 1189 1190 1191
            end
          end

          context 'when using runnners token' do
            let(:token) { job.project.runners_token }

            it 'responds with forbidden' do
1192
              expect(response).to have_gitlab_http_status(403)
1193 1194 1195 1196 1197 1198
            end
          end
        end

        context 'when job does not has artifacts' do
          it 'responds with not found' do
1199 1200
            download_artifact

1201
            expect(response).to have_gitlab_http_status(404)
1202 1203 1204 1205 1206
          end
        end

        def download_artifact(params = {}, request_headers = headers)
          params = params.merge(token: token)
1207 1208
          job.reload

1209 1210 1211
          get api("/jobs/#{job.id}/artifacts"), params, request_headers
        end
      end
1212
    end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
1213
  end
Tomasz Maczukin's avatar
Tomasz Maczukin committed
1214
end