BigW Consortium Gitlab

runner_spec.rb 39.2 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 20 21 22 23 24 25
          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'
26

27 28 29 30 31 32 33 34 35 36 37 38 39 40
          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
41
          expect(runner.token).not_to eq(registration_token)
42 43 44 45 46 47 48 49 50 51
        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)
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 committed
61
                                description: 'server.hostname'
62 63 64 65 66 67 68 69 70

          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 committed
71
                                tag_list: 'tag1, tag2'
72 73 74 75 76 77 78 79 80 81

          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 committed
82 83
                                  run_untagged: false,
                                  tag_list: ['tag']
84 85 86 87 88 89 90 91 92 93

            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 committed
94
                                  run_untagged: false
95 96 97 98 99 100 101 102 103

            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 committed
104
                                locked: true
105 106 107 108 109 110 111 112 113 114

          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" }

115
          it "updates provided Runner's parameter" do
116
            post api('/runners'), token: registration_token,
Tomasz Maczukin committed
117
                                  info: { param => value }
118 119 120 121 122 123 124 125 126 127 128 129

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

131 132 133 134 135 136 137
          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'
138

139 140 141 142 143 144 145 146 147
          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
148 149

          expect(response).to have_http_status 204
150 151 152 153
          expect(Ci::Runner.count).to eq(0)
        end
      end
    end
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174

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

          expect(response).to have_http_status :bad_request
        end
      end

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

          expect(response).to have_http_status 403
        end
      end

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

          expect(response).to have_http_status 200
        end
      end
    end
182
  end
183 184 185 186 187

  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(:runner) { create(:ci_runner) }
188 189 190 191
    let!(:job) do
      create(:ci_build, :artifacts, :extended_options,
             pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate")
    end
192

193 194 195
    before do
      project.runners << runner
    end
196 197 198 199 200 201

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

202 203 204
      before do
        stub_container_registry_config(enabled: false)
      end
205 206

      shared_examples 'no jobs available' do
207 208 209
        before do
          request_job
        end
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

        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

238
          context 'when beta version is sent' do
239
            let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' }
240

241 242 243
            it { expect(response).to have_http_status(204) }
          end

244
          context 'when pre-9-0 version is sent' do
245
            let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' }
246

247 248 249
            it { expect(response).to have_http_status(204) }
          end

250
          context 'when pre-9-0 beta version is sent' do
251
            let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' }
252

253 254 255 256 257 258 259 260
            it { expect(response).to have_http_status(204) }
          end
        end
      end

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

262 263 264 265 266 267 268
          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'
269

270 271 272 273 274 275 276 277
          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) }

278
          it 'returns 204 error' do
279
            request_job
280

281
            expect(response).to have_http_status 204
282 283 284 285
          end
        end

        context 'when jobs are finished' do
286 287 288
          before do
            job.success
          end
289

290 291 292 293 294 295 296 297 298 299 300 301 302 303
          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) }
304

305 306 307 308
          it_behaves_like 'no jobs available'
        end

        context 'when there is a pending job' do
309 310 311 312 313 314
          let(:expected_job_info) do
            { 'name' => job.name,
              'stage' => job.stage,
              'project_id' => job.project.id,
              'project_name' => job.project.name }
          end
315

316 317 318 319 320
          let(:expected_git_info) do
            { 'repo_url' => job.repo_url,
              'ref' => job.ref,
              'sha' => job.sha,
              'before_sha' => job.before_sha,
Tomasz Maczukin committed
321
              'ref_type' => 'branch' }
322
          end
323

324 325 326 327 328 329 330 331 332 333 334 335
          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
336

337
          let(:expected_variables) do
338 339
            [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
             { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
340 341
             { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }]
          end
342

343
          let(:expected_artifacts) do
344 345 346 347 348 349
            [{ 'name' => 'artifacts_file',
               'untracked' => false,
               'paths' => %w(out/),
               'when' => 'always',
               'expire_in' => '7d' }]
          end
350

351 352 353
          let(:expected_cache) do
            [{ 'key' => 'cache_key',
               'untracked' => false,
354 355
               'paths' => ['vendor/*'],
               'policy' => 'pull-push' }]
356 357
          end

358
          it 'picks a job' do
Tomasz Maczukin committed
359
            request_job info: { platform: :darwin }
360 361 362 363

            expect(response).to have_http_status(201)
            expect(response.headers).not_to have_key('X-GitLab-Last-Update')
            expect(runner.reload.platform).to eq('darwin')
364 365
            expect(json_response['id']).to eq(job.id)
            expect(json_response['token']).to eq(job.token)
366 367
            expect(json_response['job_info']).to eq(expected_job_info)
            expect(json_response['git_info']).to eq(expected_git_info)
368 369 370 371 372
            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' }])
373 374
            expect(json_response['steps']).to eq(expected_steps)
            expect(json_response['artifacts']).to eq(expected_artifacts)
375
            expect(json_response['cache']).to eq(expected_cache)
376
            expect(json_response['variables']).to include(*expected_variables)
377 378 379 380 381 382 383
          end

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

            it 'sets branch as ref_type' do
              request_job
384

385 386 387 388 389 390 391 392
              expect(response).to have_http_status(201)
              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
393

394 395 396
              expect(response).to have_http_status(201)
              expect(json_response['git_info']['ref_type']).to eq('branch')
            end
397 398 399 400 401 402 403 404 405 406
          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" }

407
              it "updates provided Runner's parameter" do
Tomasz Maczukin committed
408
                request_job info: { param => value }
409 410

                expect(response).to have_http_status(201)
411
                expect(runner.reload.read_attribute(param.to_sym)).to eq(value)
412 413 414 415 416 417
              end
            end
          end

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

            it 'returns a conflict' do
              request_job
424

425 426 427 428 429 430
              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
431 432 433
            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) }
            let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
434

435 436 437 438 439 440 441 442 443 444 445
            before do
              job.success
              job2.success
            end

            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['dependencies'].count).to eq(2)
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
              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
            let!(:job) { create(:ci_build_tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
            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

              expect(response).to have_http_status(201)
              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 } })
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
            end
          end

          context 'when explicit dependencies are defined' do
            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) }
            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
485 486 487 488 489 490

            it 'returns dependent jobs' do
              request_job

              expect(response).to have_http_status(201)
              expect(json_response['id']).to eq(test_job.id)
491
              expect(json_response['dependencies'].count).to eq(1)
492
              expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token)
493 494 495
            end
          end

496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518
          context 'when dependencies is an empty array' do
            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) }
            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

              expect(response).to have_http_status(201)
              expect(json_response['id']).to eq(empty_dependencies_job.id)
              expect(json_response['dependencies'].count).to eq(0)
            end
          end

519
          context 'when job has no tags' do
520 521 522
            before do
              job.update(tags: [])
            end
523 524

            context 'when runner is allowed to pick untagged jobs' do
525 526 527
              before do
                runner.update_column(:run_untagged, true)
              end
528 529 530

              it 'picks job' do
                request_job
531

532 533 534 535 536
                expect(response).to have_http_status 201
              end
            end

            context 'when runner is not allowed to pick untagged jobs' do
537 538 539
              before do
                runner.update_column(:run_untagged, false)
              end
540

541 542 543 544 545
              it_behaves_like 'no jobs available'
            end
          end

          context 'when triggered job is available' do
546
            let(:expected_variables) do
547 548 549
              [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
               { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
               { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true },
550 551 552 553 554
               { '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

555 556 557 558 559 560 561 562 563 564
            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)
565
              expect(json_response['variables']).to include(*expected_variables)
566 567 568 569 570 571
            end
          end

          describe 'registry credentials support' do
            let(:registry_url) { 'registry.example.com:5005' }
            let(:registry_credentials) do
Tomasz Maczukin committed
572 573 574 575
              { 'type' => 'registry',
                'url' => registry_url,
                'username' => 'gitlab-ci-token',
                'password' => job.token }
576 577 578
            end

            context 'when registry is enabled' do
579 580 581
              before do
                stub_container_registry_config(enabled: true, host_port: registry_url)
              end
582 583 584

              it 'sends registry credentials key' do
                request_job
585

586 587 588 589 590 591
                expect(json_response).to have_key('credentials')
                expect(json_response['credentials']).to include(registry_credentials)
              end
            end

            context 'when registry is disabled' do
592 593 594
              before do
                stub_container_registry_config(enabled: false, host_port: registry_url)
              end
595 596 597

              it 'does not send registry credentials' do
                request_job
598

599 600 601 602 603 604 605 606 607
                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 committed
608
          post api('/jobs/request'), new_params, { 'User-Agent' => user_agent }
609 610 611
        end
      end
    end
Tomasz Maczukin committed
612 613 614 615

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

616 617 618
      before do
        job.run!
      end
Tomasz Maczukin committed
619 620 621 622

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

Tomasz Maczukin committed
624 625 626 627 628
          expect(job.reload.status).to eq 'success'
        end

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

Tomasz Maczukin committed
630 631 632 633 634 635 636 637 638
          expect(job.reload.status).to eq 'failed'
        end
      end

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

          expect(response).to have_http_status(200)
639
          expect(job.reload.trace.raw).to eq 'BUILD TRACE UPDATED'
Tomasz Maczukin committed
640 641 642 643 644 645
        end
      end

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

647
          expect(job.reload.trace.raw).to eq 'BUILD TRACE'
Tomasz Maczukin committed
648 649 650 651 652 653 654 655
        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
656

Tomasz Maczukin committed
657 658 659 660 661 662 663 664 665
          expect(response).to have_http_status(403)
        end
      end

      def update_job(token = job.token, **params)
        new_params = params.merge(token: token)
        put api("/jobs/#{job.id}"), new_params
      end
    end
666 667 668 669 670 671 672

    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 }

673 674 675
      before do
        initial_patch_the_trace
      end
676 677 678 679

      context 'when request is valid' do
        it 'gets correct response' do
          expect(response.status).to eq 202
680
          expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
681 682 683 684 685 686 687 688 689
          expect(response.header).to have_key 'Range'
          expect(response.header).to have_key 'Job-Status'
        end

        context 'when job has been updated recently' do
          it { expect{ patch_the_trace }.not_to change { job.updated_at }}

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

691
            expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
692 693 694 695 696 697 698
          end

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

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

700
              expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
701 702 703 704 705 706 707 708 709 710 711
            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
712

713
            expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
714 715 716 717 718 719 720
          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
721

722
              expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746
            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
747
          expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786
          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
787 788 789 790 791
          job.trace.read do |stream|
            offset = stream.size
            limit = offset + content.length - 1
            request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
          end
792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807
        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
808 809

    describe 'artifacts' do
810
      let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
811 812 813
      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) }
814 815
      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') }
816

817 818 819
      before do
        job.run!
      end
820 821 822 823 824 825 826 827 828 829 830 831 832

      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

            expect(response).to have_http_status(200)
            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)
833

834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850
            authorize_artifacts_with_token_in_params(filesize: 100)

            expect(response).to have_http_status(413)
          end
        end

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

            expect(response).to have_http_status(200)
            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)
851

852 853 854 855 856 857 858 859 860
            authorize_artifacts_with_token_in_headers(filesize: 100)

            expect(response).to have_http_status(413)
          end
        end

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

862 863 864 865 866 867
            expect(response).to have_http_status(403)
          end
        end

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

869
          authorize_artifacts
870

871 872 873 874 875 876
          expect(response).to have_http_status(500)
        end

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

878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894
            expect(response).to have_http_status(403)
          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
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911

      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
            allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
          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)
912

913 914 915 916 917 918 919 920 921 922 923 924
              expect(response).to have_http_status(403)
            end
          end

          context 'when job is running' do
            shared_examples 'successful artifacts upload' do
              it 'updates successfully' do
                expect(response).to have_http_status(201)
              end
            end

            context 'when uses regular file post' do
925 926 927
              before do
                upload_artifacts(file_upload, headers_with_token, false)
              end
928 929 930 931 932

              it_behaves_like 'successful artifacts upload'
            end

            context 'when uses accelerated file post' do
933 934 935
              before do
                upload_artifacts(file_upload, headers_with_token, true)
              end
936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951

              it_behaves_like 'successful artifacts upload'
            end

            context 'when updates artifact' do
              before do
                upload_artifacts(file_upload2, headers_with_token)
                upload_artifacts(file_upload, headers_with_token)
              end

              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))
952

953 954 955 956 957 958 959 960
                expect(response).to have_http_status(403)
              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)
961

962
              upload_artifacts(file_upload, headers_with_token)
963

964 965 966 967 968 969 970
              expect(response).to have_http_status(413)
            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
971

972 973 974 975 976 977 978
              expect(response).to have_http_status(400)
            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 }, {}
979

980 981 982 983 984 985 986 987 988 989 990 991 992 993
              expect(response).to have_http_status(403)
            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)
994

995 996 997 998 999 1000 1001 1002
              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
                expect(response).to have_http_status(201)
1003
                expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now)
1004 1005 1006 1007 1008 1009 1010 1011
              end
            end

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

              it 'ignores if not specified' do
                expect(response).to have_http_status(201)
1012
                expect(job.reload.artifacts_expire_at).to be_nil
1013 1014 1015 1016 1017 1018 1019 1020
              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
                    expect(response).to have_http_status(201)
1021
                    expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now)
1022 1023 1024 1025 1026 1027 1028 1029
                  end
                end

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

                  it 'does not set expire_in' do
                    expect(response).to have_http_status(201)
1030
                    expect(job.reload.artifacts_expire_at).to be_nil
1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088
                  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
                expect(response).to have_http_status(201)
                expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
                expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
                expect(stored_artifacts_size).to eq(71759)
              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
                expect(response).to have_http_status(400)
              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
            allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
          end

1089 1090 1091
          after do
            FileUtils.remove_entry @tmpdir
          end
1092 1093 1094

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

1096 1097 1098 1099 1100
            expect(response).to have_http_status(400)
          end
        end

        def upload_artifacts(file, headers = {}, accelerated = true)
Tomasz Maczukin committed
1101 1102 1103
          params = if accelerated
                     { 'file.path' => file.path, 'file.name' => file.original_filename }
                   else
1104
                     { 'file' => file }
Tomasz Maczukin committed
1105
                   end
1106 1107 1108
          post api("/jobs/#{job.id}/artifacts"), params, headers
        end
      end
1109 1110 1111 1112

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

1113 1114 1115
        before do
          download_artifact
        end
1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150

        context 'when job has artifacts' do
          let(:job) { create(:ci_build, :artifacts) }
          let(:download_headers) do
            { 'Content-Transfer-Encoding' => 'binary',
              'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
          end

          context 'when using job token' do
            it 'download artifacts' do
              expect(response).to have_http_status(200)
              expect(response.headers).to include download_headers
            end
          end

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

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

        context 'when job does not has artifacts' do
          it 'responds with not found' do
            expect(response).to have_http_status(404)
          end
        end

        def download_artifact(params = {}, request_headers = headers)
          params = params.merge(token: token)
          get api("/jobs/#{job.id}/artifacts"), params, request_headers
        end
      end
1151
    end
1152
  end
Tomasz Maczukin committed
1153
end