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

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
      let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' }

      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
37

38 39
      it "starts a build" do
        register_builds info: { platform: :darwin }
40

41
        expect(response).to have_http_status(201)
42 43
        expect(json_response['sha']).to eq(build.sha)
        expect(runner.reload.platform).to eq("darwin")
44 45 46 47 48 49
        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 }
        )
50 51
      end

52 53 54 55 56
      context 'when builds are finished' do
        before do
          build.success
          register_builds
        end
57 58

        it_behaves_like 'no builds available'
59 60
      end

61 62 63 64 65 66
      context 'for other project with builds' do
        before do
          build.success
          create(:ci_build, :pending)
          register_builds
        end
67 68

        it_behaves_like 'no builds available'
69 70
      end

71 72
      context 'for shared runner' do
        let(:shared_runner) { create(:ci_runner, token: "SharedRunner") }
73

74
        before do
75 76
          register_builds shared_runner.token
        end
77 78

        it_behaves_like 'no builds available'
79 80
      end

81 82 83 84 85 86
      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
87

88 89 90 91 92 93 94 95 96 97 98 99 100
        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
101 102
      end

103 104 105 106
      context 'with multiple builds' do
        before do
          build.success
        end
107

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

110 111
        it "returns dependent builds" do
          register_builds info: { platform: :darwin }
112

113 114 115 116 117
          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
118
      end
119 120 121 122 123 124 125 126

      %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
127 128 129
            register_builds info: { param => value }

            expect(response).to have_http_status(201)
130 131 132 133 134
            runner.reload
            is_expected.to eq(value)
          end
        end
      end
135 136 137

      context 'when build has no tags' do
        before do
138
          build.update(tags: [])
139 140 141 142 143 144 145 146 147 148 149 150 151
        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
152 153
          before do
            runner.update_column(:run_untagged, false)
154 155
            register_builds
          end
156 157

          it_behaves_like 'no builds available'
158
        end
159
      end
160

161 162 163 164 165 166 167 168 169 170
      context 'when runner is paused' do
        let(:inactive_runner) { create(:ci_runner, :inactive, token: "InactiveRunner") }

        before do
          register_builds inactive_runner.token
        end

        it { expect(response).to have_http_status 404 }
      end

171
      def register_builds(token = runner.token, **params)
Tomasz Maczukin's avatar
Tomasz Maczukin committed
172
        post ci_api("/builds/register"), params.merge(token: token), { 'User-Agent' => user_agent }
173
      end
174 175 176
    end

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

179
      before do
180
        build.run!
Valery Sizov's avatar
Valery Sizov committed
181
        put ci_api("/builds/#{build.id}"), token: runner.token
182 183
      end

184
      it "updates a running build" do
185
        expect(response).to have_http_status(200)
186 187
      end

188
      it 'does not override trace information when no trace is given' do
189 190 191 192 193 194
        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) }

195
        it 'responds with forbidden' do
196 197
          expect(response.status).to eq 403
        end
198 199
      end
    end
200

201
    describe 'PATCH /builds/:id/trace.txt' do
202
      let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
203 204
      let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
      let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
205 206 207

      before do
        build.run!
Tomasz Maczukin's avatar
Tomasz Maczukin committed
208
        patch ci_api("/builds/#{build.id}/trace.txt"), ' appended', headers_with_range
209 210
      end

Tomasz Maczukin's avatar
Tomasz Maczukin committed
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 request is valid' do
        it { expect(response.status).to eq 202 }
        it { expect(build.reload.trace).to eq 'BUILD TRACE appended' }
        it { expect(response.header).to have_key 'Range' }
        it { expect(response.header).to have_key 'Build-Status' }
      end

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

        it { expect(response.status).to eq 416 }
        it { expect(response.header).to have_key 'Range' }
        it { expect(response.header['Range']).to eq '0-11' }
      end

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

        it { expect(response.status).to eq 416 }
        it { expect(response.header).to have_key 'Range' }
        it { expect(response.header['Range']).to eq '0-11' }
      end

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

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

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

Tomasz Maczukin's avatar
Tomasz Maczukin committed
243
        it { expect(response.status).to eq 403 }
244 245 246
      end
    end

247 248 249
    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') }
250
      let(:build) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
251 252 253 254
      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") }
255 256
      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 } }
257 258
      let(:token) { build.token }
      let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => token) }
259

260 261
      before { build.run! }

262 263 264
      describe "POST /builds/:id/artifacts/authorize" do
        context "should authorize posting artifact to running build" do
          it "using token as parameter" do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
265
            post authorize_url, { token: build.token }, headers
266

267
            expect(response).to have_http_status(200)
268
            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
269
            expect(json_response["TempPath"]).not_to be_nil
270 271 272 273
          end

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

275
            expect(response).to have_http_status(200)
276
            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
277
            expect(json_response["TempPath"]).not_to be_nil
278
          end
279

280 281
          it "using runners token" do
            post authorize_url, { token: build.project.runners_token }, headers
282

283 284 285 286 287
            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

288
          it "reject requests that did not go through gitlab-workhorse" do
Jacob Vosmaer's avatar
Jacob Vosmaer committed
289
            headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
290

291
            post authorize_url, { token: build.token }, headers
292

293 294
            expect(response).to have_http_status(500)
          end
295 296 297 298
        end

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

Kamil Trzcinski's avatar
Kamil Trzcinski committed
301
            post authorize_url, { token: build.token, filesize: 100 }, headers
302

303
            expect(response).to have_http_status(413)
304 305 306
          end

          it "using token as header" do
307
            stub_application_setting(max_artifacts_size: 0)
308

309
            post authorize_url, { filesize: 100 }, headers_with_token
310

311
            expect(response).to have_http_status(413)
312 313 314
          end
        end

315 316 317
        context 'authorization token is invalid' do
          before { post authorize_url, { token: 'invalid', filesize: 100 } }

318
          it 'responds with forbidden' do
319
            expect(response).to have_http_status(403)
320 321 322 323 324
          end
        end
      end

      describe "POST /builds/:id/artifacts" do
325
        context "disable sanitizer" do
326 327 328 329 330
          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

331
          describe 'build has been erased' do
332
            let(:build) { create(:ci_build, erased_at: Time.now) }
333 334 335 336

            before do
              upload_artifacts(file_upload, headers_with_token)
            end
337

338
            it 'responds with forbidden' do
339 340 341 342
              expect(response.status).to eq 403
            end
          end

343
          describe 'uploading artifacts for a running build' do
344
            shared_examples 'successful artifacts upload' do
345 346
              it 'updates successfully' do
                response_filename =
347
                  json_response['artifacts_file']['filename']
348 349 350 351

                expect(response).to have_http_status(201)
                expect(response_filename).to eq(file_upload.original_filename)
              end
352 353
            end

354 355 356 357 358
            context 'uses regular file post' do
              before do
                upload_artifacts(file_upload, headers_with_token, false)
              end

359
              it_behaves_like 'successful artifacts upload'
360 361
            end

362 363 364 365 366
            context 'uses accelerated file post' do
              before do
                upload_artifacts(file_upload, headers_with_token, true)
              end

367
              it_behaves_like 'successful artifacts upload'
368 369 370 371 372 373 374 375
            end

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

376
              it_behaves_like 'successful artifacts upload'
377
            end
378 379 380 381 382 383 384 385 386 387

            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
388 389
          end

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

394 395
            let(:stored_artifacts_file) { build.reload.artifacts_file.file }
            let(:stored_metadata_file) { build.reload.artifacts_metadata.file }
396
            let(:stored_artifacts_size) { build.reload.artifacts_size }
397

398 399 400
            before do
              post(post_url, post_data, headers_with_token)
            end
401

402
            context 'posts data accelerated by workhorse is correct' do
403 404 405 406 407 408 409 410
              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
411
                expect(response).to have_http_status(201)
412 413
                expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
                expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
414
                expect(stored_artifacts_size).to eq(71759)
415
              end
416 417
            end

418
            context 'no artifacts file in post data' do
419
              let(:post_data) do
420
                { 'metadata' => metadata }
421 422
              end

423
              it 'is expected to respond with bad request' do
424
                expect(response).to have_http_status(400)
425 426
              end

427
              it 'does not store metadata' do
428 429
                expect(stored_metadata_file).to be_nil
              end
430 431 432
            end
          end

433
          context 'with an expire date' do
434 435 436 437 438 439 440 441 442 443 444 445
            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

446
            context 'with an expire_in given' do
447 448
              let(:expire_in) { '7 days' }

Kamil Trzcinski's avatar
Kamil Trzcinski committed
449
              it 'updates when specified' do
450
                build.reload
451
                expect(response).to have_http_status(201)
452 453 454 455 456
                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

457
            context 'with no expire_in given' do
458 459
              let(:expire_in) { nil }

Kamil Trzcinski's avatar
Kamil Trzcinski committed
460
              it 'ignores if not specified' do
461
                build.reload
462
                expect(response).to have_http_status(201)
463 464 465
                expect(json_response['artifacts_expire_at']).to be_nil
                expect(build.artifacts_expire_at).to be_nil
              end
466 467 468
            end
          end

469
          context "artifacts file is too large" do
470
            it "fails to post too large artifact" do
471
              stub_application_setting(max_artifacts_size: 0)
472
              upload_artifacts(file_upload, headers_with_token)
473
              expect(response).to have_http_status(413)
474 475 476
            end
          end

477
          context "artifacts post request does not contain file" do
478
            it "fails to post artifacts without file" do
479
              post post_url, {}, headers_with_token
480
              expect(response).to have_http_status(400)
481 482 483
            end
          end

484
          context 'GitLab Workhorse is not configured' do
485
            it "fails to post artifacts without GitLab-Workhorse" do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
486
              post post_url, { token: build.token }, {}
487
              expect(response).to have_http_status(403)
488 489 490 491
            end
          end
        end

492
        context "artifacts are being stored outside of tmp path" do
493 494 495 496 497 498 499 500 501 502 503
          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

504
          it "fails to post artifacts for outside of tmp path" do
505
            upload_artifacts(file_upload, headers_with_token)
506
            expect(response).to have_http_status(400)
507 508 509
          end
        end

510 511 512 513 514 515 516 517 518
        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
519 520 521
        end
      end

522 523
      describe 'DELETE /builds/:id/artifacts' do
        let(:build) { create(:ci_build, :artifacts) }
524 525 526 527

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

529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
        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'
554 555 556 557
        end
      end

      describe 'GET /builds/:id/artifacts' do
558 559 560
        before do
          get get_url, token: token
        end
561 562 563 564

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

569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
          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'
586
          end
587 588
        end

589
        context 'build does not has artifacts' do
590 591
          let(:token) { build.token }

592
          it 'responds with not found' do
593
            expect(response).to have_http_status(404)
594
          end
595 596 597
        end
      end
    end
598 599
  end
end