Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
55b5a8778c
commit
e65a1b9830
|
@ -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');
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -15,6 +15,7 @@ const whiteSpaceFromCookie = Cookies.get(DIFF_WHITESPACE_COOKIE_NAME);
|
|||
|
||||
export default () => ({
|
||||
isLoading: true,
|
||||
isTreeLoaded: false,
|
||||
isBatchLoading: false,
|
||||
retrievingBatches: false,
|
||||
addedLines: null,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import initActivityCharts from '~/analytics/product_analytics/activity_charts_bundle';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => initActivityCharts());
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
|
@ -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')
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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] }
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
- page_title 'Product Analytics'
|
||||
- page_title _('Product Analytics')
|
||||
|
||||
= render 'links'
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
= render "links"
|
||||
- page_title _('Product Analytics')
|
||||
|
||||
= render 'links'
|
||||
|
||||
%p
|
||||
= _('Copy the code below to implement tracking in your application:')
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
- page_title _('Product Analytics')
|
||||
|
||||
= render 'links'
|
||||
|
||||
%p
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace fa-git icons with link svg
|
||||
merge_request: 38078
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Keep large spinner while MR file tree is loading
|
||||
merge_request: 36446
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add sign_in_count to /users/:id API for admins
|
||||
merge_request: 35726
|
||||
author: Luc Didry
|
||||
type: changed
|
|
@ -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
|
||||
|
|
|
@ -310,6 +310,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
collection do
|
||||
get :setup
|
||||
get :test
|
||||
get :graphs
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {})
|
||||
|
|
|
@ -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
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -41,6 +41,7 @@ describe('diffs/components/app', () => {
|
|||
|
||||
store = createDiffsStore();
|
||||
store.state.diffs.isLoading = false;
|
||||
store.state.diffs.isTreeLoaded = true;
|
||||
|
||||
extendStore(store);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue