Remove CI API v1
This API was mainly for internal usage, and has been moved to the general API: APIv4. The endpoints have been deprecated since 9.0, and won't see 10.0. :)
This commit is contained in:
parent
45c8c17e80
commit
1ffd0c8562
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Remove CI API v1
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: removed
|
|
@ -1,8 +1,4 @@
|
||||||
namespace :ci do
|
namespace :ci do
|
||||||
# CI API
|
|
||||||
Ci::API::API.logger Rails.logger
|
|
||||||
mount Ci::API::API => '/api'
|
|
||||||
|
|
||||||
resource :lint, only: [:show, :create]
|
resource :lint, only: [:show, :create]
|
||||||
|
|
||||||
root to: redirect('/')
|
root to: redirect('/')
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
# GitLab CI API
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
The main purpose of GitLab CI API is to provide the necessary data and context
|
|
||||||
for GitLab CI Runners.
|
|
||||||
|
|
||||||
All relevant information about the consumer API can be found in a
|
|
||||||
[separate document](../../api/README.md).
|
|
||||||
|
|
||||||
## API Prefix
|
|
||||||
|
|
||||||
The current CI API prefix is `/ci/api/v1`.
|
|
||||||
|
|
||||||
You need to prepend this prefix to all examples in this documentation, like:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
GET /ci/api/v1/builds/:id/artifacts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [Builds](builds.md)
|
|
||||||
- [Runners](runners.md)
|
|
|
@ -1,147 +0,0 @@
|
||||||
# Builds API
|
|
||||||
|
|
||||||
API used by runners to receive and update builds.
|
|
||||||
|
|
||||||
>**Note:**
|
|
||||||
This API is intended to be used only by Runners as their own
|
|
||||||
communication channel. For the consumer API see the
|
|
||||||
[Jobs API](../jobs.md).
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
This API uses two types of authentication:
|
|
||||||
|
|
||||||
1. Unique Runner's token which is the token assigned to the Runner after it
|
|
||||||
has been registered.
|
|
||||||
|
|
||||||
2. Using the build authorization token.
|
|
||||||
This is project's CI token that can be found under the **Builds** section of
|
|
||||||
a project's settings. The build authorization token can be passed as a
|
|
||||||
parameter or a value of `BUILD-TOKEN` header.
|
|
||||||
|
|
||||||
These two methods of authentication are interchangeable.
|
|
||||||
|
|
||||||
## Builds
|
|
||||||
|
|
||||||
### Runs oldest pending build by runner
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /ci/api/v1/builds/register
|
|
||||||
```
|
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
|
||||||
|-----------|---------|----------|---------------------|
|
|
||||||
| `token` | string | yes | Unique runner token |
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
curl --request POST "https://gitlab.example.com/ci/api/v1/builds/register" --form "token=t0k3n"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Responses:**
|
|
||||||
|
|
||||||
| Status | Data |Description |
|
|
||||||
|--------|------|---------------------------------------------------------------------------|
|
|
||||||
| `201` | yes | When a build is scheduled for a runner |
|
|
||||||
| `204` | no | When no builds are scheduled for a runner (for GitLab Runner >= `v1.3.0`) |
|
|
||||||
| `403` | no | When invalid token is used or no token is sent |
|
|
||||||
| `404` | no | When no builds are scheduled for a runner (for GitLab Runner < `v1.3.0`) **or** when the runner is set to `paused` in GitLab runner's configuration page |
|
|
||||||
|
|
||||||
### Update details of an existing build
|
|
||||||
|
|
||||||
```
|
|
||||||
PUT /ci/api/v1/builds/:id
|
|
||||||
```
|
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
|
||||||
|-----------|---------|----------|----------------------|
|
|
||||||
| `id` | integer | yes | The ID of a project |
|
|
||||||
| `token` | string | yes | Unique runner token |
|
|
||||||
| `state` | string | no | The state of a build |
|
|
||||||
| `trace` | string | no | The trace of a build |
|
|
||||||
|
|
||||||
```
|
|
||||||
curl --request PUT "https://gitlab.example.com/ci/api/v1/builds/1234" --form "token=t0k3n" --form "state=running" --form "trace=Running git clone...\n"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Incremental build trace update
|
|
||||||
|
|
||||||
Using this method you need to send trace content as a request body. You also need to provide the `Content-Range` header
|
|
||||||
with a range of sent trace part. Note that you need to send parts in the proper order, so the begining of the part
|
|
||||||
must start just after the end of the previous part. If you provide the wrong part, then GitLab CI API will return `416
|
|
||||||
Range Not Satisfiable` response with a header `Range: 0-X`, where `X` is the current trace length.
|
|
||||||
|
|
||||||
For example, if you receive `Range: 0-11` in the response, then your next part must contain a `Content-Range: 11-...`
|
|
||||||
header and a trace part covered by this range.
|
|
||||||
|
|
||||||
For a valid update API will return `202` response with:
|
|
||||||
* `Build-Status: {status}` header containing current status of the build,
|
|
||||||
* `Range: 0-{length}` header with the current trace length.
|
|
||||||
|
|
||||||
```
|
|
||||||
PATCH /ci/api/v1/builds/:id/trace.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
|
||||||
|-----------|---------|----------|----------------------|
|
|
||||||
| `id` | integer | yes | The ID of a build |
|
|
||||||
|
|
||||||
Headers:
|
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
|
||||||
|-----------------|---------|----------|-----------------------------------|
|
|
||||||
| `BUILD-TOKEN` | string | yes | The build authorization token |
|
|
||||||
| `Content-Range` | string | yes | Bytes range of trace that is sent |
|
|
||||||
|
|
||||||
```
|
|
||||||
curl --request PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" --header "BUILD-TOKEN=build_t0k3n" --header "Content-Range=0-21" --data "Running git clone...\n"
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Upload artifacts to build
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /ci/api/v1/builds/:id/artifacts
|
|
||||||
```
|
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
|
||||||
|-----------|---------|----------|-------------------------------|
|
|
||||||
| `id` | integer | yes | The ID of a build |
|
|
||||||
| `token` | string | yes | The build authorization token |
|
|
||||||
| `file` | mixed | yes | Artifacts file |
|
|
||||||
|
|
||||||
```
|
|
||||||
curl --request POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n" --form "file=@/path/to/file"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Download the artifacts file from build
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /ci/api/v1/builds/:id/artifacts
|
|
||||||
```
|
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
|
||||||
|-----------|---------|----------|-------------------------------|
|
|
||||||
| `id` | integer | yes | The ID of a build |
|
|
||||||
| `token` | string | yes | The build authorization token |
|
|
||||||
|
|
||||||
```
|
|
||||||
curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remove the artifacts file from build
|
|
||||||
|
|
||||||
```
|
|
||||||
DELETE /ci/api/v1/builds/:id/artifacts
|
|
||||||
```
|
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
|
||||||
|-----------|---------|----------|-------------------------------|
|
|
||||||
| ` id` | integer | yes | The ID of a build |
|
|
||||||
| `token` | string | yes | The build authorization token |
|
|
||||||
|
|
||||||
```
|
|
||||||
curl --request DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n"
|
|
||||||
```
|
|
|
@ -1,51 +0,0 @@
|
||||||
# Validate the .gitlab-ci.yml (API)
|
|
||||||
|
|
||||||
> [Introduced][ce-5953] in GitLab 8.12.
|
|
||||||
|
|
||||||
Checks if your .gitlab-ci.yml file is valid.
|
|
||||||
|
|
||||||
```
|
|
||||||
POST ci/lint
|
|
||||||
```
|
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
|
||||||
| ---------- | ------- | -------- | -------- |
|
|
||||||
| `content` | string | yes | the .gitlab-ci.yaml content|
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl --header "Content-Type: application/json" https://gitlab.example.com/api/v4/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces.
|
|
||||||
|
|
||||||
Example responses:
|
|
||||||
|
|
||||||
* Valid content:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "valid",
|
|
||||||
"errors": []
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Invalid content:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "invalid",
|
|
||||||
"errors": [
|
|
||||||
"variables config should be a hash of key value pairs"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Without the content attribute:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "content is missing"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
[ce-5953]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5953
|
|
|
@ -1,59 +0,0 @@
|
||||||
# Register and Delete Runners API
|
|
||||||
|
|
||||||
API used by Runners to register and delete themselves.
|
|
||||||
|
|
||||||
>**Note:**
|
|
||||||
This API is intended to be used only by Runners as their own
|
|
||||||
communication channel. For the consumer API see the
|
|
||||||
[new Runners API](../runners.md).
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
This API uses two types of authentication:
|
|
||||||
|
|
||||||
1. Unique Runner's token, which is the token assigned to the Runner after it
|
|
||||||
has been registered. This token can be found on the Runner's edit page (go to
|
|
||||||
**Project > Runners**, select one of the Runners listed under **Runners activated for
|
|
||||||
this project**).
|
|
||||||
|
|
||||||
2. Using Runners' registration token.
|
|
||||||
This is a token that can be found in project's settings.
|
|
||||||
It can also be found in the **Admin > Runners** settings area.
|
|
||||||
There are two types of tokens you can pass: shared Runner registration
|
|
||||||
token or project specific registration token.
|
|
||||||
|
|
||||||
## Register a new runner
|
|
||||||
|
|
||||||
Used to make GitLab CI aware of available runners.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
POST /ci/api/v1/runners/register
|
|
||||||
```
|
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
|
||||||
| --------- | ------- | --------- | ----------- |
|
|
||||||
| `token` | string | yes | Runner's registration token |
|
|
||||||
|
|
||||||
Example request:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
curl --request POST "https://gitlab.example.com/ci/api/v1/runners/register" --form "token=t0k3n"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Delete a Runner
|
|
||||||
|
|
||||||
Used to remove a Runner.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
DELETE /ci/api/v1/runners/delete
|
|
||||||
```
|
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
|
||||||
| --------- | ------- | --------- | ----------- |
|
|
||||||
| `token` | string | yes | Unique Runner's token |
|
|
||||||
|
|
||||||
Example request:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
curl --request DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" --form "token=t0k3n"
|
|
||||||
```
|
|
|
@ -1 +0,0 @@
|
||||||
This document was moved to a [new location](../../api/ci/README.md).
|
|
|
@ -1 +0,0 @@
|
||||||
This document was moved to a [new location](../../api/ci/builds.md).
|
|
|
@ -1 +0,0 @@
|
||||||
This document was moved to a [new location](../../api/ci/runners.md).
|
|
|
@ -37,7 +37,6 @@ This page gathers all the resources for the topic **Authentication** within GitL
|
||||||
- [Private Tokens](../../api/README.md#private-tokens)
|
- [Private Tokens](../../api/README.md#private-tokens)
|
||||||
- [Impersonation tokens](../../api/README.md#impersonation-tokens)
|
- [Impersonation tokens](../../api/README.md#impersonation-tokens)
|
||||||
- [GitLab as an OAuth2 provider](../../api/oauth2.md#gitlab-as-an-oauth2-provider)
|
- [GitLab as an OAuth2 provider](../../api/oauth2.md#gitlab-as-an-oauth2-provider)
|
||||||
- [GitLab Runner API - Authentication](../../api/ci/runners.md#authentication)
|
|
||||||
|
|
||||||
## Third-party resources
|
## Third-party resources
|
||||||
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
module Ci
|
|
||||||
module API
|
|
||||||
class API < Grape::API
|
|
||||||
include ::API::APIGuard
|
|
||||||
version 'v1', using: :path
|
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordNotFound do
|
|
||||||
rack_response({ 'message' => '404 Not found' }.to_json, 404)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Retain 405 error rather than a 500 error for Grape 0.15.0+.
|
|
||||||
# https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
|
|
||||||
rescue_from Grape::Exceptions::MethodNotAllowed do |e|
|
|
||||||
error! e.message, e.status, e.headers
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from Grape::Exceptions::Base do |e|
|
|
||||||
error! e.message, e.status, e.headers
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from :all do |exception|
|
|
||||||
handle_api_exception(exception)
|
|
||||||
end
|
|
||||||
|
|
||||||
content_type :txt, 'text/plain'
|
|
||||||
content_type :json, 'application/json'
|
|
||||||
format :json
|
|
||||||
|
|
||||||
helpers ::SentryHelper
|
|
||||||
helpers ::Ci::API::Helpers
|
|
||||||
helpers ::API::Helpers
|
|
||||||
helpers Gitlab::CurrentSettings
|
|
||||||
|
|
||||||
mount ::Ci::API::Builds
|
|
||||||
mount ::Ci::API::Runners
|
|
||||||
mount ::Ci::API::Triggers
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,219 +0,0 @@
|
||||||
module Ci
|
|
||||||
module API
|
|
||||||
# Builds API
|
|
||||||
class Builds < Grape::API
|
|
||||||
resource :builds do
|
|
||||||
# Runs oldest pending build by runner - Runners only
|
|
||||||
#
|
|
||||||
# Parameters:
|
|
||||||
# token (required) - The uniq token of runner
|
|
||||||
#
|
|
||||||
# Example Request:
|
|
||||||
# POST /builds/register
|
|
||||||
post "register" do
|
|
||||||
authenticate_runner!
|
|
||||||
required_attributes! [:token]
|
|
||||||
not_found! unless current_runner.active?
|
|
||||||
update_runner_info
|
|
||||||
|
|
||||||
if current_runner.is_runner_queue_value_latest?(params[:last_update])
|
|
||||||
header 'X-GitLab-Last-Update', params[:last_update]
|
|
||||||
Gitlab::Metrics.add_event(:build_not_found_cached)
|
|
||||||
return build_not_found!
|
|
||||||
end
|
|
||||||
|
|
||||||
new_update = current_runner.ensure_runner_queue_value
|
|
||||||
|
|
||||||
result = Ci::RegisterJobService.new(current_runner).execute
|
|
||||||
|
|
||||||
if result.valid?
|
|
||||||
if result.build
|
|
||||||
Gitlab::Metrics.add_event(:build_found,
|
|
||||||
project: result.build.project.full_path)
|
|
||||||
|
|
||||||
present result.build, with: Entities::BuildDetails
|
|
||||||
else
|
|
||||||
Gitlab::Metrics.add_event(:build_not_found)
|
|
||||||
|
|
||||||
header 'X-GitLab-Last-Update', new_update
|
|
||||||
|
|
||||||
build_not_found!
|
|
||||||
end
|
|
||||||
else
|
|
||||||
# We received build that is invalid due to concurrency conflict
|
|
||||||
Gitlab::Metrics.add_event(:build_invalid)
|
|
||||||
conflict!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Update an existing build - Runners only
|
|
||||||
#
|
|
||||||
# Parameters:
|
|
||||||
# id (required) - The ID of a project
|
|
||||||
# state (optional) - The state of a build
|
|
||||||
# trace (optional) - The trace of a build
|
|
||||||
# Example Request:
|
|
||||||
# PUT /builds/:id
|
|
||||||
put ":id" do
|
|
||||||
authenticate_runner!
|
|
||||||
build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id])
|
|
||||||
validate_build!(build)
|
|
||||||
|
|
||||||
update_runner_info
|
|
||||||
|
|
||||||
build.trace.set(params[:trace]) if params[:trace]
|
|
||||||
|
|
||||||
Gitlab::Metrics.add_event(:update_build,
|
|
||||||
project: build.project.full_path)
|
|
||||||
|
|
||||||
case params[:state].to_s
|
|
||||||
when 'success'
|
|
||||||
build.success
|
|
||||||
when 'failed'
|
|
||||||
build.drop
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Send incremental log update - Runners only
|
|
||||||
#
|
|
||||||
# Parameters:
|
|
||||||
# id (required) - The ID of a build
|
|
||||||
# Body:
|
|
||||||
# content of logs to append
|
|
||||||
# Headers:
|
|
||||||
# Content-Range (required) - range of content that was sent
|
|
||||||
# BUILD-TOKEN (required) - The build authorization token
|
|
||||||
# Example Request:
|
|
||||||
# PATCH /builds/:id/trace.txt
|
|
||||||
patch ":id/trace.txt" do
|
|
||||||
build = authenticate_build!
|
|
||||||
|
|
||||||
error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
|
|
||||||
content_range = request.headers['Content-Range']
|
|
||||||
content_range = content_range.split('-')
|
|
||||||
|
|
||||||
stream_size = build.trace.append(request.body.read, content_range[0].to_i)
|
|
||||||
if stream_size < 0
|
|
||||||
return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{-stream_size}" })
|
|
||||||
end
|
|
||||||
|
|
||||||
status 202
|
|
||||||
header 'Build-Status', build.status
|
|
||||||
header 'Range', "0-#{stream_size}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Authorize artifacts uploading for build - Runners only
|
|
||||||
#
|
|
||||||
# Parameters:
|
|
||||||
# id (required) - The ID of a build
|
|
||||||
# token (required) - The build authorization token
|
|
||||||
# filesize (optional) - the size of uploaded file
|
|
||||||
# Example Request:
|
|
||||||
# POST /builds/:id/artifacts/authorize
|
|
||||||
post ":id/artifacts/authorize" do
|
|
||||||
require_gitlab_workhorse!
|
|
||||||
Gitlab::Workhorse.verify_api_request!(headers)
|
|
||||||
not_allowed! unless Gitlab.config.artifacts.enabled
|
|
||||||
build = authenticate_build!
|
|
||||||
forbidden!('build is not running') unless build.running?
|
|
||||||
|
|
||||||
if params[:filesize]
|
|
||||||
file_size = params[:filesize].to_i
|
|
||||||
file_to_large! unless file_size < max_artifacts_size
|
|
||||||
end
|
|
||||||
|
|
||||||
status 200
|
|
||||||
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
|
|
||||||
Gitlab::Workhorse.artifact_upload_ok
|
|
||||||
end
|
|
||||||
|
|
||||||
# Upload artifacts to build - Runners only
|
|
||||||
#
|
|
||||||
# Parameters:
|
|
||||||
# id (required) - The ID of a build
|
|
||||||
# token (required) - The build authorization token
|
|
||||||
# file (required) - Artifacts file
|
|
||||||
# expire_in (optional) - Specify when artifacts should expire (ex. 7d)
|
|
||||||
# Parameters (accelerated by GitLab Workhorse):
|
|
||||||
# file.path - path to locally stored body (generated by Workhorse)
|
|
||||||
# file.name - real filename as send in Content-Disposition
|
|
||||||
# file.type - real content type as send in Content-Type
|
|
||||||
# metadata.path - path to locally stored body (generated by Workhorse)
|
|
||||||
# metadata.name - filename (generated by Workhorse)
|
|
||||||
# Headers:
|
|
||||||
# BUILD-TOKEN (required) - The build authorization token, the same as token
|
|
||||||
# Body:
|
|
||||||
# The file content
|
|
||||||
#
|
|
||||||
# Example Request:
|
|
||||||
# POST /builds/:id/artifacts
|
|
||||||
post ":id/artifacts" do
|
|
||||||
require_gitlab_workhorse!
|
|
||||||
not_allowed! unless Gitlab.config.artifacts.enabled
|
|
||||||
build = authenticate_build!
|
|
||||||
forbidden!('Build is not running!') unless build.running?
|
|
||||||
|
|
||||||
artifacts_upload_path = ArtifactUploader.artifacts_upload_path
|
|
||||||
artifacts = uploaded_file(:file, artifacts_upload_path)
|
|
||||||
metadata = uploaded_file(:metadata, artifacts_upload_path)
|
|
||||||
|
|
||||||
bad_request!('Missing artifacts file!') unless artifacts
|
|
||||||
file_to_large! unless artifacts.size < max_artifacts_size
|
|
||||||
|
|
||||||
build.artifacts_file = artifacts
|
|
||||||
build.artifacts_metadata = metadata
|
|
||||||
build.artifacts_expire_in =
|
|
||||||
params['expire_in'] ||
|
|
||||||
Gitlab::CurrentSettings.current_application_settings
|
|
||||||
.default_artifacts_expire_in
|
|
||||||
|
|
||||||
if build.save
|
|
||||||
present(build, with: Entities::BuildDetails)
|
|
||||||
else
|
|
||||||
render_validation_error!(build)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Download the artifacts file from build - Runners only
|
|
||||||
#
|
|
||||||
# Parameters:
|
|
||||||
# id (required) - The ID of a build
|
|
||||||
# token (required) - The build authorization token
|
|
||||||
# Headers:
|
|
||||||
# BUILD-TOKEN (required) - The build authorization token, the same as token
|
|
||||||
# Example Request:
|
|
||||||
# GET /builds/:id/artifacts
|
|
||||||
get ":id/artifacts" do
|
|
||||||
build = authenticate_build!
|
|
||||||
artifacts_file = build.artifacts_file
|
|
||||||
|
|
||||||
unless artifacts_file.exists?
|
|
||||||
not_found!
|
|
||||||
end
|
|
||||||
|
|
||||||
unless artifacts_file.file_storage?
|
|
||||||
return redirect_to build.artifacts_file.url
|
|
||||||
end
|
|
||||||
|
|
||||||
present_file!(artifacts_file.path, artifacts_file.filename)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Remove the artifacts file from build - Runners only
|
|
||||||
#
|
|
||||||
# Parameters:
|
|
||||||
# id (required) - The ID of a build
|
|
||||||
# token (required) - The build authorization token
|
|
||||||
# Headers:
|
|
||||||
# BUILD-TOKEN (required) - The build authorization token, the same as token
|
|
||||||
# Example Request:
|
|
||||||
# DELETE /builds/:id/artifacts
|
|
||||||
delete ":id/artifacts" do
|
|
||||||
build = authenticate_build!
|
|
||||||
|
|
||||||
status(200)
|
|
||||||
build.erase_artifacts!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,93 +0,0 @@
|
||||||
module Ci
|
|
||||||
module API
|
|
||||||
module Entities
|
|
||||||
class Commit < Grape::Entity
|
|
||||||
expose :id, :sha, :project_id, :created_at
|
|
||||||
expose :status, :finished_at, :duration
|
|
||||||
expose :git_commit_message, :git_author_name, :git_author_email
|
|
||||||
end
|
|
||||||
|
|
||||||
class CommitWithBuilds < Commit
|
|
||||||
expose :builds
|
|
||||||
end
|
|
||||||
|
|
||||||
class ArtifactFile < Grape::Entity
|
|
||||||
expose :filename, :size
|
|
||||||
end
|
|
||||||
|
|
||||||
class BuildOptions < Grape::Entity
|
|
||||||
expose :image
|
|
||||||
expose :services
|
|
||||||
expose :artifacts
|
|
||||||
expose :cache
|
|
||||||
expose :dependencies
|
|
||||||
expose :after_script
|
|
||||||
end
|
|
||||||
|
|
||||||
class Build < Grape::Entity
|
|
||||||
expose :id, :ref, :tag, :sha, :status
|
|
||||||
expose :name, :token, :stage
|
|
||||||
expose :project_id
|
|
||||||
expose :project_name
|
|
||||||
expose :artifacts_file, using: ArtifactFile, if: ->(build, _) { build.artifacts? }
|
|
||||||
end
|
|
||||||
|
|
||||||
class BuildCredentials < Grape::Entity
|
|
||||||
expose :type, :url, :username, :password
|
|
||||||
end
|
|
||||||
|
|
||||||
class BuildDetails < Build
|
|
||||||
expose :commands
|
|
||||||
expose :repo_url
|
|
||||||
expose :before_sha
|
|
||||||
expose :allow_git_fetch
|
|
||||||
expose :token
|
|
||||||
expose :artifacts_expire_at, if: ->(build, _) { build.artifacts? }
|
|
||||||
|
|
||||||
expose :options do |model|
|
|
||||||
# This part ensures that output of old API is still the same after adding support
|
|
||||||
# for extended docker configuration options, used by new API
|
|
||||||
#
|
|
||||||
# I'm leaving this here, not in the model, because it should be removed at the same time
|
|
||||||
# when old API will be removed (planned for August 2017).
|
|
||||||
model.options.dup.tap do |options|
|
|
||||||
options[:image] = options[:image][:name] if options[:image].is_a?(Hash)
|
|
||||||
options[:services]&.map! do |service|
|
|
||||||
if service.is_a?(Hash)
|
|
||||||
service[:name]
|
|
||||||
else
|
|
||||||
service
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
expose :timeout do |model|
|
|
||||||
model.timeout
|
|
||||||
end
|
|
||||||
|
|
||||||
expose :variables
|
|
||||||
expose :depends_on_builds, using: Build
|
|
||||||
|
|
||||||
expose :credentials, using: BuildCredentials
|
|
||||||
end
|
|
||||||
|
|
||||||
class Runner < Grape::Entity
|
|
||||||
expose :id, :token
|
|
||||||
end
|
|
||||||
|
|
||||||
class RunnerProject < Grape::Entity
|
|
||||||
expose :id, :project_id, :runner_id
|
|
||||||
end
|
|
||||||
|
|
||||||
class WebHook < Grape::Entity
|
|
||||||
expose :id, :project_id, :url
|
|
||||||
end
|
|
||||||
|
|
||||||
class TriggerRequest < Grape::Entity
|
|
||||||
expose :id, :variables
|
|
||||||
expose :pipeline, using: Commit, as: :commit
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,89 +0,0 @@
|
||||||
module Ci
|
|
||||||
module API
|
|
||||||
module Helpers
|
|
||||||
BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN".freeze
|
|
||||||
BUILD_TOKEN_PARAM = :token
|
|
||||||
UPDATE_RUNNER_EVERY = 10 * 60
|
|
||||||
|
|
||||||
def authenticate_runners!
|
|
||||||
forbidden! unless runner_registration_token_valid?
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate_runner!
|
|
||||||
forbidden! unless current_runner
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate_build!
|
|
||||||
build = Ci::Build.find_by_id(params[:id])
|
|
||||||
|
|
||||||
validate_build!(build) do
|
|
||||||
forbidden! unless build_token_valid?(build)
|
|
||||||
end
|
|
||||||
|
|
||||||
build
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_build!(build)
|
|
||||||
not_found! unless build
|
|
||||||
|
|
||||||
yield if block_given?
|
|
||||||
|
|
||||||
project = build.project
|
|
||||||
forbidden!('Project has been deleted!') if project.nil? || project.pending_delete?
|
|
||||||
forbidden!('Build has been erased!') if build.erased?
|
|
||||||
end
|
|
||||||
|
|
||||||
def runner_registration_token_valid?
|
|
||||||
ActiveSupport::SecurityUtils.variable_size_secure_compare(
|
|
||||||
params[:token],
|
|
||||||
current_application_settings.runners_registration_token)
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_token_valid?(build)
|
|
||||||
token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
|
|
||||||
|
|
||||||
# We require to also check `runners_token` to maintain compatibility with old version of runners
|
|
||||||
token && (build.valid_token?(token) || build.project.valid_runners_token?(token))
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_runner_info
|
|
||||||
return unless update_runner?
|
|
||||||
|
|
||||||
current_runner.contacted_at = Time.now
|
|
||||||
current_runner.assign_attributes(get_runner_version_from_params)
|
|
||||||
current_runner.save if current_runner.changed?
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_runner?
|
|
||||||
# Use a random threshold to prevent beating DB updates.
|
|
||||||
# It generates a distribution between [40m, 80m].
|
|
||||||
#
|
|
||||||
contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY)
|
|
||||||
|
|
||||||
current_runner.contacted_at.nil? ||
|
|
||||||
(Time.now - current_runner.contacted_at) >= contacted_at_max_age
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_not_found!
|
|
||||||
if headers['User-Agent'].to_s =~ /gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /
|
|
||||||
no_content!
|
|
||||||
else
|
|
||||||
not_found!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_runner
|
|
||||||
@runner ||= Runner.find_by_token(params[:token].to_s)
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_runner_version_from_params
|
|
||||||
return unless params["info"].present?
|
|
||||||
attributes_for_keys(%w(name version revision platform architecture), params["info"])
|
|
||||||
end
|
|
||||||
|
|
||||||
def max_artifacts_size
|
|
||||||
current_application_settings.max_artifacts_size.megabytes.to_i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,50 +0,0 @@
|
||||||
module Ci
|
|
||||||
module API
|
|
||||||
class Runners < Grape::API
|
|
||||||
resource :runners do
|
|
||||||
desc 'Delete a runner'
|
|
||||||
params do
|
|
||||||
requires :token, type: String, desc: 'The unique token of the runner'
|
|
||||||
end
|
|
||||||
delete "delete" do
|
|
||||||
authenticate_runner!
|
|
||||||
|
|
||||||
status(200)
|
|
||||||
Ci::Runner.find_by_token(params[:token]).destroy
|
|
||||||
end
|
|
||||||
|
|
||||||
desc 'Register a new runner' do
|
|
||||||
success Entities::Runner
|
|
||||||
end
|
|
||||||
params do
|
|
||||||
requires :token, type: String, desc: 'The unique token of the runner'
|
|
||||||
optional :description, type: String, desc: 'The description of the runner'
|
|
||||||
optional :tag_list, type: Array[String], desc: 'A list of tags the runner should run for'
|
|
||||||
optional :run_untagged, type: Boolean, desc: 'Flag if the runner should execute untagged jobs'
|
|
||||||
optional :locked, type: Boolean, desc: 'Lock this runner for this specific project'
|
|
||||||
end
|
|
||||||
post "register" do
|
|
||||||
runner_params = declared(params, include_missing: false).except(:token)
|
|
||||||
|
|
||||||
runner =
|
|
||||||
if runner_registration_token_valid?
|
|
||||||
# Create shared runner. Requires admin access
|
|
||||||
Ci::Runner.create(runner_params.merge(is_shared: true))
|
|
||||||
elsif project = Project.find_by(runners_token: params[:token])
|
|
||||||
# Create a specific runner for project.
|
|
||||||
project.runners.create(runner_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
return forbidden! unless runner
|
|
||||||
|
|
||||||
if runner.id
|
|
||||||
runner.update(get_runner_version_from_params)
|
|
||||||
present runner, with: Entities::Runner
|
|
||||||
else
|
|
||||||
not_found!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,39 +0,0 @@
|
||||||
module Ci
|
|
||||||
module API
|
|
||||||
class Triggers < Grape::API
|
|
||||||
resource :projects do
|
|
||||||
desc 'Trigger a GitLab CI project build' do
|
|
||||||
success Entities::TriggerRequest
|
|
||||||
end
|
|
||||||
params do
|
|
||||||
requires :id, type: Integer, desc: 'The ID of a CI project'
|
|
||||||
requires :ref, type: String, desc: "The name of project's branch or tag"
|
|
||||||
requires :token, type: String, desc: 'The unique token of the trigger'
|
|
||||||
optional :variables, type: Hash, desc: 'Optional build variables'
|
|
||||||
end
|
|
||||||
post ":id/refs/:ref/trigger" do
|
|
||||||
project = Project.find_by(ci_id: params[:id])
|
|
||||||
trigger = Ci::Trigger.find_by_token(params[:token])
|
|
||||||
not_found! unless project && trigger
|
|
||||||
unauthorized! unless trigger.project == project
|
|
||||||
|
|
||||||
# Validate variables
|
|
||||||
variables = params[:variables].to_h
|
|
||||||
unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
|
|
||||||
render_api_error!('variables needs to be a map of key-valued strings', 400)
|
|
||||||
end
|
|
||||||
|
|
||||||
# create request and trigger builds
|
|
||||||
result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref], variables)
|
|
||||||
pipeline = result.pipeline
|
|
||||||
|
|
||||||
if pipeline.persisted?
|
|
||||||
present result.trigger_request, with: Entities::TriggerRequest
|
|
||||||
else
|
|
||||||
render_validation_error!(pipeline)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,912 +0,0 @@
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
describe Ci::API::Builds do
|
|
||||||
let(:runner) { FactoryGirl.create(:ci_runner, tag_list: %w(mysql ruby)) }
|
|
||||||
let(:project) { FactoryGirl.create(:project, shared_runners_enabled: false) }
|
|
||||||
let(:last_update) { nil }
|
|
||||||
|
|
||||||
describe "Builds API for runners" do
|
|
||||||
let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
|
|
||||||
|
|
||||||
before do
|
|
||||||
project.runners << runner
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "POST /builds/register" do
|
|
||||||
let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
|
||||||
let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' }
|
|
||||||
let!(:last_update) { }
|
|
||||||
let!(:new_update) { }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_container_registry_config(enabled: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
shared_examples 'no builds available' do
|
|
||||||
context 'when runner sends version in User-Agent' do
|
|
||||||
context 'for stable version' do
|
|
||||||
it 'gives 204 and set X-GitLab-Last-Update' do
|
|
||||||
expect(response).to have_http_status(204)
|
|
||||||
expect(response.header).to have_key('X-GitLab-Last-Update')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when last_update is up-to-date' do
|
|
||||||
let(:last_update) { runner.ensure_runner_queue_value }
|
|
||||||
|
|
||||||
it 'gives 204 and set the same X-GitLab-Last-Update' do
|
|
||||||
expect(response).to have_http_status(204)
|
|
||||||
expect(response.header['X-GitLab-Last-Update'])
|
|
||||||
.to eq(last_update)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when last_update is outdated' do
|
|
||||||
let(:last_update) { runner.ensure_runner_queue_value }
|
|
||||||
let(:new_update) { runner.tick_runner_queue }
|
|
||||||
|
|
||||||
it 'gives 204 and set a new X-GitLab-Last-Update' do
|
|
||||||
expect(response).to have_http_status(204)
|
|
||||||
expect(response.header['X-GitLab-Last-Update'])
|
|
||||||
.to eq(new_update)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for beta version' do
|
|
||||||
let(:user_agent) { 'gitlab-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
|
|
||||||
|
|
||||||
context "when runner doesn't have a User-Agent" do
|
|
||||||
let(:user_agent) { nil }
|
|
||||||
it { expect(response).to have_http_status(404) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when an old image syntax is used' do
|
|
||||||
before do
|
|
||||||
build.update!(options: { image: 'codeclimate' })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'starts a build' do
|
|
||||||
register_builds info: { platform: :darwin }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
expect(json_response["options"]).to eq({ "image" => "codeclimate" })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when a new image syntax is used' do
|
|
||||||
before do
|
|
||||||
build.update!(options: { image: { name: 'codeclimate' } })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'starts a build' do
|
|
||||||
register_builds info: { platform: :darwin }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
expect(json_response["options"]).to eq({ "image" => "codeclimate" })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when an old service syntax is used' do
|
|
||||||
before do
|
|
||||||
build.update!(options: { services: ['mysql'] })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'starts a build' do
|
|
||||||
register_builds info: { platform: :darwin }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
expect(json_response["options"]).to eq({ "services" => ["mysql"] })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when a new service syntax is used' do
|
|
||||||
before do
|
|
||||||
build.update!(options: { services: [name: 'mysql'] })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'starts a build' do
|
|
||||||
register_builds info: { platform: :darwin }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
expect(json_response["options"]).to eq({ "services" => ["mysql"] })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when no image or service is defined' do
|
|
||||||
before do
|
|
||||||
build.update!(options: {})
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'starts a build' do
|
|
||||||
register_builds info: { platform: :darwin }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
|
|
||||||
expect(json_response["options"]).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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(response.headers).not_to have_key('X-GitLab-Last-Update')
|
|
||||||
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_JOB_NAME", "value" => "spinach", "public" => true },
|
|
||||||
{ "key" => "CI_JOB_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
|
|
||||||
|
|
||||||
context 'when concurrently updating build' do
|
|
||||||
before do
|
|
||||||
expect_any_instance_of(Ci::Build).to receive(:run!)
|
|
||||||
.and_raise(ActiveRecord::StaleObjectError.new(nil, nil))
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns a conflict' do
|
|
||||||
register_builds info: { platform: :darwin }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(409)
|
|
||||||
expect(response.headers).not_to have_key('X-GitLab-Last-Update')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'registry credentials' do
|
|
||||||
let(:registry_credentials) do
|
|
||||||
{ 'type' => 'registry',
|
|
||||||
'url' => 'registry.example.com:5005',
|
|
||||||
'username' => 'gitlab-ci-token',
|
|
||||||
'password' => build.token }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when registry is enabled' do
|
|
||||||
before do
|
|
||||||
stub_container_registry_config(enabled: true, host_port: 'registry.example.com:5005')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'sends registry credentials key' do
|
|
||||||
register_builds info: { platform: :darwin }
|
|
||||||
|
|
||||||
expect(json_response).to have_key('credentials')
|
|
||||||
expect(json_response['credentials']).to include(registry_credentials)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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 }
|
|
||||||
|
|
||||||
expect(json_response).to have_key('credentials')
|
|
||||||
expect(json_response['credentials']).not_to include(registry_credentials)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when docker configuration options are used' do
|
|
||||||
let!(:build) { create(:ci_build, :extended_options, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
|
||||||
|
|
||||||
it 'starts a build' do
|
|
||||||
register_builds info: { platform: :darwin }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
expect(json_response['options']['image']).to eq('ruby:2.1')
|
|
||||||
expect(json_response['options']['services']).to eq(['postgres', 'docker:dind'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when builds are finished' do
|
|
||||||
before do
|
|
||||||
build.success
|
|
||||||
register_builds
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'no builds available'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for other project with builds' do
|
|
||||||
before do
|
|
||||||
build.success
|
|
||||||
create(:ci_build, :pending)
|
|
||||||
register_builds
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'no builds available'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for shared runner' do
|
|
||||||
let!(:runner) { create(:ci_runner, :shared, token: "SharedRunner") }
|
|
||||||
|
|
||||||
before do
|
|
||||||
register_builds(runner.token)
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'no builds available'
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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_JOB_NAME", "value" => "spinach", "public" => true },
|
|
||||||
{ "key" => "CI_JOB_STAGE", "value" => "test", "public" => true },
|
|
||||||
{ "key" => "CI_PIPELINE_TRIGGERED", "value" => "true", "public" => true },
|
|
||||||
{ "key" => "DB_NAME", "value" => "postgres", "public" => true },
|
|
||||||
{ "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
|
|
||||||
{ "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false }
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with multiple builds' do
|
|
||||||
before do
|
|
||||||
build.success
|
|
||||||
end
|
|
||||||
|
|
||||||
let!(:test_build) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
|
|
||||||
|
|
||||||
it "returns dependent builds" do
|
|
||||||
register_builds info: { platform: :darwin }
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
%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
|
|
||||||
register_builds info: { param => value }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
runner.reload
|
|
||||||
is_expected.to eq(value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when build has no tags' do
|
|
||||||
before do
|
|
||||||
build.update(tags: [])
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when runner is allowed to pick untagged builds' do
|
|
||||||
before do
|
|
||||||
runner.update_column(:run_untagged, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
before do
|
|
||||||
runner.update_column(:run_untagged, false)
|
|
||||||
register_builds
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'no builds available'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when runner is paused' do
|
|
||||||
let(:runner) { create(:ci_runner, :inactive, token: 'InactiveRunner') }
|
|
||||||
|
|
||||||
it 'responds with 404' do
|
|
||||||
register_builds
|
|
||||||
|
|
||||||
expect(response).to have_http_status 404
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not update runner info' do
|
|
||||||
expect { register_builds }
|
|
||||||
.not_to change { runner.reload.contacted_at }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def register_builds(token = runner.token, **params)
|
|
||||||
new_params = params.merge(token: token, last_update: last_update)
|
|
||||||
|
|
||||||
post ci_api("/builds/register"), new_params, { 'User-Agent' => user_agent }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "PUT /builds/:id" do
|
|
||||||
let(:build) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
build.run!
|
|
||||||
put ci_api("/builds/#{build.id}"), token: runner.token
|
|
||||||
end
|
|
||||||
|
|
||||||
it "updates a running build" do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not override trace information when no trace is given' do
|
|
||||||
expect(build.reload.trace.raw).to eq 'BUILD TRACE'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'job has been erased' do
|
|
||||||
let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
|
|
||||||
|
|
||||||
it 'responds with forbidden' do
|
|
||||||
expect(response.status).to eq 403
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'PATCH /builds/:id/trace.txt' do
|
|
||||||
let(:build) do
|
|
||||||
attributes = { runner_id: runner.id, pipeline: pipeline }
|
|
||||||
create(:ci_build, :running, :trace, attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
|
|
||||||
let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
|
|
||||||
let(:update_interval) { 10.seconds.to_i }
|
|
||||||
|
|
||||||
def patch_the_trace(content = ' appended', request_headers = nil)
|
|
||||||
unless request_headers
|
|
||||||
build.trace.read do |stream|
|
|
||||||
offset = stream.size
|
|
||||||
limit = offset + content.length - 1
|
|
||||||
request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
|
|
||||||
end
|
|
||||||
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
|
|
||||||
|
|
||||||
before do
|
|
||||||
initial_patch_the_trace
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when request is valid' do
|
|
||||||
it 'gets correct response' do
|
|
||||||
expect(response.status).to eq 202
|
|
||||||
expect(build.reload.trace.raw).to eq 'BUILD TRACE appended'
|
|
||||||
expect(response.header).to have_key 'Range'
|
|
||||||
expect(response.header).to have_key 'Build-Status'
|
|
||||||
end
|
|
||||||
|
|
||||||
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.raw).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.raw).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.raw).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.raw).to eq 'BUILD TRACE appended'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when project for the build has been deleted' do
|
|
||||||
let(:build) do
|
|
||||||
attributes = { runner_id: runner.id, pipeline: pipeline }
|
|
||||||
create(:ci_build, :running, :trace, attributes) do |build|
|
|
||||||
build.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
|
|
||||||
expect(build.reload.trace.raw).to eq 'BUILD TRACE appended'
|
|
||||||
expect(response.header).to have_key 'Range'
|
|
||||||
expect(response.header).to have_key 'Build-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 build has been errased' do
|
|
||||||
let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
|
|
||||||
|
|
||||||
it { expect(response.status).to eq 403 }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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') }
|
|
||||||
let(:build) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
|
|
||||||
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") }
|
|
||||||
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(:token) { build.token }
|
|
||||||
let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => token) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
build.run!
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "POST /builds/:id/artifacts/authorize" do
|
|
||||||
context "authorizes posting artifact to running build" do
|
|
||||||
it "using token as parameter" do
|
|
||||||
post authorize_url, { token: build.token }, 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 "using token as header" do
|
|
||||||
post authorize_url, {}, headers_with_token
|
|
||||||
|
|
||||||
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 "using runners token" do
|
|
||||||
post authorize_url, { token: build.project.runners_token }, 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 "reject requests that did not go through gitlab-workhorse" do
|
|
||||||
headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
|
|
||||||
|
|
||||||
post authorize_url, { token: build.token }, headers
|
|
||||||
|
|
||||||
expect(response).to have_http_status(500)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "fails to post too large artifact" do
|
|
||||||
it "using token as parameter" do
|
|
||||||
stub_application_setting(max_artifacts_size: 0)
|
|
||||||
|
|
||||||
post authorize_url, { token: build.token, filesize: 100 }, headers
|
|
||||||
|
|
||||||
expect(response).to have_http_status(413)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "using token as header" do
|
|
||||||
stub_application_setting(max_artifacts_size: 0)
|
|
||||||
|
|
||||||
post authorize_url, { filesize: 100 }, headers_with_token
|
|
||||||
|
|
||||||
expect(response).to have_http_status(413)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'authorization token is invalid' do
|
|
||||||
before do
|
|
||||||
post authorize_url, { token: 'invalid', filesize: 100 }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'responds with forbidden' do
|
|
||||||
expect(response).to have_http_status(403)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "POST /builds/:id/artifacts" do
|
|
||||||
context "disable sanitizer" 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
|
|
||||||
|
|
||||||
describe 'build has been erased' do
|
|
||||||
let(:build) { create(:ci_build, erased_at: Time.now) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
upload_artifacts(file_upload, headers_with_token)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'responds with forbidden' do
|
|
||||||
expect(response.status).to eq 403
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'uploading artifacts for a running build' do
|
|
||||||
shared_examples 'successful artifacts upload' do
|
|
||||||
it 'updates successfully' do
|
|
||||||
response_filename =
|
|
||||||
json_response['artifacts_file']['filename']
|
|
||||||
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
expect(response_filename).to eq(file_upload.original_filename)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'uses regular file post' do
|
|
||||||
before do
|
|
||||||
upload_artifacts(file_upload, headers_with_token, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'successful artifacts upload'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'uses accelerated file post' do
|
|
||||||
before do
|
|
||||||
upload_artifacts(file_upload, headers_with_token, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'successful artifacts upload'
|
|
||||||
end
|
|
||||||
|
|
||||||
context '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
|
|
||||||
let(:token) { build.project.runners_token }
|
|
||||||
|
|
||||||
before do
|
|
||||||
upload_artifacts(file_upload, headers_with_token)
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'successful artifacts upload'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'posts artifacts file and metadata file' do
|
|
||||||
let!(:artifacts) { file_upload }
|
|
||||||
let!(:metadata) { file_upload2 }
|
|
||||||
|
|
||||||
let(:stored_artifacts_file) { build.reload.artifacts_file.file }
|
|
||||||
let(:stored_metadata_file) { build.reload.artifacts_metadata.file }
|
|
||||||
let(:stored_artifacts_size) { build.reload.artifacts_size }
|
|
||||||
|
|
||||||
before do
|
|
||||||
post(post_url, post_data, headers_with_token)
|
|
||||||
end
|
|
||||||
|
|
||||||
context '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 '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
|
|
||||||
|
|
||||||
context 'with an expire date' do
|
|
||||||
let!(:artifacts) { file_upload }
|
|
||||||
let(:default_artifacts_expire_in) {}
|
|
||||||
|
|
||||||
let(:post_data) do
|
|
||||||
{ 'file.path' => artifacts.path,
|
|
||||||
'file.name' => artifacts.original_filename,
|
|
||||||
'expire_in' => expire_in }
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_application_setting(
|
|
||||||
default_artifacts_expire_in: default_artifacts_expire_in)
|
|
||||||
|
|
||||||
post(post_url, post_data, headers_with_token)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with an expire_in given' do
|
|
||||||
let(:expire_in) { '7 days' }
|
|
||||||
|
|
||||||
it 'updates when specified' do
|
|
||||||
build.reload
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
expect(json_response['artifacts_expire_at']).not_to be_empty
|
|
||||||
expect(build.artifacts_expire_at)
|
|
||||||
.to be_within(5.minutes).of(7.days.from_now)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with no expire_in given' do
|
|
||||||
let(:expire_in) { nil }
|
|
||||||
|
|
||||||
it 'ignores if not specified' do
|
|
||||||
build.reload
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
expect(json_response['artifacts_expire_at']).to be_nil
|
|
||||||
expect(build.artifacts_expire_at).to be_nil
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with application default' do
|
|
||||||
context 'default to 5 days' do
|
|
||||||
let(:default_artifacts_expire_in) { '5 days' }
|
|
||||||
|
|
||||||
it 'sets to application default' do
|
|
||||||
build.reload
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
expect(json_response['artifacts_expire_at'])
|
|
||||||
.not_to be_empty
|
|
||||||
expect(build.artifacts_expire_at)
|
|
||||||
.to be_within(5.minutes).of(5.days.from_now)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'default to 0' do
|
|
||||||
let(:default_artifacts_expire_in) { '0' }
|
|
||||||
|
|
||||||
it 'does not set expire_in' do
|
|
||||||
build.reload
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
expect(json_response['artifacts_expire_at']).to be_nil
|
|
||||||
expect(build.artifacts_expire_at).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "artifacts file is too large" do
|
|
||||||
it "fails to post too large artifact" do
|
|
||||||
stub_application_setting(max_artifacts_size: 0)
|
|
||||||
upload_artifacts(file_upload, headers_with_token)
|
|
||||||
expect(response).to have_http_status(413)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "artifacts post request does not contain file" do
|
|
||||||
it "fails to post artifacts without file" do
|
|
||||||
post post_url, {}, headers_with_token
|
|
||||||
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 post_url, { token: build.token }, {}
|
|
||||||
expect(response).to have_http_status(403)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "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
|
|
||||||
|
|
||||||
after do
|
|
||||||
FileUtils.remove_entry @tmpdir
|
|
||||||
end
|
|
||||||
|
|
||||||
it "fails to post artifacts for outside of tmp path" do
|
|
||||||
upload_artifacts(file_upload, headers_with_token)
|
|
||||||
expect(response).to have_http_status(400)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'DELETE /builds/:id/artifacts' do
|
|
||||||
let(:build) { create(:ci_build, :artifacts) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
delete delete_url, token: build.token
|
|
||||||
end
|
|
||||||
|
|
||||||
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'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'GET /builds/:id/artifacts' do
|
|
||||||
before do
|
|
||||||
get get_url, token: token
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'build has artifacts' do
|
|
||||||
let(:build) { create(:ci_build, :artifacts) }
|
|
||||||
let(:download_headers) do
|
|
||||||
{ 'Content-Transfer-Encoding' => 'binary',
|
|
||||||
'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
|
|
||||||
end
|
|
||||||
|
|
||||||
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'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'build does not has artifacts' do
|
|
||||||
let(:token) { build.token }
|
|
||||||
|
|
||||||
it 'responds with not found' do
|
|
||||||
expect(response).to have_http_status(404)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,127 +0,0 @@
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
describe Ci::API::Runners do
|
|
||||||
include StubGitlabCalls
|
|
||||||
|
|
||||||
let(:registration_token) { 'abcdefg123456' }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_gitlab_calls
|
|
||||||
stub_application_setting(runners_registration_token: registration_token)
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "POST /runners/register" do
|
|
||||||
context 'when runner token is provided' do
|
|
||||||
before do
|
|
||||||
post ci_api("/runners/register"), token: registration_token
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates runner with default values' do
|
|
||||||
expect(response).to have_http_status 201
|
|
||||||
expect(Ci::Runner.first.run_untagged).to be true
|
|
||||||
expect(Ci::Runner.first.token).not_to eq(registration_token)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when runner description is provided' do
|
|
||||||
before do
|
|
||||||
post ci_api("/runners/register"), token: registration_token,
|
|
||||||
description: "server.hostname"
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates runner' do
|
|
||||||
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
|
|
||||||
before do
|
|
||||||
post ci_api("/runners/register"), token: registration_token,
|
|
||||||
tag_list: "tag1, tag2"
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates runner' do
|
|
||||||
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 ci_api("/runners/register"), token: registration_token,
|
|
||||||
run_untagged: false,
|
|
||||||
tag_list: ['tag']
|
|
||||||
|
|
||||||
expect(response).to have_http_status 201
|
|
||||||
expect(Ci::Runner.first.run_untagged).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when tags are not provided' do
|
|
||||||
it 'does not create runner' do
|
|
||||||
post ci_api("/runners/register"), token: registration_token,
|
|
||||||
run_untagged: false
|
|
||||||
|
|
||||||
expect(response).to have_http_status 404
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when project token is provided' do
|
|
||||||
let(:project) { FactoryGirl.create(:project) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
post ci_api("/runners/register"), token: project.runners_token
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates runner' do
|
|
||||||
expect(response).to have_http_status 201
|
|
||||||
expect(project.runners.size).to eq(1)
|
|
||||||
expect(Ci::Runner.first.token).not_to eq(registration_token)
|
|
||||||
expect(Ci::Runner.first.token).not_to eq(project.runners_token)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when token is invalid' do
|
|
||||||
it 'returns 403 error' do
|
|
||||||
post ci_api("/runners/register"), token: 'invalid'
|
|
||||||
|
|
||||||
expect(response).to have_http_status 403
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when no token provided' do
|
|
||||||
it 'returns 400 error' do
|
|
||||||
post ci_api("/runners/register")
|
|
||||||
|
|
||||||
expect(response).to have_http_status 400
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
%w(name version revision platform architecture).each do |param|
|
|
||||||
context "creates runner with #{param} saved" do
|
|
||||||
let(:value) { "#{param}_value" }
|
|
||||||
|
|
||||||
subject { Ci::Runner.first.read_attribute(param.to_sym) }
|
|
||||||
|
|
||||||
it do
|
|
||||||
post ci_api("/runners/register"), token: registration_token, info: { param => value }
|
|
||||||
expect(response).to have_http_status 201
|
|
||||||
is_expected.to eq(value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "DELETE /runners/delete" do
|
|
||||||
it 'returns 200' do
|
|
||||||
runner = FactoryGirl.create(:ci_runner)
|
|
||||||
delete ci_api("/runners/delete"), token: runner.token
|
|
||||||
|
|
||||||
expect(response).to have_http_status 200
|
|
||||||
expect(Ci::Runner.count).to eq(0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,90 +0,0 @@
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
describe Ci::API::Triggers do
|
|
||||||
describe 'POST /projects/:project_id/refs/:ref/trigger' do
|
|
||||||
let!(:trigger_token) { 'secure token' }
|
|
||||||
let!(:project) { create(:project, :repository, ci_id: 10) }
|
|
||||||
let!(:project2) { create(:project, ci_id: 11) }
|
|
||||||
|
|
||||||
let!(:trigger) do
|
|
||||||
create(:ci_trigger,
|
|
||||||
project: project,
|
|
||||||
token: trigger_token,
|
|
||||||
owner: create(:user))
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:options) do
|
|
||||||
{
|
|
||||||
token: trigger_token
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_ci_pipeline_to_return_yaml_file
|
|
||||||
|
|
||||||
project.add_developer(trigger.owner)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'Handles errors' do
|
|
||||||
it 'returns bad request if token is missing' do
|
|
||||||
post ci_api("/projects/#{project.ci_id}/refs/master/trigger")
|
|
||||||
expect(response).to have_http_status(400)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns not found if project is not found' do
|
|
||||||
post ci_api('/projects/0/refs/master/trigger'), options
|
|
||||||
expect(response).to have_http_status(404)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns unauthorized if token is for different project' do
|
|
||||||
post ci_api("/projects/#{project2.ci_id}/refs/master/trigger"), options
|
|
||||||
expect(response).to have_http_status(401)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'Have a commit' do
|
|
||||||
let(:pipeline) { project.pipelines.last }
|
|
||||||
|
|
||||||
it 'creates builds' do
|
|
||||||
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
pipeline.builds.reload
|
|
||||||
expect(pipeline.builds.pending.size).to eq(2)
|
|
||||||
expect(pipeline.builds.size).to eq(5)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns bad request with no builds created if there\'s no commit for that ref' do
|
|
||||||
post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options
|
|
||||||
expect(response).to have_http_status(400)
|
|
||||||
expect(json_response['message']['base'])
|
|
||||||
.to contain_exactly('Reference not found')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'Validates variables' do
|
|
||||||
let(:variables) do
|
|
||||||
{ 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'validates variables to be a hash' do
|
|
||||||
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value')
|
|
||||||
expect(response).to have_http_status(400)
|
|
||||||
|
|
||||||
expect(json_response['error']).to eq('variables is invalid')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'validates variables needs to be a map of key-valued strings' do
|
|
||||||
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) })
|
|
||||||
expect(response).to have_http_status(400)
|
|
||||||
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates trigger request with variables' do
|
|
||||||
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables)
|
|
||||||
expect(response).to have_http_status(201)
|
|
||||||
pipeline.builds.reload
|
|
||||||
expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in New Issue