BigW Consortium Gitlab

builds_spec.rb 24.3 KB
Newer Older
1 2
require 'spec_helper'

3
describe Ci::API::API do
4 5
  include ApiHelpers

6
  let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
7
  let(:project) { FactoryGirl.create(:empty_project) }
8 9

  describe "Builds API for runners" do
10
    let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
11 12

    before do
13
      project.runners << runner
14 15 16
    end

    describe "POST /builds/register" do
17
      let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
18 19
      let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' }

20 21 22 23
      before do
        stub_container_registry_config(enabled: false)
      end

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
      shared_examples 'no builds available' do
        context 'when runner sends version in User-Agent' do
          context 'for stable version' do
            it { expect(response).to have_http_status(204) }
          end

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

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

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
      context 'when there is a pending build' do
        it 'starts a build' do
          register_builds info: { platform: :darwin }

          expect(response).to have_http_status(201)
          expect(json_response['sha']).to eq(build.sha)
          expect(runner.reload.platform).to eq("darwin")
          expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
          expect(json_response["variables"]).to include(
            { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
            { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
            { "key" => "DB_NAME", "value" => "postgres", "public" => true }
          )
        end

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

61 62
        context 'registry credentials' do
          let(:registry_credentials) do
63
            { 'type' => 'registry',
64 65 66
              'url' => 'registry.example.com:5005',
              'username' => 'gitlab-ci-token',
              'password' => build.token }
67 68
          end

69 70 71 72
          context 'when registry is enabled' do
            before do
              stub_container_registry_config(enabled: true, host_port: 'registry.example.com:5005')
            end
73

74 75
            it 'sends registry credentials key' do
              register_builds info: { platform: :darwin }
76

77 78 79
              expect(json_response).to have_key('credentials')
              expect(json_response['credentials']).to include(registry_credentials)
            end
80 81
          end

82 83 84 85 86 87 88
          context 'when registry is disabled' do
            before do
              stub_container_registry_config(enabled: false, host_port: 'registry.example.com:5005')
            end

            it 'does not send registry credentials' do
              register_builds info: { platform: :darwin }
89

90 91 92
              expect(json_response).to have_key('credentials')
              expect(json_response['credentials']).not_to include(registry_credentials)
            end
93 94
          end
        end
95 96
      end

97 98 99 100 101
      context 'when builds are finished' do
        before do
          build.success
          register_builds
        end
102 103

        it_behaves_like 'no builds available'
104 105
      end

106 107 108 109 110 111
      context 'for other project with builds' do
        before do
          build.success
          create(:ci_build, :pending)
          register_builds
        end
112 113

        it_behaves_like 'no builds available'
114 115
      end

116 117
      context 'for shared runner' do
        let(:shared_runner) { create(:ci_runner, token: "SharedRunner") }
118

119
        before do
120 121
          register_builds shared_runner.token
        end
122 123

        it_behaves_like 'no builds available'
124 125
      end

126 127 128 129 130 131
      context 'for triggered build' do
        before do
          trigger = create(:ci_trigger, project: project)
          create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [build], trigger: trigger)
          project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
        end
132

133 134 135 136 137 138 139 140 141 142 143 144 145
        it "returns variables for triggers" do
          register_builds info: { platform: :darwin }

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

148 149 150 151
      context 'with multiple builds' do
        before do
          build.success
        end
152

153
        let!(:test_build) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
154

155 156
        it "returns dependent builds" do
          register_builds info: { platform: :darwin }
157

158 159 160 161 162
          expect(response).to have_http_status(201)
          expect(json_response["id"]).to eq(test_build.id)
          expect(json_response["depends_on_builds"].count).to eq(1)
          expect(json_response["depends_on_builds"][0]).to include('id' => build.id, 'name' => 'spinach')
        end
163
      end
164 165 166 167 168 169 170 171

      %w(name version revision platform architecture).each do |param|
        context "updates runner #{param}" do
          let(:value) { "#{param}_value" }

          subject { runner.read_attribute(param.to_sym) }

          it do
172 173 174
            register_builds info: { param => value }

            expect(response).to have_http_status(201)
175 176 177 178 179
            runner.reload
            is_expected.to eq(value)
          end
        end
      end
180 181 182

      context 'when build has no tags' do
        before do
183
          build.update(tags: [])
184 185 186 187 188 189 190 191 192 193 194 195 196
        end

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

          it 'picks build' do
            register_builds

            expect(response).to have_http_status 201
          end
        end

        context 'when runner is not allowed to pick untagged builds' do
197 198
          before do
            runner.update_column(:run_untagged, false)
199 200
            register_builds
          end
201 202

          it_behaves_like 'no builds available'
203
        end
204
      end
205

206
      context 'when runner is paused' do
207
        let(:runner) { create(:ci_runner, :inactive, token: 'InactiveRunner') }
208

209 210 211 212
        it 'responds with 404' do
          register_builds

          expect(response).to have_http_status 404
213 214
        end

215 216 217 218
        it 'does not update runner info' do
          expect { register_builds }
            .not_to change { runner.reload.contacted_at }
        end
219 220
      end

221
      def register_builds(token = runner.token, **params)
Tomasz Maczukin committed
222
        post ci_api("/builds/register"), params.merge(token: token), { 'User-Agent' => user_agent }
223
      end
224 225 226
    end

    describe "PUT /builds/:id" do
227
      let(:build) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }
228

229
      before do
230
        build.run!
Valery Sizov committed
231
        put ci_api("/builds/#{build.id}"), token: runner.token
232 233
      end

234
      it "updates a running build" do
235
        expect(response).to have_http_status(200)
236 237
      end

238
      it 'does not override trace information when no trace is given' do
239 240 241 242 243 244
        expect(build.reload.trace).to eq 'BUILD TRACE'
      end

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

245
        it 'responds with forbidden' do
246 247
          expect(response.status).to eq 403
        end
248 249
      end
    end
250

251
    describe 'PATCH /builds/:id/trace.txt' do
252
      let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) }
Tomasz Maczukin committed
253 254
      let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
      let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
      let(:update_interval) { 10.seconds.to_i }

      def patch_the_trace(content = ' appended', request_headers = nil)
        unless request_headers
          offset = build.trace_length
          limit = offset + content.length - 1
          request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
        end

        Timecop.travel(build.updated_at + update_interval) do
          patch ci_api("/builds/#{build.id}/trace.txt"), content, request_headers
          build.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
277 278 279

      before do
        build.run!
280
        initial_patch_the_trace
281 282
      end

Tomasz Maczukin committed
283
      context 'when request is valid' do
Valery Sizov committed
284 285
        it 'gets correct response' do
          expect(response.status).to eq 202
286
          expect(build.reload.trace).to eq 'BUILD TRACE appended'
Valery Sizov committed
287 288 289 290
          expect(response.header).to have_key 'Range'
          expect(response.header).to have_key 'Build-Status'
        end

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
        context 'when build has been updated recently' do
          it { expect{ patch_the_trace }.not_to change { build.updated_at }}

          it 'changes the build trace' do
            patch_the_trace

            expect(build.reload.trace).to eq 'BUILD TRACE appended appended'
          end

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

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

              expect(build.reload.trace).to eq 'BUILD TRACE appended'
            end
          end
        end

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

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

          it 'changes the build.trace' do
            patch_the_trace

            expect(build.reload.trace).to eq 'BUILD TRACE appended appended'
          end

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

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

              expect(build.reload.trace).to eq 'BUILD TRACE appended'
            end
          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
          expect(build.reload.trace).to eq 'BUILD TRACE appended'
          expect(response.header).to have_key 'Range'
          expect(response.header).to have_key 'Build-Status'
        end
Tomasz Maczukin committed
345 346 347 348 349
      end

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

350
        it 'gets 416 error response with range headers' do
Valery Sizov committed
351 352 353 354
          expect(response.status).to eq 416
          expect(response.header).to have_key 'Range'
          expect(response.header['Range']).to eq '0-11'
        end
Tomasz Maczukin committed
355 356 357 358 359
      end

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

360
        it 'gets 416 error response with range headers' do
Valery Sizov committed
361 362 363 364
          expect(response.status).to eq 416
          expect(response.header).to have_key 'Range'
          expect(response.header['Range']).to eq '0-11'
        end
Tomasz Maczukin committed
365 366 367
      end

      context 'when Content-Range header is missing' do
368
        let(:headers_with_range) { headers }
Tomasz Maczukin committed
369 370

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

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

Tomasz Maczukin committed
376
        it { expect(response.status).to eq 403 }
377 378 379
      end
    end

380 381 382
    context "Artifacts" do
      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') }
383
      let(:build) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
384 385 386 387
      let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") }
      let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
      let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
      let(:get_url) { ci_api("/builds/#{build.id}/artifacts") }
388 389
      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 } }
390 391
      let(:token) { build.token }
      let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => token) }
392

393 394
      before { build.run! }

395 396 397
      describe "POST /builds/:id/artifacts/authorize" do
        context "should authorize posting artifact to running build" do
          it "using token as parameter" do
398
            post authorize_url, { token: build.token }, headers
399

400
            expect(response).to have_http_status(200)
401
            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
402
            expect(json_response["TempPath"]).not_to be_nil
403 404 405 406
          end

          it "using token as header" do
            post authorize_url, {}, headers_with_token
407

408
            expect(response).to have_http_status(200)
409
            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
410
            expect(json_response["TempPath"]).not_to be_nil
411
          end
412

413 414
          it "using runners token" do
            post authorize_url, { token: build.project.runners_token }, headers
415

416 417 418 419 420
            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

421
          it "reject requests that did not go through gitlab-workhorse" do
Jacob Vosmaer committed
422
            headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
423

424
            post authorize_url, { token: build.token }, headers
425

426 427
            expect(response).to have_http_status(500)
          end
428 429 430 431
        end

        context "should fail to post too large artifact" do
          it "using token as parameter" do
432
            stub_application_setting(max_artifacts_size: 0)
433

434
            post authorize_url, { token: build.token, filesize: 100 }, headers
435

436
            expect(response).to have_http_status(413)
437 438 439
          end

          it "using token as header" do
440
            stub_application_setting(max_artifacts_size: 0)
441

442
            post authorize_url, { filesize: 100 }, headers_with_token
443

444
            expect(response).to have_http_status(413)
445 446 447
          end
        end

448 449 450
        context 'authorization token is invalid' do
          before { post authorize_url, { token: 'invalid', filesize: 100 } }

451
          it 'responds with forbidden' do
452
            expect(response).to have_http_status(403)
453 454 455 456 457
          end
        end
      end

      describe "POST /builds/:id/artifacts" do
458
        context "disable sanitizer" do
459 460 461 462 463
          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

464
          describe 'build has been erased' do
465
            let(:build) { create(:ci_build, erased_at: Time.now) }
466 467 468 469

            before do
              upload_artifacts(file_upload, headers_with_token)
            end
470

471
            it 'responds with forbidden' do
472 473 474 475
              expect(response.status).to eq 403
            end
          end

476
          describe 'uploading artifacts for a running build' do
477
            shared_examples 'successful artifacts upload' do
478 479
              it 'updates successfully' do
                response_filename =
480
                  json_response['artifacts_file']['filename']
481 482 483 484

                expect(response).to have_http_status(201)
                expect(response_filename).to eq(file_upload.original_filename)
              end
485 486
            end

487 488 489 490 491
            context 'uses regular file post' do
              before do
                upload_artifacts(file_upload, headers_with_token, false)
              end

492
              it_behaves_like 'successful artifacts upload'
493 494
            end

495 496 497 498 499
            context 'uses accelerated file post' do
              before do
                upload_artifacts(file_upload, headers_with_token, true)
              end

500
              it_behaves_like 'successful artifacts upload'
501 502 503 504 505 506 507 508
            end

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

509
              it_behaves_like 'successful artifacts upload'
510
            end
511 512 513 514 515 516 517 518 519 520

            context 'when using runners token' do
              let(:token) { build.project.runners_token }

              before do
                upload_artifacts(file_upload, headers_with_token)
              end

              it_behaves_like 'successful artifacts upload'
            end
521 522
          end

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

527 528
            let(:stored_artifacts_file) { build.reload.artifacts_file.file }
            let(:stored_metadata_file) { build.reload.artifacts_metadata.file }
529
            let(:stored_artifacts_size) { build.reload.artifacts_size }
530

531 532 533
            before do
              post(post_url, post_data, headers_with_token)
            end
534

535
            context 'posts data accelerated by workhorse is correct' do
536 537 538 539 540 541 542 543
              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
544
                expect(response).to have_http_status(201)
545 546
                expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
                expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
547
                expect(stored_artifacts_size).to eq(71759)
548
              end
549 550
            end

551
            context 'no artifacts file in post data' do
552
              let(:post_data) do
553
                { 'metadata' => metadata }
554 555
              end

556
              it 'is expected to respond with bad request' do
557
                expect(response).to have_http_status(400)
558 559
              end

560
              it 'does not store metadata' do
561 562
                expect(stored_metadata_file).to be_nil
              end
563 564 565
            end
          end

566
          context 'with an expire date' do
567 568 569 570 571 572 573 574 575 576 577 578
            let!(:artifacts) { file_upload }

            let(:post_data) do
              { 'file.path' => artifacts.path,
                'file.name' => artifacts.original_filename,
                'expire_in' => expire_in }
            end

            before do
              post(post_url, post_data, headers_with_token)
            end

579
            context 'with an expire_in given' do
580 581
              let(:expire_in) { '7 days' }

Kamil Trzcinski committed
582
              it 'updates when specified' do
583
                build.reload
584
                expect(response).to have_http_status(201)
585 586 587 588 589
                expect(json_response['artifacts_expire_at']).not_to be_empty
                expect(build.artifacts_expire_at).to be_within(5.minutes).of(Time.now + 7.days)
              end
            end

590
            context 'with no expire_in given' do
591 592
              let(:expire_in) { nil }

Kamil Trzcinski committed
593
              it 'ignores if not specified' do
594
                build.reload
595
                expect(response).to have_http_status(201)
596 597 598
                expect(json_response['artifacts_expire_at']).to be_nil
                expect(build.artifacts_expire_at).to be_nil
              end
599 600 601
            end
          end

602
          context "artifacts file is too large" do
603
            it "fails to post too large artifact" do
604
              stub_application_setting(max_artifacts_size: 0)
605
              upload_artifacts(file_upload, headers_with_token)
606
              expect(response).to have_http_status(413)
607 608 609
            end
          end

610
          context "artifacts post request does not contain file" do
611
            it "fails to post artifacts without file" do
612
              post post_url, {}, headers_with_token
613
              expect(response).to have_http_status(400)
614 615 616
            end
          end

617
          context 'GitLab Workhorse is not configured' do
618
            it "fails to post artifacts without GitLab-Workhorse" do
619
              post post_url, { token: build.token }, {}
620
              expect(response).to have_http_status(403)
621 622 623 624
            end
          end
        end

625
        context "artifacts are being stored outside of tmp path" do
626 627 628 629 630 631 632 633 634 635 636
          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

          after do
            FileUtils.remove_entry @tmpdir
          end

637
          it "fails to post artifacts for outside of tmp path" do
638
            upload_artifacts(file_upload, headers_with_token)
639
            expect(response).to have_http_status(400)
640 641 642
          end
        end

643 644 645 646 647 648 649 650 651
        def upload_artifacts(file, headers = {}, accelerated = true)
          if accelerated
            post post_url, {
              'file.path' => file.path,
              'file.name' => file.original_filename
            }, headers
          else
            post post_url, { file: file }, headers
          end
652 653 654
        end
      end

655 656
      describe 'DELETE /builds/:id/artifacts' do
        let(:build) { create(:ci_build, :artifacts) }
657 658 659 660

        before do
          delete delete_url, token: build.token
        end
661

662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686
        shared_examples 'having removable artifacts' do
          it 'removes build artifacts' do
            build.reload

            expect(response).to have_http_status(200)
            expect(build.artifacts_file.exists?).to be_falsy
            expect(build.artifacts_metadata.exists?).to be_falsy
            expect(build.artifacts_size).to be_nil
          end
        end

        context 'when using build token' do
          before do
            delete delete_url, token: build.token
          end

          it_behaves_like 'having removable artifacts'
        end

        context 'when using runnners token' do
          before do
            delete delete_url, token: build.project.runners_token
          end

          it_behaves_like 'having removable artifacts'
687 688 689 690
        end
      end

      describe 'GET /builds/:id/artifacts' do
691 692 693
        before do
          get get_url, token: token
        end
694 695 696 697

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

702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718
          shared_examples 'having downloadable artifacts' do
            it 'download artifacts' do
              expect(response).to have_http_status(200)
              expect(response.headers).to include download_headers
            end
          end

          context 'when using build token' do
            let(:token) { build.token }

            it_behaves_like 'having downloadable artifacts'
          end

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

            it_behaves_like 'having downloadable artifacts'
719
          end
720 721
        end

722
        context 'build does not has artifacts' do
723 724
          let(:token) { build.token }

725
          it 'responds with not found' do
726
            expect(response).to have_http_status(404)
727
          end
728 729 730
        end
      end
    end
731 732
  end
end