Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-30 15:09:40 +00:00
parent 55b5a8778c
commit e65a1b9830
65 changed files with 3324 additions and 2633 deletions

View File

@ -0,0 +1,28 @@
import Vue from 'vue';
import ActivityChart from './components/activity_chart.vue';
export default () => {
const containers = document.querySelectorAll('.js-project-analytics-chart');
if (!containers) {
return false;
}
return containers.forEach(container => {
const { chartData } = container.dataset;
const formattedData = JSON.parse(chartData);
return new Vue({
el: container,
provide: {
formattedData,
},
components: {
ActivityChart,
},
render(createElement) {
return createElement('activity-chart');
},
});
});
};

View File

@ -0,0 +1,42 @@
<script>
import { s__ } from '~/locale';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
export default {
i18n: {
noDataMsg: s__(
'ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already.',
),
},
components: {
GlColumnChart,
},
inject: {
formattedData: {
default: {},
},
},
computed: {
seriesData() {
return {
full: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]),
};
},
},
};
</script>
<template>
<div class="gl-xs-w-full">
<gl-column-chart
v-if="formattedData.keys"
:data="seriesData"
:x-axis-title="__('Value')"
:y-axis-title="__('Number of events')"
:x-axis-type="'category'"
/>
<p v-else data-testid="noActivityChartData">
{{ $options.i18n.noDataMsg }}
</p>
</div>
</template>

View File

@ -127,7 +127,13 @@ export default {
emailPatchPath: state => state.diffs.emailPatchPath,
retrievingBatches: state => state.diffs.retrievingBatches,
}),
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion', 'currentDiffFileId']),
...mapState('diffs', [
'showTreeList',
'isLoading',
'startVersion',
'currentDiffFileId',
'isTreeLoaded',
]),
...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
@ -400,7 +406,7 @@ export default {
<template>
<div v-show="shouldShow">
<div v-if="isLoading" class="loading"><gl-loading-icon size="lg" /></div>
<div v-if="isLoading || !isTreeLoaded" class="loading"><gl-loading-icon size="lg" /></div>
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
<compare-versions
:merge-request-diffs="mergeRequestDiffs"

View File

@ -15,6 +15,7 @@ const whiteSpaceFromCookie = Cookies.get(DIFF_WHITESPACE_COOKIE_NAME);
export default () => ({
isLoading: true,
isTreeLoaded: false,
isBatchLoading: false,
retrievingBatches: false,
addedLines: null,

View File

@ -323,6 +323,7 @@ export default {
[types.SET_TREE_DATA](state, { treeEntries, tree }) {
state.treeEntries = treeEntries;
state.tree = tree;
state.isTreeLoaded = true;
},
[types.SET_RENDER_TREE_LIST](state, renderTreeList) {
state.renderTreeList = renderTreeList;

View File

@ -0,0 +1,3 @@
import initActivityCharts from '~/analytics/product_analytics/activity_charts_bundle';
document.addEventListener('DOMContentLoaded', () => initActivityCharts());

View File

@ -15,7 +15,12 @@ module Groups
end
def update
if @group.update(group_variables_params)
update_result = Ci::ChangeVariablesService.new(
container: @group, current_user: current_user,
params: group_variables_params
).execute
if update_result
respond_to do |format|
format.json { render_group_variables }
end

View File

@ -16,6 +16,19 @@ class Projects::ProductAnalyticsController < Projects::ApplicationController
@event = product_analytics_events.try(:first)
end
def graphs
@graphs = []
@timerange = 30
requested_graphs = %w(platform os_timezone br_lang doc_charset)
requested_graphs.each do |graph|
@graphs << ProductAnalytics::BuildGraphService
.new(project, { graph: graph, timerange: @timerange })
.execute
end
end
private
def product_analytics_events

View File

@ -249,14 +249,6 @@ module Ci
pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) }
end
after_transition any => [:success] do |pipeline|
next unless Gitlab::Ci::Features.keep_latest_artifacts_for_ref_enabled?(pipeline.project)
pipeline.run_after_commit do
Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(pipeline.id)
end
end
end
scope :internal, -> { where(source: internal_sources) }

View File

@ -3,6 +3,7 @@
module Ci
class Ref < ApplicationRecord
extend Gitlab::Ci::Model
include AfterCommitQueue
include Gitlab::OptimisticLocking
FAILING_STATUSES = %w[failed broken still_failing].freeze
@ -15,6 +16,7 @@ module Ci
transition unknown: :success
transition fixed: :success
transition %i[failed broken still_failing] => :fixed
transition success: same
end
event :do_fail do
@ -29,6 +31,14 @@ module Ci
state :fixed, value: 3
state :broken, value: 4
state :still_failing, value: 5
after_transition any => [:fixed, :success] do |ci_ref|
next unless ::Gitlab::Ci::Features.keep_latest_artifacts_for_ref_enabled?(ci_ref.project)
ci_ref.run_after_commit do
Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(ci_ref.last_finished_pipeline_id)
end
end
end
class << self

View File

@ -19,13 +19,14 @@ module CommitStatusEnums
scheduler_failure: 11,
data_integrity_failure: 12,
forward_deployment_failure: 13,
protected_environment_failure: 1_000,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
upstream_bridge_project_not_found: 1_004,
insufficient_upstream_permissions: 1_005,
bridge_pipeline_is_child_pipeline: 1_006,
downstream_pipeline_creation_failed: 1_007
}
end
end
CommitStatusEnums.prepend_if_ee('EE::CommitStatusEnums')

View File

@ -63,6 +63,8 @@ class ExternalPullRequest < ApplicationRecord
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY', value: source_repository)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY', value: target_repository)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch)

View File

@ -20,6 +20,10 @@ class ProductAnalyticsEvent < ApplicationRecord
where('collector_tstamp BETWEEN ? AND ? ', today - duration + 1, today + 1)
}
def self.count_by_graph(graph, days)
group(graph).timerange(days).count
end
def as_json_wo_empty
as_json.compact
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
module Ci
class ChangeVariableService < BaseContainerService
def execute
case params[:action]
when :create
container.variables.create(params[:variable_params])
when :update
variable.tap do |target_variable|
target_variable.update(params[:variable_params].except(:key))
end
when :destroy
variable.tap do |target_variable|
target_variable.destroy
end
end
end
private
def variable
container.variables.find_by!(params[:variable_params].slice(:key)) # rubocop:disable CodeReuse/ActiveRecord
end
end
end
::Ci::ChangeVariableService.prepend_if_ee('EE::Ci::ChangeVariableService')

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Ci
class ChangeVariablesService < BaseContainerService
def execute
container.update(params)
end
end
end
::Ci::ChangeVariablesService.prepend_if_ee('EE::Ci::ChangeVariablesService')

View File

@ -26,9 +26,5 @@ module Git
def removing_tag?
Gitlab::Git.blank_ref?(newrev)
end
def tag_name
Gitlab::Git.ref_name(ref)
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module ProductAnalytics
class BuildGraphService
def initialize(project, params)
@project = project
@params = params
end
def execute
graph = @params[:graph].to_sym
timerange = @params[:timerange].days
results = product_analytics_events.count_by_graph(graph, timerange)
{
id: graph,
keys: results.keys,
values: results.values
}
end
private
def product_analytics_events
@project.product_analytics_events
end
end
end

View File

@ -56,8 +56,9 @@
- if git_import_enabled?
%div
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' }, **tracking_attrs(track_label, 'click_button', 'repo_url') }
= icon('git', text: 'Repo by URL')
%button.btn.btn-svg.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' }, **tracking_attrs(track_label, 'click_button', 'repo_url') }
= sprite_icon('link', size: 16, css_class: 'gl-icon')
= _('Repo by URL')
- if manifest_import_enabled?
%div

View File

@ -0,0 +1,6 @@
- graph = local_assigns.fetch(:graph)
%h3
= graph[:id]
.js-project-analytics-chart{ "data-chart-data": graph.to_json, "data-chart-id": graph[:id] }

View File

@ -2,6 +2,8 @@
%ul.nav-links
= nav_link(path: 'product_analytics#index') do
= link_to _('Events'), project_product_analytics_path(@project)
= nav_link(path: 'product_analytics#graphs') do
= link_to 'Graphs', graphs_project_product_analytics_path(@project)
= nav_link(path: 'product_analytics#test') do
= link_to _('Test'), test_project_product_analytics_path(@project)
= nav_link(path: 'product_analytics#setup') do

View File

@ -0,0 +1,12 @@
- page_title _('Product Analytics')
= render 'links'
%p
= _('Showing graphs based on events of the last %{timerange} days.') % { timerange: @timerange }
- @graphs.each_slice(2) do |pair|
.row.append-bottom-10
- pair.each do |graph|
.col-md-6{ id: graph[:id] }
= render 'graph', graph: graph

View File

@ -1,4 +1,4 @@
- page_title 'Product Analytics'
- page_title _('Product Analytics')
= render 'links'

View File

@ -1,4 +1,7 @@
= render "links"
- page_title _('Product Analytics')
= render 'links'
%p
= _('Copy the code below to implement tracking in your application:')

View File

@ -1,3 +1,5 @@
- page_title _('Product Analytics')
= render 'links'
%p

View File

@ -0,0 +1,5 @@
---
title: "Add ENV vars that expose source and target repository for CI Pipelines that run on an External Pull Requests."
merge_request: 37616
author: Rafael Dohms @rdohms
type: added

View File

@ -0,0 +1,5 @@
---
title: Replace fa-git icons with link svg
merge_request: 38078
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Keep large spinner while MR file tree is loading
merge_request: 36446
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add sign_in_count to /users/:id API for admins
merge_request: 35726
author: Luc Didry
type: changed

View File

@ -8,94 +8,122 @@
# * timeout profile after 30 seconds
# * write to $TMPDIR/stackprof.$PID.$RAND.profile
if Gitlab::Utils.to_boolean(ENV['STACKPROF_ENABLED'].to_s)
Gitlab::Cluster::LifecycleEvents.on_worker_start do
require 'stackprof'
require 'tmpdir'
module Gitlab
class StackProf
# this is a workaround for sidekiq, which defines its own SIGUSR2 handler.
# by defering to the sidekiq startup event, we get to set up our own
# handler late enough.
# see also: https://github.com/mperham/sidekiq/pull/4653
def self.install
require 'stackprof'
require 'tmpdir'
Gitlab::AppJsonLogger.info "stackprof: listening on SIGUSR2 signal"
# create a pipe in order to propagate signal out of the signal handler
# see also: https://cr.yp.to/docs/selfpipe.html
read, write = IO.pipe
# create a separate thread that polls for signals on the pipe.
#
# this way we do not execute in signal handler context, which
# lifts restrictions and also serializes the calls in a thread-safe
# manner.
#
# it's very similar to a goroutine and channel design.
#
# another nice benefit of this method is that we can timeout the
# IO.select call, allowing the profile to automatically stop after
# a given interval (by default 30 seconds), avoiding unbounded memory
# growth from a profile that was started and never stopped.
t = Thread.new do
timeout_s = ENV['STACKPROF_TIMEOUT_S']&.to_i || 30
current_timeout_s = nil
loop do
got_value = IO.select([read], nil, nil, current_timeout_s)
read.getbyte if got_value
if StackProf.running?
stackprof_file_prefix = ENV['STACKPROF_FILE_PREFIX'] || Dir.tmpdir
stackprof_out_file = "#{stackprof_file_prefix}/stackprof.#{Process.pid}.#{SecureRandom.hex(6)}.profile"
Gitlab::AppJsonLogger.info(
event: "stackprof",
message: "stopping profile",
output_filename: stackprof_out_file,
pid: Process.pid,
timeout_s: timeout_s,
timed_out: got_value.nil?
)
StackProf.stop
StackProf.results(stackprof_out_file)
current_timeout_s = nil
else
Gitlab::AppJsonLogger.info(
event: "stackprof",
message: "starting profile",
pid: Process.pid
)
StackProf.start(
mode: :cpu,
raw: Gitlab::Utils.to_boolean(ENV['STACKPROF_RAW'] || 'true'),
interval: ENV['STACKPROF_INTERVAL_US']&.to_i || 10_000
)
current_timeout_s = timeout_s
if Gitlab::Runtime.sidekiq?
Sidekiq.configure_server do |config|
config.on :startup do
on_worker_start
end
end
else
Gitlab::Cluster::LifecycleEvents.on_worker_start do
on_worker_start
end
end
end
t.abort_on_exception = true
# in the case of puma, this will override the existing SIGUSR2 signal handler
# that can be used to trigger a restart.
#
# puma cluster has two types of restarts:
# * SIGUSR1: phased restart
# * SIGUSR2: restart
#
# phased restart is not supported in our configuration, because we use
# preload_app. this means we will always perform a normal restart.
# additionally, phased restart is not supported when sending a SIGUSR2
# directly to a puma worker (as opposed to the master process).
#
# the result is that the behaviour of SIGUSR1 and SIGUSR2 is identical in
# our configuration, and we can always use a SIGUSR1 to perform a restart.
#
# thus, it is acceptable for us to re-appropriate the SIGUSR2 signal, and
# override the puma behaviour.
#
# see also:
# * https://github.com/puma/puma/blob/master/docs/signals.md#puma-signals
# * https://github.com/phusion/unicorn/blob/master/SIGNALS
# * https://github.com/mperham/sidekiq/wiki/Signals
Signal.trap('SIGUSR2') do
write.write('.')
def self.on_worker_start
Gitlab::AppJsonLogger.info(
event: "stackprof",
message: "listening on SIGUSR2 signal",
pid: Process.pid
)
# create a pipe in order to propagate signal out of the signal handler
# see also: https://cr.yp.to/docs/selfpipe.html
read, write = IO.pipe
# create a separate thread that polls for signals on the pipe.
#
# this way we do not execute in signal handler context, which
# lifts restrictions and also serializes the calls in a thread-safe
# manner.
#
# it's very similar to a goroutine and channel design.
#
# another nice benefit of this method is that we can timeout the
# IO.select call, allowing the profile to automatically stop after
# a given interval (by default 30 seconds), avoiding unbounded memory
# growth from a profile that was started and never stopped.
t = Thread.new do
timeout_s = ENV['STACKPROF_TIMEOUT_S']&.to_i || 30
current_timeout_s = nil
loop do
got_value = IO.select([read], nil, nil, current_timeout_s)
read.getbyte if got_value
if ::StackProf.running?
stackprof_file_prefix = ENV['STACKPROF_FILE_PREFIX'] || Dir.tmpdir
stackprof_out_file = "#{stackprof_file_prefix}/stackprof.#{Process.pid}.#{SecureRandom.hex(6)}.profile"
Gitlab::AppJsonLogger.info(
event: "stackprof",
message: "stopping profile",
output_filename: stackprof_out_file,
pid: Process.pid,
timeout_s: timeout_s,
timed_out: got_value.nil?
)
::StackProf.stop
::StackProf.results(stackprof_out_file)
current_timeout_s = nil
else
Gitlab::AppJsonLogger.info(
event: "stackprof",
message: "starting profile",
pid: Process.pid
)
::StackProf.start(
mode: :cpu,
raw: Gitlab::Utils.to_boolean(ENV['STACKPROF_RAW'] || 'true'),
interval: ENV['STACKPROF_INTERVAL_US']&.to_i || 10_000
)
current_timeout_s = timeout_s
end
end
end
t.abort_on_exception = true
# in the case of puma, this will override the existing SIGUSR2 signal handler
# that can be used to trigger a restart.
#
# puma cluster has two types of restarts:
# * SIGUSR1: phased restart
# * SIGUSR2: restart
#
# phased restart is not supported in our configuration, because we use
# preload_app. this means we will always perform a normal restart.
# additionally, phased restart is not supported when sending a SIGUSR2
# directly to a puma worker (as opposed to the master process).
#
# the result is that the behaviour of SIGUSR1 and SIGUSR2 is identical in
# our configuration, and we can always use a SIGUSR1 to perform a restart.
#
# thus, it is acceptable for us to re-appropriate the SIGUSR2 signal, and
# override the puma behaviour.
#
# see also:
# * https://github.com/puma/puma/blob/master/docs/signals.md#puma-signals
# * https://github.com/phusion/unicorn/blob/master/SIGNALS
# * https://github.com/mperham/sidekiq/wiki/Signals
Signal.trap('SIGUSR2') do
write.write('.')
end
end
end
end
if Gitlab::Utils.to_boolean(ENV['STACKPROF_ENABLED'].to_s)
Gitlab::StackProf.install
end

View File

@ -310,6 +310,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
collection do
get :setup
get :test
get :graphs
end
end

View File

@ -314,7 +314,8 @@ Example Responses:
"current_sign_in_ip": "196.165.1.102",
"last_sign_in_ip": "172.127.2.22",
"plan": "gold",
"trial": true
"trial": true,
"sign_in_count": 1337
}
```

View File

@ -56,6 +56,8 @@ Kubernetes-specific environment variables are detailed in the
| `CI_ENVIRONMENT_SLUG` | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. Only present if [`environment:name`](../yaml/README.md#environmentname) is set. |
| `CI_ENVIRONMENT_URL` | 9.3 | all | The URL of the environment for this job. Only present if [`environment:url`](../yaml/README.md#environmenturl) is set. |
| `CI_EXTERNAL_PULL_REQUEST_IID` | 12.3 | all | Pull Request ID from GitHub if the [pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
| `CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY` | 13.3 | all | The source repository name of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
| `CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY` | 13.3 | all | The target repository name of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
| `CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME` | 12.3 | all | The source branch name of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
| `CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA` | 12.3 | all | The HEAD SHA of the source branch of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
| `CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME` | 12.3 | all | The target branch name of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |

View File

@ -75,7 +75,7 @@ their execution by clicking **Execute query** button on the top left:
## Apollo Client
To save duplicated clients getting created in different apps, we have a
[default client](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/javascripts/lib/graphql.js) that should be used. This setups the
[default client](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/javascripts/lib/graphql.js) that should be used. This sets up the
Apollo client with the correct URL and also sets the CSRF headers.
Default client accepts two parameters: `resolvers` and `config`.

View File

@ -281,6 +281,10 @@ This can be done via `pkill -USR2 puma:`. The `:` disambiguates between `puma
4.3.3.gitlab.2 ...` (the master process) from `puma: cluster worker 0: ...` (the
worker processes), selecting the latter.
For Sidekiq, the signal can be sent to the `sidekiq-cluster` process via `pkill
-USR2 bin/sidekiq-cluster`, which will forward the signal to all Sidekiq
children. Alternatively, you can also select a specific pid of interest.
Production profiles can be especially noisy. It can be helpful to visualize them
as a [flamegraph](https://github.com/brendangregg/FlameGraph). This can be done
via:

View File

@ -82,15 +82,15 @@ Alerts can be used to trigger actions, like opening an issue automatically
1. Click **Save changes**.
After enabling, GitLab automatically opens an issue when an alert is triggered containing
values extracted from [alert's payload](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config):
values extracted from the [`alerts` field in webhook payload](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config):
- Issue author: `GitLab Alert Bot`
- Issue title: Extract from `annotations/title`, `annotations/summary` or `labels/alertname`
- Alert `Summary`: A list of properties
- `starts_at`: Alert start time via `startsAt`
- `full_query`: Alert query extracted from `generatorURL`
- Issue title: Extracted from the alert payload fields `annotations/title`, `annotations/summary`, or `labels/alertname`.
- Alert `Summary`: A list of properties from the alert's payload.
- `starts_at`: Alert start time from the payload's `startsAt` field
- `full_query`: Alert query extracted from the payload's `generatorURL` field
- Optional list of attached annotations extracted from `annotations/*`
- Alert [GFM](../../user/markdown.md): GitLab Flavored Markdown from `annotations/gitlab_incident_markdown`
- Alert [GFM](../../user/markdown.md): GitLab Flavored Markdown from the payload's `annotations/gitlab_incident_markdown` field.
When GitLab receives a **Recovery Alert**, it closes the associated issue.
This action is recorded as a system message on the issue indicating that it

View File

@ -6,6 +6,7 @@ module API
expose :highest_role
expose :current_sign_in_ip
expose :last_sign_in_ip
expose :sign_in_count
end
end
end

View File

@ -51,9 +51,11 @@ module API
optional :variable_type, type: String, values: ::Ci::GroupVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
end
post ':id/variables' do
variable_params = declared_params(include_missing: false)
variable = user_group.variables.create(variable_params)
variable = ::Ci::ChangeVariableService.new(
container: user_group,
current_user: current_user,
params: { action: :create, variable_params: declared_params(include_missing: false) }
).execute
if variable.valid?
present variable, with: Entities::Variable
@ -74,17 +76,19 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
put ':id/variables/:key' do
variable = user_group.variables.find_by(key: params[:key])
variable = ::Ci::ChangeVariableService.new(
container: user_group,
current_user: current_user,
params: { action: :update, variable_params: declared_params(include_missing: false) }
).execute
break not_found!('GroupVariable') unless variable
variable_params = declared_params(include_missing: false).except(:key)
if variable.update(variable_params)
if variable.valid?
present variable, with: Entities::Variable
else
render_validation_error!(variable)
end
rescue ::ActiveRecord::RecordNotFound
not_found!('GroupVariable')
end
# rubocop: enable CodeReuse/ActiveRecord
@ -96,10 +100,17 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
delete ':id/variables/:key' do
variable = user_group.variables.find_by(key: params[:key])
not_found!('GroupVariable') unless variable
variable = user_group.variables.find_by!(key: params[:key])
destroy_conditionally!(variable)
destroy_conditionally!(variable) do |target_variable|
::Ci::ChangeVariableService.new(
container: user_group,
current_user: current_user,
params: { action: :destroy, variable_params: declared_params(include_missing: false) }
).execute
end
rescue ::ActiveRecord::RecordNotFound
not_found!('GroupVariable')
end
# rubocop: enable CodeReuse/ActiveRecord
end

View File

@ -16406,6 +16406,9 @@ msgstr ""
msgid "Number of employees"
msgstr ""
msgid "Number of events"
msgstr ""
msgid "Number of events for this project: %{total_count}."
msgstr ""
@ -17941,6 +17944,9 @@ msgstr ""
msgid "Product Analytics"
msgstr ""
msgid "ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already."
msgstr ""
msgid "Productivity"
msgstr ""
@ -22018,6 +22024,9 @@ msgstr ""
msgid "Showing all issues"
msgstr ""
msgid "Showing graphs based on events of the last %{timerange} days."
msgstr ""
msgid "Showing last %{size} of log -"
msgstr ""

View File

@ -5,7 +5,7 @@ module QA
module KubernetesCluster
class ProjectCluster < Base
attr_writer :cluster,
:install_helm_tiller, :install_ingress, :install_prometheus, :install_runner, :domain
:install_ingress, :install_prometheus, :install_runner, :domain
attribute :project do
Resource::Project.fabricate!
@ -36,33 +36,27 @@ module QA
cluster_page.add_cluster!
end
if @install_helm_tiller
Page::Project::Operations::Kubernetes::Show.perform do |show|
# We must wait a few seconds for permissions to be set up correctly for new cluster
sleep 10
Page::Project::Operations::Kubernetes::Show.perform do |show|
# We must wait a few seconds for permissions to be set up correctly for new cluster
sleep 25
# Open applications tab
show.open_applications
# Open applications tab
show.open_applications
# Helm must be installed before everything else
show.install!(:helm)
show.await_installed(:helm)
show.install!(:ingress) if @install_ingress
show.install!(:prometheus) if @install_prometheus
show.install!(:runner) if @install_runner
show.install!(:ingress) if @install_ingress
show.install!(:prometheus) if @install_prometheus
show.install!(:runner) if @install_runner
show.await_installed(:ingress) if @install_ingress
show.await_installed(:prometheus) if @install_prometheus
show.await_installed(:runner) if @install_runner
show.await_installed(:ingress) if @install_ingress
show.await_installed(:prometheus) if @install_prometheus
show.await_installed(:runner) if @install_runner
if @install_ingress
populate(:ingress_ip)
if @install_ingress
populate(:ingress_ip)
show.open_details
show.set_domain("#{ingress_ip}.nip.io")
show.save_domain
end
show.open_details
show.set_domain("#{ingress_ip}.nip.io")
show.save_domain
end
end
end

View File

@ -38,7 +38,7 @@ module QA
--no-deploy traefik
CMD
command.gsub!("--network #{network} ", '') unless QA::Runtime::Env.running_in_ci?
command.gsub!("--network #{network} --hostname #{host_name}", '') unless QA::Runtime::Env.running_in_ci?
shell command
end

View File

@ -38,7 +38,6 @@ module QA
Resource::KubernetesCluster::ProjectCluster.fabricate! do |k8s_cluster|
k8s_cluster.project = project
k8s_cluster.cluster = cluster
k8s_cluster.install_helm_tiller = true
k8s_cluster.install_ingress = true
k8s_cluster.install_prometheus = true
k8s_cluster.install_runner = true

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Monitor', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/230927', type: :stale } do
RSpec.describe 'Monitor' do
describe 'with Prometheus in a Gitlab-managed cluster', :orchestrated, :kubernetes, :requires_admin do
before :all do
@cluster = Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create!
@ -98,7 +98,6 @@ module QA
Resource::KubernetesCluster::ProjectCluster.fabricate! do |cluster_settings|
cluster_settings.project = @project
cluster_settings.cluster = @cluster
cluster_settings.install_helm_tiller = true
cluster_settings.install_runner = true
cluster_settings.install_ingress = true
cluster_settings.install_prometheus = true

View File

@ -66,6 +66,27 @@ RSpec.describe Projects::ProductAnalyticsController do
end
end
describe 'GET #graphs' do
it 'renders graphs with 200 status code' do
get :graphs, params: project_params
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:graphs)
end
context 'feature flag disabled' do
before do
stub_feature_flags(product_analytics: false)
end
it 'returns not found' do
get :graphs, params: project_params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
private
def project_params(opts = {})

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Product Analytics > Graphs' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
before do
project.add_maintainer(user)
sign_in(user)
end
it 'shows graphs', :js do
create(:product_analytics_event, project: project)
visit(graphs_project_product_analytics_path(project))
expect(page).to have_content('Showing graphs based on events')
expect(page).to have_content('platform')
expect(page).to have_content('os_timezone')
expect(page).to have_content('br_lang')
expect(page).to have_content('doc_charset')
end
end

View File

@ -0,0 +1,39 @@
import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import ActivityChart from '~/analytics/product_analytics/components/activity_chart.vue';
describe('Activity Chart Bundle', () => {
let wrapper;
function mountComponent({ provide }) {
wrapper = shallowMount(ActivityChart, {
provide: {
formattedData: {},
...provide,
},
});
}
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findChart = () => wrapper.find(GlColumnChart);
const findNoData = () => wrapper.find('[data-testid="noActivityChartData"]');
describe('Activity Chart', () => {
it('renders an warning message with no data', () => {
mountComponent({ provide: { formattedData: {} } });
expect(findNoData().exists()).toBe(true);
});
it('renders a chart with data', () => {
mountComponent({
provide: { formattedData: { keys: ['key1', 'key2'], values: [5038, 2241] } },
});
expect(findNoData().exists()).toBe(false);
expect(findChart().exists()).toBe(true);
});
});
});

View File

@ -41,6 +41,7 @@ describe('diffs/components/app', () => {
store = createDiffsStore();
store.state.diffs.isLoading = false;
store.state.diffs.isTreeLoaded = true;
extendStore(store);

View File

@ -17,6 +17,7 @@ describe('Diffs tree list component', () => {
});
// Setup initial state
store.state.diffs.isTreeLoaded = true;
store.state.diffs.diffFiles.push('test');
store.state.diffs = {
addedLines: 10,

View File

@ -830,6 +830,7 @@ describe('DiffsStoreMutations', () => {
const state = {
treeEntries: {},
tree: [],
isTreeLoaded: false,
};
mutations[types.SET_TREE_DATA](state, {
@ -844,6 +845,7 @@ describe('DiffsStoreMutations', () => {
});
expect(state.tree).toEqual(['tree']);
expect(state.isTreeLoaded).toEqual(true);
});
});

View File

@ -813,6 +813,8 @@ RSpec.describe Ci::Pipeline, :mailer do
expect(subject.to_hash)
.to include(
'CI_EXTERNAL_PULL_REQUEST_IID' => pull_request.pull_request_iid.to_s,
'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY' => pull_request.source_repository,
'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY' => pull_request.target_repository,
'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA' => pull_request.source_sha,
'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA' => pull_request.target_sha,
'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME' => pull_request.source_branch,
@ -3264,32 +3266,6 @@ RSpec.describe Ci::Pipeline, :mailer do
end
end
end
context 'when transitioning to success' do
context 'when feature is enabled' do
before do
stub_feature_flags(keep_latest_artifacts_for_ref: true)
end
it 'calls the PipelineSuccessUnlockArtifactsWorker' do
expect(Ci::PipelineSuccessUnlockArtifactsWorker).to receive(:perform_async).with(pipeline.id)
pipeline.succeed!
end
end
context 'when feature is disabled' do
before do
stub_feature_flags(keep_latest_artifacts_for_ref: false)
end
it 'does not call the PipelineSuccessUnlockArtifactsWorker' do
expect(Ci::PipelineSuccessUnlockArtifactsWorker).not_to receive(:perform_async)
pipeline.succeed!
end
end
end
end
describe '#default_branch?' do

View File

@ -3,8 +3,69 @@
require 'spec_helper'
RSpec.describe Ci::Ref do
using RSpec::Parameterized::TableSyntax
it { is_expected.to belong_to(:project) }
describe 'state machine transitions' do
context 'unlock artifacts transition' do
let(:ci_ref) { create(:ci_ref) }
let(:unlock_artifacts_worker_spy) { class_spy(::Ci::PipelineSuccessUnlockArtifactsWorker) }
before do
stub_const('Ci::PipelineSuccessUnlockArtifactsWorker', unlock_artifacts_worker_spy)
end
context 'when keep latest artifact feature is enabled' do
before do
stub_feature_flags(keep_latest_artifacts_for_ref: true)
end
where(:initial_state, :action, :count) do
:unknown | :succeed! | 1
:unknown | :do_fail! | 0
:success | :succeed! | 1
:success | :do_fail! | 0
:failed | :succeed! | 1
:failed | :do_fail! | 0
:fixed | :succeed! | 1
:fixed | :do_fail! | 0
:broken | :succeed! | 1
:broken | :do_fail! | 0
:still_failing | :succeed | 1
:still_failing | :do_fail | 0
end
with_them do
context "when transitioning states" do
before do
status_value = Ci::Ref.state_machines[:status].states[initial_state].value
ci_ref.update!(status: status_value)
end
it 'calls unlock artifacts service' do
ci_ref.send(action)
expect(unlock_artifacts_worker_spy).to have_received(:perform_async).exactly(count).times
end
end
end
end
context 'when keep latest artifact feature is not enabled' do
before do
stub_feature_flags(keep_latest_artifacts_for_ref: false)
end
it 'does not call unlock artifacts service' do
ci_ref.succeed!
expect(unlock_artifacts_worker_spy).not_to have_received(:perform_async)
end
end
end
end
describe '.ensure_for' do
let_it_be(:project) { create(:project, :repository) }

View File

@ -21,4 +21,18 @@ RSpec.describe ProductAnalyticsEvent, type: :model do
it { expect(described_class.timerange(7.days)).to match_array([event_1, event_2]) }
it { expect(described_class.timerange(30.days)).to match_array([event_1, event_2, event_3]) }
end
describe '.count_by_graph' do
let_it_be(:events) do
[
create(:product_analytics_event, platform: 'web'),
create(:product_analytics_event, platform: 'web'),
create(:product_analytics_event, platform: 'app'),
create(:product_analytics_event, platform: 'mobile', collector_tstamp: Time.zone.now - 10.days)
]
end
it { expect(described_class.count_by_graph('platform', 7.days)).to eq({ 'app' => 1, 'web' => 2 }) }
it { expect(described_class.count_by_graph('platform', 30.days)).to eq({ 'app' => 1, 'mobile' => 1, 'web' => 2 }) }
end
end

View File

@ -0,0 +1,891 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
let(:registration_token) { 'abcdefg123456' }
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
describe '/api/v4/jobs' do
let(:root_namespace) { create(:namespace) }
let(:namespace) { create(:namespace, parent: root_namespace) }
let(:project) { create(:project, namespace: namespace, shared_runners_enabled: false) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:user) { create(:user) }
let(:job) do
create(:ci_build, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
end
describe 'artifacts' do
let(:job) { create(:ci_build, :pending, user: user, project: project, pipeline: pipeline, runner_id: runner.id) }
let(:jwt) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt } }
let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) }
let(:file_upload) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
let(:file_upload2) { fixture_file_upload('spec/fixtures/dk.png', 'image/gif') }
before do
stub_artifacts_object_storage
job.run!
end
shared_examples_for 'rejecting artifacts that are too large' do
let(:filesize) { 100.megabytes.to_i }
let(:sample_max_size) { (filesize / 1.megabyte) - 10 } # Set max size to be smaller than file size to trigger error
shared_examples_for 'failed request' do
it 'responds with payload too large error' do
send_request
expect(response).to have_gitlab_http_status(:payload_too_large)
end
end
context 'based on plan limit setting' do
let(:application_max_size) { sample_max_size + 100 }
let(:limit_name) { "#{Ci::JobArtifact::PLAN_LIMIT_PREFIX}archive" }
before do
create(:plan_limits, :default_plan, limit_name => sample_max_size)
stub_application_setting(max_artifacts_size: application_max_size)
end
it_behaves_like 'failed request'
end
context 'based on application setting' do
before do
stub_application_setting(max_artifacts_size: sample_max_size)
end
it_behaves_like 'failed request'
end
context 'based on root namespace setting' do
let(:application_max_size) { sample_max_size + 10 }
before do
stub_application_setting(max_artifacts_size: application_max_size)
root_namespace.update!(max_artifacts_size: sample_max_size)
end
it_behaves_like 'failed request'
end
context 'based on child namespace setting' do
let(:application_max_size) { sample_max_size + 10 }
let(:root_namespace_max_size) { sample_max_size + 10 }
before do
stub_application_setting(max_artifacts_size: application_max_size)
root_namespace.update!(max_artifacts_size: root_namespace_max_size)
namespace.update!(max_artifacts_size: sample_max_size)
end
it_behaves_like 'failed request'
end
context 'based on project setting' do
let(:application_max_size) { sample_max_size + 10 }
let(:root_namespace_max_size) { sample_max_size + 10 }
let(:child_namespace_max_size) { sample_max_size + 10 }
before do
stub_application_setting(max_artifacts_size: application_max_size)
root_namespace.update!(max_artifacts_size: root_namespace_max_size)
namespace.update!(max_artifacts_size: child_namespace_max_size)
project.update!(max_artifacts_size: sample_max_size)
end
it_behaves_like 'failed request'
end
end
describe 'POST /api/v4/jobs/:id/artifacts/authorize' do
context 'when using token as parameter' do
context 'and the artifact is too large' do
it_behaves_like 'rejecting artifacts that are too large' do
let(:success_code) { :ok }
let(:send_request) { authorize_artifacts_with_token_in_params(filesize: filesize) }
end
end
context 'posting artifacts to running job' do
subject do
authorize_artifacts_with_token_in_params
end
it_behaves_like 'API::CI::Runner application context metadata', '/api/:version/jobs/:id/artifacts/authorize' do
let(:send_request) { subject }
end
it 'updates runner info' do
expect { subject }.to change { runner.reload.contacted_at }
end
shared_examples 'authorizes local file' do
it 'succeeds' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).to eq(JobArtifactUploader.workhorse_local_upload_path)
expect(json_response['RemoteObject']).to be_nil
end
end
context 'when using local storage' do
it_behaves_like 'authorizes local file'
end
context 'when using remote storage' do
context 'when direct upload is enabled' do
before do
stub_artifacts_object_storage(enabled: true, direct_upload: true)
end
it 'succeeds' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response).not_to have_key('TempPath')
expect(json_response['RemoteObject']).to have_key('ID')
expect(json_response['RemoteObject']).to have_key('GetURL')
expect(json_response['RemoteObject']).to have_key('StoreURL')
expect(json_response['RemoteObject']).to have_key('DeleteURL')
expect(json_response['RemoteObject']).to have_key('MultipartUpload')
end
end
context 'when direct upload is disabled' do
before do
stub_artifacts_object_storage(enabled: true, direct_upload: false)
end
it_behaves_like 'authorizes local file'
end
end
end
end
context 'when using token as header' do
it 'authorizes posting artifacts to running job' do
authorize_artifacts_with_token_in_headers
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).not_to be_nil
end
it 'fails to post too large artifact' do
stub_application_setting(max_artifacts_size: 0)
authorize_artifacts_with_token_in_headers(filesize: 100)
expect(response).to have_gitlab_http_status(:payload_too_large)
end
end
context 'when using runners token' do
it 'fails to authorize artifacts posting' do
authorize_artifacts(token: job.project.runners_token)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
it 'reject requests that did not go through gitlab-workhorse' do
headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
authorize_artifacts
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'authorization token is invalid' do
it 'responds with forbidden' do
authorize_artifacts(token: 'invalid', filesize: 100 )
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'authorize uploading of an lsif artifact' do
before do
stub_feature_flags(code_navigation: job.project)
end
it 'adds ProcessLsif header' do
authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['ProcessLsif']).to be_truthy
end
it 'adds ProcessLsifReferences header' do
authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['ProcessLsifReferences']).to be_truthy
end
context 'code_navigation feature flag is disabled' do
it 'responds with a forbidden error' do
stub_feature_flags(code_navigation: false)
authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
aggregate_failures do
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['ProcessLsif']).to be_falsy
expect(json_response['ProcessLsifReferences']).to be_falsy
end
end
end
context 'code_navigation_references feature flag is disabled' do
it 'sets ProcessLsifReferences header to false' do
stub_feature_flags(code_navigation_references: false)
authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['ProcessLsif']).to be_truthy
expect(json_response['ProcessLsifReferences']).to be_falsy
end
end
end
end
def authorize_artifacts(params = {}, request_headers = headers)
post api("/jobs/#{job.id}/artifacts/authorize"), params: params, headers: request_headers
end
def authorize_artifacts_with_token_in_params(params = {}, request_headers = headers)
params = params.merge(token: job.token)
authorize_artifacts(params, request_headers)
end
def authorize_artifacts_with_token_in_headers(params = {}, request_headers = headers_with_token)
authorize_artifacts(params, request_headers)
end
end
describe 'POST /api/v4/jobs/:id/artifacts' do
it_behaves_like 'API::CI::Runner application context metadata', '/api/:version/jobs/:id/artifacts' do
let(:send_request) do
upload_artifacts(file_upload, headers_with_token)
end
end
it 'updates runner info' do
expect { upload_artifacts(file_upload, headers_with_token) }.to change { runner.reload.contacted_at }
end
context 'when the artifact is too large' do
it_behaves_like 'rejecting artifacts that are too large' do
# This filesize validation also happens in non remote stored files,
# it's just that it's hard to stub the filesize in other cases to be
# more than a megabyte.
let!(:fog_connection) do
stub_artifacts_object_storage(direct_upload: true)
end
let(:file_upload) { fog_to_uploaded_file(object) }
let(:success_code) { :created }
let(:object) do
fog_connection.directories.new(key: 'artifacts').files.create( # rubocop:disable Rails/SaveBang
key: 'tmp/uploads/12312300',
body: 'content'
)
end
let(:send_request) do
upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => '12312300')
end
before do
allow(object).to receive(:content_length).and_return(filesize)
end
end
end
context 'when artifacts are being stored inside of tmp path' do
before do
# by configuring this path we allow to pass temp file from any path
allow(JobArtifactUploader).to receive(:workhorse_upload_path).and_return('/')
end
context 'when job has been erased' do
let(:job) { create(:ci_build, erased_at: Time.now) }
before do
upload_artifacts(file_upload, headers_with_token)
end
it 'responds with forbidden' do
upload_artifacts(file_upload, headers_with_token)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when job is running' do
shared_examples 'successful artifacts upload' do
it 'updates successfully' do
expect(response).to have_gitlab_http_status(:created)
end
end
context 'when uses accelerated file post' do
context 'for file stored locally' do
before do
upload_artifacts(file_upload, headers_with_token)
end
it_behaves_like 'successful artifacts upload'
end
context 'for file stored remotely' do
let!(:fog_connection) do
stub_artifacts_object_storage(direct_upload: true)
end
let(:object) do
fog_connection.directories.new(key: 'artifacts').files.create( # rubocop:disable Rails/SaveBang
key: 'tmp/uploads/12312300',
body: 'content'
)
end
let(:file_upload) { fog_to_uploaded_file(object) }
before do
upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => remote_id)
end
context 'when valid remote_id is used' do
let(:remote_id) { '12312300' }
it_behaves_like 'successful artifacts upload'
end
context 'when invalid remote_id is used' do
let(:remote_id) { 'invalid id' }
it 'responds with bad request' do
expect(response).to have_gitlab_http_status(:internal_server_error)
expect(json_response['message']).to eq("Missing file")
end
end
end
end
context 'when using runners token' do
it 'responds with forbidden' do
upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token))
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when artifacts post request does not contain file' do
it 'fails to post artifacts without file' do
post api("/jobs/#{job.id}/artifacts"), params: {}, headers: headers_with_token
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'GitLab Workhorse is not configured' do
it 'fails to post artifacts without GitLab-Workhorse' do
post api("/jobs/#{job.id}/artifacts"), params: { token: job.token }, headers: {}
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'Is missing GitLab Workhorse token headers' do
let(:jwt) { JWT.encode({ 'iss' => 'invalid-header' }, Gitlab::Workhorse.secret, 'HS256') }
it 'fails to post artifacts without GitLab-Workhorse' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).once
upload_artifacts(file_upload, headers_with_token)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when setting an expire date' do
let(:default_artifacts_expire_in) {}
let(:post_data) do
{ file: file_upload,
expire_in: expire_in }
end
before do
stub_application_setting(default_artifacts_expire_in: default_artifacts_expire_in)
upload_artifacts(file_upload, headers_with_token, post_data)
end
context 'when an expire_in is given' do
let(:expire_in) { '7 days' }
it 'updates when specified' do
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now)
end
end
context 'when no expire_in is given' do
let(:expire_in) { nil }
it 'ignores if not specified' do
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.artifacts_expire_at).to be_nil
end
context 'with application default' do
context 'when default is 5 days' do
let(:default_artifacts_expire_in) { '5 days' }
it 'sets to application default' do
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now)
end
end
context 'when default is 0' do
let(:default_artifacts_expire_in) { '0' }
it 'does not set expire_in' do
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.artifacts_expire_at).to be_nil
end
end
end
end
end
context 'posts artifacts file and metadata file' do
let!(:artifacts) { file_upload }
let!(:artifacts_sha256) { Digest::SHA256.file(artifacts.path).hexdigest }
let!(:metadata) { file_upload2 }
let!(:metadata_sha256) { Digest::SHA256.file(metadata.path).hexdigest }
let(:stored_artifacts_file) { job.reload.artifacts_file }
let(:stored_metadata_file) { job.reload.artifacts_metadata }
let(:stored_artifacts_size) { job.reload.artifacts_size }
let(:stored_artifacts_sha256) { job.reload.job_artifacts_archive.file_sha256 }
let(:stored_metadata_sha256) { job.reload.job_artifacts_metadata.file_sha256 }
let(:file_keys) { post_data.keys }
let(:send_rewritten_field) { true }
before do
workhorse_finalize_with_multiple_files(
api("/jobs/#{job.id}/artifacts"),
method: :post,
file_keys: file_keys,
params: post_data,
headers: headers_with_token,
send_rewritten_field: send_rewritten_field
)
end
context 'when posts data accelerated by workhorse is correct' do
let(:post_data) { { file: artifacts, metadata: metadata } }
it 'stores artifacts and artifacts metadata' do
expect(response).to have_gitlab_http_status(:created)
expect(stored_artifacts_file.filename).to eq(artifacts.original_filename)
expect(stored_metadata_file.filename).to eq(metadata.original_filename)
expect(stored_artifacts_size).to eq(artifacts.size)
expect(stored_artifacts_sha256).to eq(artifacts_sha256)
expect(stored_metadata_sha256).to eq(metadata_sha256)
end
end
context 'with a malicious file.path param' do
let(:post_data) { {} }
let(:tmp_file) { Tempfile.new('crafted.file.path') }
let(:url) { "/jobs/#{job.id}/artifacts?file.path=#{tmp_file.path}" }
it 'rejects the request' do
expect(response).to have_gitlab_http_status(:bad_request)
expect(stored_artifacts_size).to be_nil
end
end
context 'when workhorse header is missing' do
let(:post_data) { { file: artifacts, metadata: metadata } }
let(:send_rewritten_field) { false }
it 'rejects the request' do
expect(response).to have_gitlab_http_status(:bad_request)
expect(stored_artifacts_size).to be_nil
end
end
context 'when there is no artifacts file in post data' do
let(:post_data) do
{ metadata: metadata }
end
it 'is expected to respond with bad request' do
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'does not store metadata' do
expect(stored_metadata_file).to be_nil
end
end
end
context 'when artifact_type is archive' do
context 'when artifact_format is zip' do
let(:params) { { artifact_type: :archive, artifact_format: :zip } }
it 'stores junit test report' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.job_artifacts_archive).not_to be_nil
end
end
context 'when artifact_format is gzip' do
let(:params) { { artifact_type: :archive, artifact_format: :gzip } }
it 'returns an error' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:bad_request)
expect(job.reload.job_artifacts_archive).to be_nil
end
end
end
context 'when artifact_type is junit' do
context 'when artifact_format is gzip' do
let(:file_upload) { fixture_file_upload('spec/fixtures/junit/junit.xml.gz') }
let(:params) { { artifact_type: :junit, artifact_format: :gzip } }
it 'stores junit test report' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.job_artifacts_junit).not_to be_nil
end
end
context 'when artifact_format is raw' do
let(:file_upload) { fixture_file_upload('spec/fixtures/junit/junit.xml.gz') }
let(:params) { { artifact_type: :junit, artifact_format: :raw } }
it 'returns an error' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:bad_request)
expect(job.reload.job_artifacts_junit).to be_nil
end
end
end
context 'when artifact_type is metrics_referee' do
context 'when artifact_format is gzip' do
let(:file_upload) { fixture_file_upload('spec/fixtures/referees/metrics_referee.json.gz') }
let(:params) { { artifact_type: :metrics_referee, artifact_format: :gzip } }
it 'stores metrics_referee data' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.job_artifacts_metrics_referee).not_to be_nil
end
end
context 'when artifact_format is raw' do
let(:file_upload) { fixture_file_upload('spec/fixtures/referees/metrics_referee.json.gz') }
let(:params) { { artifact_type: :metrics_referee, artifact_format: :raw } }
it 'returns an error' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:bad_request)
expect(job.reload.job_artifacts_metrics_referee).to be_nil
end
end
end
context 'when artifact_type is network_referee' do
context 'when artifact_format is gzip' do
let(:file_upload) { fixture_file_upload('spec/fixtures/referees/network_referee.json.gz') }
let(:params) { { artifact_type: :network_referee, artifact_format: :gzip } }
it 'stores network_referee data' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.job_artifacts_network_referee).not_to be_nil
end
end
context 'when artifact_format is raw' do
let(:file_upload) { fixture_file_upload('spec/fixtures/referees/network_referee.json.gz') }
let(:params) { { artifact_type: :network_referee, artifact_format: :raw } }
it 'returns an error' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:bad_request)
expect(job.reload.job_artifacts_network_referee).to be_nil
end
end
end
context 'when artifact_type is dotenv' do
context 'when artifact_format is gzip' do
let(:file_upload) { fixture_file_upload('spec/fixtures/build.env.gz') }
let(:params) { { artifact_type: :dotenv, artifact_format: :gzip } }
it 'stores dotenv file' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.job_artifacts_dotenv).not_to be_nil
end
it 'parses dotenv file' do
expect do
upload_artifacts(file_upload, headers_with_token, params)
end.to change { job.job_variables.count }.from(0).to(2)
end
context 'when parse error happens' do
let(:file_upload) { fixture_file_upload('spec/fixtures/ci_build_artifacts_metadata.gz') }
it 'returns an error' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('Invalid Format')
end
end
end
context 'when artifact_format is raw' do
let(:file_upload) { fixture_file_upload('spec/fixtures/build.env.gz') }
let(:params) { { artifact_type: :dotenv, artifact_format: :raw } }
it 'returns an error' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:bad_request)
expect(job.reload.job_artifacts_dotenv).to be_nil
end
end
end
end
context 'when artifacts already exist for the job' do
let(:params) do
{
artifact_type: :archive,
artifact_format: :zip,
'file.sha256' => uploaded_sha256
}
end
let(:existing_sha256) { '0' * 64 }
let!(:existing_artifact) do
create(:ci_job_artifact, :archive, file_sha256: existing_sha256, job: job)
end
context 'when sha256 is the same of the existing artifact' do
let(:uploaded_sha256) { existing_sha256 }
it 'ignores the new artifact' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.job_artifacts_archive).to eq(existing_artifact)
end
end
context 'when sha256 is different than the existing artifact' do
let(:uploaded_sha256) { '1' * 64 }
it 'logs and returns an error' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:bad_request)
expect(job.reload.job_artifacts_archive).to eq(existing_artifact)
end
end
end
context 'when object storage throws errors' do
let(:params) { { artifact_type: :archive, artifact_format: :zip } }
it 'does not store artifacts' do
allow_next_instance_of(JobArtifactUploader) do |uploader|
allow(uploader).to receive(:store!).and_raise(Errno::EIO)
end
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:service_unavailable)
expect(job.reload.job_artifacts_archive).to be_nil
end
end
context 'when artifacts are being stored outside of tmp path' do
let(:new_tmpdir) { Dir.mktmpdir }
before do
# init before overwriting tmp dir
file_upload
# by configuring this path we allow to pass file from @tmpdir only
# but all temporary files are stored in system tmp directory
allow(Dir).to receive(:tmpdir).and_return(new_tmpdir)
end
after do
FileUtils.remove_entry(new_tmpdir)
end
it 'fails to post artifacts for outside of tmp path' do
upload_artifacts(file_upload, headers_with_token)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
def upload_artifacts(file, headers = {}, params = {})
workhorse_finalize(
api("/jobs/#{job.id}/artifacts"),
method: :post,
file_key: :file,
params: params.merge(file: file),
headers: headers,
send_rewritten_field: true
)
end
end
describe 'GET /api/v4/jobs/:id/artifacts' do
let(:token) { job.token }
it_behaves_like 'API::CI::Runner application context metadata', '/api/:version/jobs/:id/artifacts' do
let(:send_request) { download_artifact }
end
it 'updates runner info' do
expect { download_artifact }.to change { runner.reload.contacted_at }
end
context 'when job has artifacts' do
let(:job) { create(:ci_build) }
let(:store) { JobArtifactUploader::Store::LOCAL }
before do
create(:ci_job_artifact, :archive, file_store: store, job: job)
end
context 'when using job token' do
context 'when artifacts are stored locally' do
let(:download_headers) do
{ 'Content-Transfer-Encoding' => 'binary',
'Content-Disposition' => %q(attachment; filename="ci_build_artifacts.zip"; filename*=UTF-8''ci_build_artifacts.zip) }
end
before do
download_artifact
end
it 'download artifacts' do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers.to_h).to include download_headers
end
end
context 'when artifacts are stored remotely' do
let(:store) { JobArtifactUploader::Store::REMOTE }
let!(:job) { create(:ci_build) }
context 'when proxy download is being used' do
before do
download_artifact(direct_download: false)
end
it 'uses workhorse send-url' do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers.to_h).to include(
'Gitlab-Workhorse-Send-Data' => /send-url:/)
end
end
context 'when direct download is being used' do
before do
download_artifact(direct_download: true)
end
it 'receive redirect for downloading artifacts' do
expect(response).to have_gitlab_http_status(:found)
expect(response.headers).to include('Location')
end
end
end
end
context 'when using runnners token' do
let(:token) { job.project.runners_token }
before do
download_artifact
end
it 'responds with forbidden' do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when job does not have artifacts' do
it 'responds with not found' do
download_artifact
expect(response).to have_gitlab_http_status(:not_found)
end
end
def download_artifact(params = {}, request_headers = headers)
params = params.merge(token: token)
job.reload
get api("/jobs/#{job.id}/artifacts"), params: params, headers: request_headers
end
end
end
end
end

View File

@ -0,0 +1,196 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
let(:registration_token) { 'abcdefg123456' }
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
describe '/api/v4/jobs' do
let(:root_namespace) { create(:namespace) }
let(:namespace) { create(:namespace, parent: root_namespace) }
let(:project) { create(:project, namespace: namespace, shared_runners_enabled: false) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:user) { create(:user) }
let(:job) do
create(:ci_build, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
end
describe 'PUT /api/v4/jobs/:id' do
let(:job) do
create(:ci_build, :pending, :trace_live, pipeline: pipeline, project: project, user: user, runner_id: runner.id)
end
before do
job.run!
end
it_behaves_like 'API::CI::Runner application context metadata', '/api/:version/jobs/:id' do
let(:send_request) { update_job(state: 'success') }
end
it 'updates runner info' do
expect { update_job(state: 'success') }.to change { runner.reload.contacted_at }
end
context 'when status is given' do
it 'mark job as succeeded' do
update_job(state: 'success')
job.reload
expect(job).to be_success
end
it 'mark job as failed' do
update_job(state: 'failed')
job.reload
expect(job).to be_failed
expect(job).to be_unknown_failure
end
context 'when failure_reason is script_failure' do
before do
update_job(state: 'failed', failure_reason: 'script_failure')
job.reload
end
it { expect(job).to be_script_failure }
end
context 'when failure_reason is runner_system_failure' do
before do
update_job(state: 'failed', failure_reason: 'runner_system_failure')
job.reload
end
it { expect(job).to be_runner_system_failure }
end
context 'when failure_reason is unrecognized value' do
before do
update_job(state: 'failed', failure_reason: 'what_is_this')
job.reload
end
it { expect(job).to be_unknown_failure }
end
context 'when failure_reason is job_execution_timeout' do
before do
update_job(state: 'failed', failure_reason: 'job_execution_timeout')
job.reload
end
it { expect(job).to be_job_execution_timeout }
end
context 'when failure_reason is unmet_prerequisites' do
before do
update_job(state: 'failed', failure_reason: 'unmet_prerequisites')
job.reload
end
it { expect(job).to be_unmet_prerequisites }
end
end
context 'when trace is given' do
it 'creates a trace artifact' do
allow(BuildFinishedWorker).to receive(:perform_async).with(job.id) do
ArchiveTraceWorker.new.perform(job.id)
end
update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
job.reload
expect(response).to have_gitlab_http_status(:ok)
expect(job.trace.raw).to eq 'BUILD TRACE UPDATED'
expect(job.job_artifacts_trace.open.read).to eq 'BUILD TRACE UPDATED'
end
context 'when concurrent update of trace is happening' do
before do
job.trace.write('wb') do
update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
end
end
it 'returns that operation conflicts' do
expect(response).to have_gitlab_http_status(:conflict)
end
end
end
context 'when no trace is given' do
it 'does not override trace information' do
update_job
expect(job.reload.trace.raw).to eq 'BUILD TRACE'
end
context 'when running state is sent' do
it 'updates update_at value' do
expect { update_job_after_time }.to change { job.reload.updated_at }
end
end
context 'when other state is sent' do
it "doesn't update update_at value" do
expect { update_job_after_time(20.minutes, state: 'success') }.not_to change { job.reload.updated_at }
end
end
end
context 'when job has been erased' do
let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
it 'responds with forbidden' do
update_job
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when job has already been finished' do
before do
job.trace.set('Job failed')
job.drop!(:script_failure)
end
it 'does not update job status and job trace' do
update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
job.reload
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.header['Job-Status']).to eq 'failed'
expect(job.trace.raw).to eq 'Job failed'
expect(job).to be_failed
end
end
def update_job(token = job.token, **params)
new_params = params.merge(token: token)
put api("/jobs/#{job.id}"), params: new_params
end
def update_job_after_time(update_interval = 20.minutes, state = 'running')
Timecop.travel(job.updated_at + update_interval) do
update_job(job.token, state: state)
end
end
end
end
end

View File

@ -0,0 +1,861 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
let(:registration_token) { 'abcdefg123456' }
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
describe '/api/v4/jobs' do
let(:root_namespace) { create(:namespace) }
let(:namespace) { create(:namespace, parent: root_namespace) }
let(:project) { create(:project, namespace: namespace, shared_runners_enabled: false) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:user) { create(:user) }
let(:job) do
create(:ci_build, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
end
describe 'POST /api/v4/jobs/request' do
let!(:last_update) {}
let!(:new_update) { }
let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' }
before do
job
stub_container_registry_config(enabled: false)
end
shared_examples 'no jobs available' do
before do
request_job
end
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_gitlab_http_status(:no_content)
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_gitlab_http_status(:no_content)
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_gitlab_http_status(:no_content)
expect(response.header['X-GitLab-Last-Update']).to eq(new_update)
end
end
context 'when beta version is sent' do
let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' }
it { expect(response).to have_gitlab_http_status(:no_content) }
end
context 'when pre-9-0 version is sent' do
let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' }
it { expect(response).to have_gitlab_http_status(:no_content) }
end
context 'when pre-9-0 beta version is sent' do
let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' }
it { expect(response).to have_gitlab_http_status(:no_content) }
end
end
end
context 'when no token is provided' do
it 'returns 400 error' do
post api('/jobs/request')
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when invalid token is provided' do
it 'returns 403 error' do
post api('/jobs/request'), params: { token: 'invalid' }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when valid token is provided' do
context 'when Runner is not active' do
let(:runner) { create(:ci_runner, :inactive) }
let(:update_value) { runner.ensure_runner_queue_value }
it 'returns 204 error' do
request_job
expect(response).to have_gitlab_http_status(:no_content)
expect(response.header['X-GitLab-Last-Update']).to eq(update_value)
end
end
context 'when jobs are finished' do
before do
job.success
end
it_behaves_like 'no jobs available'
end
context 'when other projects have pending jobs' do
before do
job.success
create(:ci_build, :pending)
end
it_behaves_like 'no jobs available'
end
context 'when shared runner requests job for project without shared_runners_enabled' do
let(:runner) { create(:ci_runner, :instance) }
it_behaves_like 'no jobs available'
end
context 'when there is a pending job' do
let(:expected_job_info) do
{ 'name' => job.name,
'stage' => job.stage,
'project_id' => job.project.id,
'project_name' => job.project.name }
end
let(:expected_git_info) do
{ 'repo_url' => job.repo_url,
'ref' => job.ref,
'sha' => job.sha,
'before_sha' => job.before_sha,
'ref_type' => 'branch',
'refspecs' => ["+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
"+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}"],
'depth' => project.ci_default_git_depth }
end
let(:expected_steps) do
[{ 'name' => 'script',
'script' => %w(echo),
'timeout' => job.metadata_timeout,
'when' => 'on_success',
'allow_failure' => false },
{ 'name' => 'after_script',
'script' => %w(ls date),
'timeout' => job.metadata_timeout,
'when' => 'always',
'allow_failure' => true }]
end
let(:expected_variables) do
[{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true, 'masked' => false },
{ 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true, 'masked' => false },
{ 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true, 'masked' => false }]
end
let(:expected_artifacts) do
[{ 'name' => 'artifacts_file',
'untracked' => false,
'paths' => %w(out/),
'when' => 'always',
'expire_in' => '7d',
"artifact_type" => "archive",
"artifact_format" => "zip" }]
end
let(:expected_cache) do
[{ 'key' => 'cache_key',
'untracked' => false,
'paths' => ['vendor/*'],
'policy' => 'pull-push' }]
end
let(:expected_features) { { 'trace_sections' => true } }
it 'picks a job' do
request_job info: { platform: :darwin }
expect(response).to have_gitlab_http_status(:created)
expect(response.headers['Content-Type']).to eq('application/json')
expect(response.headers).not_to have_key('X-GitLab-Last-Update')
expect(runner.reload.platform).to eq('darwin')
expect(json_response['id']).to eq(job.id)
expect(json_response['token']).to eq(job.token)
expect(json_response['job_info']).to eq(expected_job_info)
expect(json_response['git_info']).to eq(expected_git_info)
expect(json_response['image']).to eq({ 'name' => 'ruby:2.7', 'entrypoint' => '/bin/sh', 'ports' => [] })
expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
'alias' => nil, 'command' => nil, 'ports' => [] },
{ 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [] }])
expect(json_response['steps']).to eq(expected_steps)
expect(json_response['artifacts']).to eq(expected_artifacts)
expect(json_response['cache']).to eq(expected_cache)
expect(json_response['variables']).to include(*expected_variables)
expect(json_response['features']).to eq(expected_features)
end
it 'creates persistent ref' do
expect_any_instance_of(::Ci::PersistentRef).to receive(:create_ref)
.with(job.sha, "refs/#{Repository::REF_PIPELINES}/#{job.commit_id}")
request_job info: { platform: :darwin }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['id']).to eq(job.id)
end
context 'when job is made for tag' do
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
it 'sets branch as ref_type' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['git_info']['ref_type']).to eq('tag')
end
context 'when GIT_DEPTH is specified' do
before do
create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline)
end
it 'specifies refspecs' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['git_info']['refspecs']).to include("+refs/tags/#{job.ref}:refs/tags/#{job.ref}")
end
end
context 'when a Gitaly exception is thrown during response' do
before do
allow_next_instance_of(Ci::BuildRunnerPresenter) do |instance|
allow(instance).to receive(:artifacts).and_raise(GRPC::DeadlineExceeded)
end
end
it 'fails the job as a scheduler failure' do
request_job
expect(response).to have_gitlab_http_status(:no_content)
expect(job.reload.failed?).to be_truthy
expect(job.failure_reason).to eq('scheduler_failure')
expect(job.runner_id).to eq(runner.id)
expect(job.runner_session).to be_nil
end
end
context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do
before do
project.update!(ci_default_git_depth: nil)
end
it 'specifies refspecs' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['git_info']['refspecs'])
.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
'+refs/tags/*:refs/tags/*',
'+refs/heads/*:refs/remotes/origin/*')
end
end
end
context 'when job filtered by job_age' do
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) }
context 'job is queued less than job_age parameter' do
let(:job_age) { 120 }
it 'gives 204' do
request_job(job_age: job_age)
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'job is queued more than job_age parameter' do
let(:job_age) { 30 }
it 'picks a job' do
request_job(job_age: job_age)
expect(response).to have_gitlab_http_status(:created)
end
end
end
context 'when job is made for branch' do
it 'sets tag as ref_type' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['git_info']['ref_type']).to eq('branch')
end
context 'when GIT_DEPTH is specified' do
before do
create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline)
end
it 'specifies refspecs' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['git_info']['refspecs']).to include("+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}")
end
end
context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do
before do
project.update!(ci_default_git_depth: nil)
end
it 'specifies refspecs' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['git_info']['refspecs'])
.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
'+refs/tags/*:refs/tags/*',
'+refs/heads/*:refs/remotes/origin/*')
end
end
end
context 'when job is for a release' do
let!(:job) { create(:ci_build, :release_options, pipeline: pipeline) }
context 'when `multi_build_steps` is passed by the runner' do
it 'exposes release info' do
request_job info: { features: { multi_build_steps: true } }
expect(response).to have_gitlab_http_status(:created)
expect(response.headers).not_to have_key('X-GitLab-Last-Update')
expect(json_response['steps']).to eq([
{
"name" => "script",
"script" => ["make changelog | tee release_changelog.txt"],
"timeout" => 3600,
"when" => "on_success",
"allow_failure" => false
},
{
"name" => "release",
"script" =>
["release-cli create --name \"Release $CI_COMMIT_SHA\" --description \"Created using the release-cli $EXTRA_DESCRIPTION\" --tag-name \"release-$CI_COMMIT_SHA\" --ref \"$CI_COMMIT_SHA\""],
"timeout" => 3600,
"when" => "on_success",
"allow_failure" => false
}
])
end
end
context 'when `multi_build_steps` is not passed by the runner' do
it 'drops the job' do
request_job
expect(response).to have_gitlab_http_status(:no_content)
end
end
end
context 'when job is made for merge request' do
let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) }
let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) }
let(:merge_request) { create(:merge_request) }
it 'sets branch as ref_type' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['git_info']['ref_type']).to eq('branch')
end
context 'when GIT_DEPTH is specified' do
before do
create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline)
end
it 'returns the overwritten git depth for merge request refspecs' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['git_info']['depth']).to eq(1)
end
end
end
it 'updates runner info' do
expect { request_job }.to change { runner.reload.contacted_at }
end
%w(version revision platform architecture).each do |param|
context "when info parameter '#{param}' is present" do
let(:value) { "#{param}_value" }
it "updates provided Runner's parameter" do
request_job info: { param => value }
expect(response).to have_gitlab_http_status(:created)
expect(runner.reload.read_attribute(param.to_sym)).to eq(value)
end
end
end
it "sets the runner's ip_address" do
post api('/jobs/request'),
params: { token: runner.token },
headers: { 'User-Agent' => user_agent, 'X-Forwarded-For' => '123.222.123.222' }
expect(response).to have_gitlab_http_status(:created)
expect(runner.reload.ip_address).to eq('123.222.123.222')
end
it "handles multiple X-Forwarded-For addresses" do
post api('/jobs/request'),
params: { token: runner.token },
headers: { 'User-Agent' => user_agent, 'X-Forwarded-For' => '123.222.123.222, 127.0.0.1' }
expect(response).to have_gitlab_http_status(:created)
expect(runner.reload.ip_address).to eq('123.222.123.222')
end
context 'when concurrently updating a job' 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
request_job
expect(response).to have_gitlab_http_status(:conflict)
expect(response.headers).not_to have_key('X-GitLab-Last-Update')
end
end
context 'when project and pipeline have multiple jobs' do
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do
job.success
job2.success
end
it 'returns dependent jobs' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['id']).to eq(test_job.id)
expect(json_response['dependencies'].count).to eq(2)
expect(json_response['dependencies']).to include(
{ 'id' => job.id, 'name' => job.name, 'token' => job.token },
{ 'id' => job2.id, 'name' => job2.name, 'token' => job2.token })
end
end
context 'when pipeline have jobs with artifacts' do
let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do
job.success
end
it 'returns dependent jobs' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['id']).to eq(test_job.id)
expect(json_response['dependencies'].count).to eq(1)
expect(json_response['dependencies']).to include(
{ 'id' => job.id, 'name' => job.name, 'token' => job.token,
'artifacts_file' => { 'filename' => 'ci_build_artifacts.zip', 'size' => 107464 } })
end
end
context 'when explicit dependencies are defined' do
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
stage: 'deploy', stage_idx: 1,
options: { script: ['bash'], dependencies: [job2.name] })
end
before do
job.success
job2.success
end
it 'returns dependent jobs' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['id']).to eq(test_job.id)
expect(json_response['dependencies'].count).to eq(1)
expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token)
end
end
context 'when dependencies is an empty array' do
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:empty_dependencies_job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
stage: 'deploy', stage_idx: 1,
options: { script: ['bash'], dependencies: [] })
end
before do
job.success
job2.success
end
it 'returns an empty array' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['id']).to eq(empty_dependencies_job.id)
expect(json_response['dependencies'].count).to eq(0)
end
end
context 'when job has no tags' do
before do
job.update!(tags: [])
end
context 'when runner is allowed to pick untagged jobs' do
before do
runner.update_column(:run_untagged, true)
end
it 'picks job' do
request_job
expect(response).to have_gitlab_http_status(:created)
end
end
context 'when runner is not allowed to pick untagged jobs' do
before do
runner.update_column(:run_untagged, false)
end
it_behaves_like 'no jobs available'
end
end
context 'when triggered job is available' do
let(:expected_variables) do
[{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true, 'masked' => false },
{ 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true, 'masked' => false },
{ 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true, 'masked' => false },
{ 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true, 'masked' => false },
{ 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false, 'masked' => false },
{ 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false, 'masked' => false }]
end
let(:trigger) { create(:ci_trigger, project: project) }
let!(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, builds: [job], trigger: trigger) }
before do
project.variables << ::Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
end
shared_examples 'expected variables behavior' do
it 'returns variables for triggers' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['variables']).to include(*expected_variables)
end
end
context 'when variables are stored in trigger_request' do
before do
trigger_request.update_attribute(:variables, { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } )
end
it_behaves_like 'expected variables behavior'
end
context 'when variables are stored in pipeline_variables' do
before do
create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1')
end
it_behaves_like 'expected variables behavior'
end
end
describe 'registry credentials support' do
let(:registry_url) { 'registry.example.com:5005' }
let(:registry_credentials) do
{ 'type' => 'registry',
'url' => registry_url,
'username' => 'gitlab-ci-token',
'password' => job.token }
end
context 'when registry is enabled' do
before do
stub_container_registry_config(enabled: true, host_port: registry_url)
end
it 'sends registry credentials key' do
request_job
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_url)
end
it 'does not send registry credentials' do
request_job
expect(json_response).to have_key('credentials')
expect(json_response['credentials']).not_to include(registry_credentials)
end
end
end
describe 'timeout support' do
context 'when project specifies job timeout' do
let(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) }
it 'contains info about timeout taken from project' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['runner_info']).to include({ 'timeout' => 1234 })
end
context 'when runner specifies lower timeout' do
let(:runner) { create(:ci_runner, :project, maximum_timeout: 1000, projects: [project]) }
it 'contains info about timeout overridden by runner' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['runner_info']).to include({ 'timeout' => 1000 })
end
end
context 'when runner specifies bigger timeout' do
let(:runner) { create(:ci_runner, :project, maximum_timeout: 2000, projects: [project]) }
it 'contains info about timeout not overridden by runner' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response['runner_info']).to include({ 'timeout' => 1234 })
end
end
end
end
end
describe 'port support' do
let(:job) { create(:ci_build, pipeline: pipeline, options: options) }
context 'when job image has ports' do
let(:options) do
{
image: {
name: 'ruby',
ports: [80]
},
services: ['mysql']
}
end
it 'returns the image ports' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to include(
'id' => job.id,
'image' => a_hash_including('name' => 'ruby', 'ports' => [{ 'number' => 80, 'protocol' => 'http', 'name' => 'default_port' }]),
'services' => all(a_hash_including('name' => 'mysql')))
end
end
context 'when job services settings has ports' do
let(:options) do
{
image: 'ruby',
services: [
{
name: 'tomcat',
ports: [{ number: 8081, protocol: 'http', name: 'custom_port' }]
}
]
}
end
it 'returns the service ports' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to include(
'id' => job.id,
'image' => a_hash_including('name' => 'ruby'),
'services' => all(a_hash_including('name' => 'tomcat', 'ports' => [{ 'number' => 8081, 'protocol' => 'http', 'name' => 'custom_port' }])))
end
end
end
describe 'a job with excluded artifacts' do
context 'when excluded paths are defined' do
let(:job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'test',
stage: 'deploy', stage_idx: 1,
options: { artifacts: { paths: ['abc'], exclude: ['cde'] } })
end
context 'when a runner supports this feature' do
it 'exposes excluded paths when the feature is enabled' do
stub_feature_flags(ci_artifacts_exclude: true)
request_job info: { features: { artifacts_exclude: true } }
expect(response).to have_gitlab_http_status(:created)
expect(json_response.dig('artifacts').first).to include('exclude' => ['cde'])
end
it 'does not expose excluded paths when the feature is disabled' do
stub_feature_flags(ci_artifacts_exclude: false)
request_job info: { features: { artifacts_exclude: true } }
expect(response).to have_gitlab_http_status(:created)
expect(json_response.dig('artifacts').first).not_to have_key('exclude')
end
end
context 'when a runner does not support this feature' do
it 'does not expose the build at all' do
stub_feature_flags(ci_artifacts_exclude: true)
request_job
expect(response).to have_gitlab_http_status(:no_content)
end
end
end
it 'does not expose excluded paths when these are empty' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response.dig('artifacts').first).not_to have_key('exclude')
end
end
def request_job(token = runner.token, **params)
new_params = params.merge(token: token, last_update: last_update)
post api('/jobs/request'), params: new_params.to_json, headers: { 'User-Agent' => user_agent, 'Content-Type': 'application/json' }
end
end
context 'for web-ide job' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:service) { ::Ci::CreateWebIdeTerminalService.new(project, user, ref: 'master').execute }
let(:pipeline) { service[:pipeline] }
let(:build) { pipeline.builds.first }
let(:job) { {} }
let(:config_content) do
'terminal: { image: ruby, services: [mysql], before_script: [ls], tags: [tag-1], variables: { KEY: value } }'
end
before do
stub_webide_config_file(config_content)
project.add_maintainer(user)
pipeline
end
context 'when runner has matching tag' do
before do
runner.update!(tag_list: ['tag-1'])
end
it 'successfully picks job' do
request_job
build.reload
expect(build).to be_running
expect(build.runner).to eq(runner)
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to include(
"id" => build.id,
"variables" => include("key" => 'KEY', "value" => 'value', "public" => true, "masked" => false),
"image" => a_hash_including("name" => 'ruby'),
"services" => all(a_hash_including("name" => 'mysql')),
"job_info" => a_hash_including("name" => 'terminal', "stage" => 'terminal'))
end
end
context 'when runner does not have matching tags' do
it 'does not pick a job' do
request_job
build.reload
expect(build).to be_pending
expect(response).to have_gitlab_http_status(:no_content)
end
end
def request_job(token = runner.token, **params)
post api('/jobs/request'), params: params.merge(token: token)
end
end
end
end
end

View File

@ -0,0 +1,292 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
let(:registration_token) { 'abcdefg123456' }
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
describe '/api/v4/jobs' do
let(:root_namespace) { create(:namespace) }
let(:namespace) { create(:namespace, parent: root_namespace) }
let(:project) { create(:project, namespace: namespace, shared_runners_enabled: false) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:user) { create(:user) }
let(:job) do
create(:ci_build, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
end
describe 'PATCH /api/v4/jobs/:id/trace' do
let(:job) do
create(:ci_build, :running, :trace_live,
project: project, user: user, runner_id: runner.id, pipeline: pipeline)
end
let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } }
let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
let(:update_interval) { 10.seconds.to_i }
before do
initial_patch_the_trace
end
it_behaves_like 'API::CI::Runner application context metadata', '/api/:version/jobs/:id/trace' do
let(:send_request) { patch_the_trace }
end
it 'updates runner info' do
runner.update!(contacted_at: 1.year.ago)
expect { patch_the_trace }.to change { runner.reload.contacted_at }
end
context 'when request is valid' do
it 'gets correct response' do
expect(response).to have_gitlab_http_status(:accepted)
expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
expect(response.header).to have_key 'Range'
expect(response.header).to have_key 'Job-Status'
expect(response.header).to have_key 'X-GitLab-Trace-Update-Interval'
end
context 'when job has been updated recently' do
it { expect { patch_the_trace }.not_to change { job.updated_at }}
it "changes the job's trace" do
patch_the_trace
expect(job.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 { job.updated_at }}
it "doesn't change the build.trace" do
force_patch_the_trace
expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
end
end
end
context 'when job was not updated recently' do
let(:update_interval) { 15.minutes.to_i }
it { expect { patch_the_trace }.to change { job.updated_at } }
it 'changes the job.trace' do
patch_the_trace
expect(job.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 { job.updated_at } }
it "doesn't change the job.trace" do
force_patch_the_trace
expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
end
end
end
context 'when project for the build has been deleted' do
let(:job) do
create(:ci_build, :running, :trace_live, runner_id: runner.id, pipeline: pipeline) do |job|
job.project.update!(pending_delete: true)
end
end
it 'responds with forbidden' do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when trace is patched' do
before do
patch_the_trace
end
it 'has valid trace' do
expect(response).to have_gitlab_http_status(:accepted)
expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
end
context 'when job is cancelled' do
before do
job.cancel
end
context 'when trace is patched' do
before do
patch_the_trace
end
it 'returns Forbidden ' do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when redis data are flushed' do
before do
redis_shared_state_cleanup!
end
it 'has empty trace' do
expect(job.reload.trace.raw).to eq ''
end
context 'when we perform partial patch' do
before do
patch_the_trace('hello', headers.merge({ 'Content-Range' => "28-32/5" }))
end
it 'returns an error' do
expect(response).to have_gitlab_http_status(:range_not_satisfiable)
expect(response.header['Range']).to eq('0-0')
end
end
context 'when we resend full trace' do
before do
patch_the_trace('BUILD TRACE appended appended hello', headers.merge({ 'Content-Range' => "0-34/35" }))
end
it 'succeeds with updating trace' do
expect(response).to have_gitlab_http_status(:accepted)
expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended hello'
end
end
end
end
context 'when concurrent update of trace is happening' do
before do
job.trace.write('wb') do
patch_the_trace
end
end
it 'returns that operation conflicts' do
expect(response).to have_gitlab_http_status(:conflict)
end
end
context 'when the job is canceled' do
before do
job.cancel
patch_the_trace
end
it 'receives status in header' do
expect(response.header['Job-Status']).to eq 'canceled'
end
end
context 'when build trace is being watched' do
before do
job.trace.being_watched!
end
it 'returns X-GitLab-Trace-Update-Interval as 3' do
patch_the_trace
expect(response).to have_gitlab_http_status(:accepted)
expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('3')
end
end
context 'when build trace is not being watched' do
it 'returns X-GitLab-Trace-Update-Interval as 30' do
patch_the_trace
expect(response).to have_gitlab_http_status(:accepted)
expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('30')
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).to have_gitlab_http_status(:accepted)
expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
expect(response.header).to have_key 'Range'
expect(response.header).to have_key 'Job-Status'
end
end
context 'when content-range start is too big' do
let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20/6' }) }
it 'gets 416 error response with range headers' do
expect(response).to have_gitlab_http_status(:range_not_satisfiable)
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/13' }) }
it 'gets 416 error response with range headers' do
expect(response).to have_gitlab_http_status(:range_not_satisfiable)
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).to have_gitlab_http_status(:bad_request) }
end
context 'when job has been errased' do
let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
it { expect(response).to have_gitlab_http_status(:forbidden) }
end
def patch_the_trace(content = ' appended', request_headers = nil)
unless request_headers
job.trace.read do |stream|
offset = stream.size
limit = offset + content.length - 1
request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
end
end
Timecop.travel(job.updated_at + update_interval) do
patch api("/jobs/#{job.id}/trace"), params: content, headers: request_headers
job.reload
end
end
def initial_patch_the_trace
patch_the_trace(' appended', headers_with_range)
end
def force_patch_the_trace
2.times { patch_the_trace('') }
end
end
end
end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
let(:registration_token) { 'abcdefg123456' }
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
describe '/api/v4/runners' do
describe 'DELETE /api/v4/runners' do
context 'when no token is provided' do
it 'returns 400 error' do
delete api('/runners')
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when invalid token is provided' do
it 'returns 403 error' do
delete api('/runners'), params: { token: 'invalid' }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when valid token is provided' do
let(:runner) { create(:ci_runner) }
it 'deletes Runner' do
delete api('/runners'), params: { token: runner.token }
expect(response).to have_gitlab_http_status(:no_content)
expect(::Ci::Runner.count).to eq(0)
end
it_behaves_like '412 response' do
let(:request) { api('/runners') }
let(:params) { { token: runner.token } }
end
end
end
end
end

View File

@ -0,0 +1,250 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
let(:registration_token) { 'abcdefg123456' }
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
describe '/api/v4/runners' do
describe 'POST /api/v4/runners' do
context 'when no token is provided' do
it 'returns 400 error' do
post api('/runners')
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when invalid token is provided' do
it 'returns 403 error' do
post api('/runners'), params: { token: 'invalid' }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when valid token is provided' do
it 'creates runner with default values' do
post api('/runners'), params: { token: registration_token }
runner = ::Ci::Runner.first
expect(response).to have_gitlab_http_status(:created)
expect(json_response['id']).to eq(runner.id)
expect(json_response['token']).to eq(runner.token)
expect(runner.run_untagged).to be true
expect(runner.active).to be true
expect(runner.token).not_to eq(registration_token)
expect(runner).to be_instance_type
end
context 'when project token is used' do
let(:project) { create(:project) }
it 'creates project runner' do
post api('/runners'), params: { token: project.runners_token }
expect(response).to have_gitlab_http_status(:created)
expect(project.runners.size).to eq(1)
runner = ::Ci::Runner.first
expect(runner.token).not_to eq(registration_token)
expect(runner.token).not_to eq(project.runners_token)
expect(runner).to be_project_type
end
end
context 'when group token is used' do
let(:group) { create(:group) }
it 'creates a group runner' do
post api('/runners'), params: { token: group.runners_token }
expect(response).to have_gitlab_http_status(:created)
expect(group.runners.reload.size).to eq(1)
runner = ::Ci::Runner.first
expect(runner.token).not_to eq(registration_token)
expect(runner.token).not_to eq(group.runners_token)
expect(runner).to be_group_type
end
end
end
context 'when runner description is provided' do
it 'creates runner' do
post api('/runners'), params: {
token: registration_token,
description: 'server.hostname'
}
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.description).to eq('server.hostname')
end
end
context 'when runner tags are provided' do
it 'creates runner' do
post api('/runners'), params: {
token: registration_token,
tag_list: 'tag1, tag2'
}
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
end
end
context 'when option for running untagged jobs is provided' do
context 'when tags are provided' do
it 'creates runner' do
post api('/runners'), params: {
token: registration_token,
run_untagged: false,
tag_list: ['tag']
}
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.run_untagged).to be false
expect(::Ci::Runner.first.tag_list.sort).to eq(['tag'])
end
end
context 'when tags are not provided' do
it 'returns 400 error' do
post api('/runners'), params: {
token: registration_token,
run_untagged: false
}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to include(
'tags_list' => ['can not be empty when runner is not allowed to pick untagged jobs'])
end
end
end
context 'when option for locking Runner is provided' do
it 'creates runner' do
post api('/runners'), params: {
token: registration_token,
locked: true
}
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.locked).to be true
end
end
context 'when option for activating a Runner is provided' do
context 'when active is set to true' do
it 'creates runner' do
post api('/runners'), params: {
token: registration_token,
active: true
}
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.active).to be true
end
end
context 'when active is set to false' do
it 'creates runner' do
post api('/runners'), params: {
token: registration_token,
active: false
}
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.active).to be false
end
end
end
context 'when access_level is provided for Runner' do
context 'when access_level is set to ref_protected' do
it 'creates runner' do
post api('/runners'), params: {
token: registration_token,
access_level: 'ref_protected'
}
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.ref_protected?).to be true
end
end
context 'when access_level is set to not_protected' do
it 'creates runner' do
post api('/runners'), params: {
token: registration_token,
access_level: 'not_protected'
}
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.ref_protected?).to be false
end
end
end
context 'when maximum job timeout is specified' do
it 'creates runner' do
post api('/runners'), params: {
token: registration_token,
maximum_timeout: 9000
}
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.maximum_timeout).to eq(9000)
end
context 'when maximum job timeout is empty' do
it 'creates runner' do
post api('/runners'), params: {
token: registration_token,
maximum_timeout: ''
}
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.maximum_timeout).to be_nil
end
end
end
%w(name version revision platform architecture).each do |param|
context "when info parameter '#{param}' info is present" do
let(:value) { "#{param}_value" }
it "updates provided Runner's parameter" do
post api('/runners'), params: {
token: registration_token,
info: { param => value }
}
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.read_attribute(param.to_sym)).to eq(value)
end
end
end
it "sets the runner's ip_address" do
post api('/runners'),
params: { token: registration_token },
headers: { 'X-Forwarded-For' => '123.111.123.111' }
expect(response).to have_gitlab_http_status(:created)
expect(::Ci::Runner.first.ip_address).to eq('123.111.123.111')
end
end
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
let(:registration_token) { 'abcdefg123456' }
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
end
describe '/api/v4/runners' do
describe 'POST /api/v4/runners/verify' do
let(:runner) { create(:ci_runner) }
context 'when no token is provided' do
it 'returns 400 error' do
post api('/runners/verify')
expect(response).to have_gitlab_http_status :bad_request
end
end
context 'when invalid token is provided' do
it 'returns 403 error' do
post api('/runners/verify'), params: { token: 'invalid-token' }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when valid token is provided' do
it 'verifies Runner credentials' do
post api('/runners/verify'), params: { token: runner.token }
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
end

File diff suppressed because it is too large Load Diff

View File

@ -169,6 +169,14 @@ RSpec.describe API::GroupVariables do
expect(response).to have_gitlab_http_status(:not_found)
end
it 'responds with 400 if the update fails' do
put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { value: 'shrt', masked: true }
expect(response).to have_gitlab_http_status(:bad_request)
expect(variable.reload.masked).to eq(false)
expect(json_response['message']).to eq('value' => ['is invalid'])
end
end
context 'authorized user with invalid permissions' do

View File

@ -64,6 +64,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
expect(json_response).to have_key('note')
expect(json_response['note']).to eq(user.note)
expect(json_response).to have_key('sign_in_count')
end
end
@ -72,6 +73,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
get api("/users/#{user.id}", user)
expect(json_response).not_to have_key('note')
expect(json_response).not_to have_key('sign_in_count')
end
end
end

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::ChangeVariableService do
let(:service) { described_class.new(container: group, current_user: user, params: params) }
let_it_be(:user) { create(:user) }
let(:group) { create(:group) }
describe '#execute' do
subject(:execute) { service.execute }
context 'when creating a variable' do
let(:params) { { variable_params: { key: 'new_variable', value: 'variable_value' }, action: :create } }
it 'persists a variable' do
expect { execute }.to change(Ci::GroupVariable, :count).from(0).to(1)
end
end
context 'when updating a variable' do
let!(:variable) { create(:ci_group_variable, value: 'old_value') }
let(:params) { { variable_params: { key: variable.key, value: 'new_value' }, action: :update } }
before do
group.variables << variable
end
it 'updates a variable' do
expect { execute }.to change { variable.reload.value }.from('old_value').to('new_value')
end
context 'when the variable does not exist' do
before do
variable.destroy!
end
it 'raises a record not found error' do
expect { execute }.to raise_error(::ActiveRecord::RecordNotFound)
end
end
end
context 'when destroying a variable' do
let!(:variable) { create(:ci_group_variable) }
let(:params) { { variable_params: { key: variable.key }, action: :destroy } }
before do
group.variables << variable
end
it 'destroys a variable' do
expect { execute }.to change { Ci::GroupVariable.exists?(variable.id) }.from(true).to(false)
end
context 'when the variable does not exist' do
before do
variable.destroy!
end
it 'raises a record not found error' do
expect { execute }.to raise_error(::ActiveRecord::RecordNotFound)
end
end
end
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::ChangeVariablesService do
let(:service) { described_class.new(container: group, current_user: user, params: params) }
let_it_be(:user) { create(:user) }
let(:group) { spy(:group, variables: []) }
let(:params) { { variables_attributes: [{ key: 'new_variable', value: 'variable_value' }] } }
describe '#execute' do
subject(:execute) { service.execute }
it 'delegates to ActiveRecord update' do
execute
expect(group).to have_received(:update).with(params)
end
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ProductAnalytics::BuildGraphService do
let_it_be(:project) { create(:project) }
let_it_be(:events) do
[
create(:product_analytics_event, project: project, platform: 'web'),
create(:product_analytics_event, project: project, platform: 'web'),
create(:product_analytics_event, project: project, platform: 'app'),
create(:product_analytics_event, project: project, platform: 'mobile'),
create(:product_analytics_event, project: project, platform: 'mobile', collector_tstamp: Time.zone.now - 60.days)
]
end
let(:params) { { graph: 'platform', timerange: 5 } }
subject { described_class.new(project, params).execute }
it 'returns a valid graph hash' do
expect(subject[:id]).to eq(:platform)
expect(subject[:keys]).to eq(%w(app mobile web))
expect(subject[:values]).to eq([1, 1, 2])
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
RSpec.shared_examples 'API::CI::Runner application context metadata' do |api_route|
it 'contains correct context metadata' do
# Avoids popping the context from the thread so we can
# check its content after the request.
allow(Labkit::Context).to receive(:pop)
send_request
Labkit::Context.with_context do |context|
expected_context = {
'meta.caller_id' => api_route,
'meta.user' => job.user.username,
'meta.project' => job.project.full_path,
'meta.root_namespace' => job.project.full_path_components.first
}
expect(context.to_h).to include(expected_context)
end
end
end