Commit 487820ad authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'disallow-retry-of-old-builds' into 'master'

Disallow retry of old builds

Closes #50939

See merge request gitlab-org/gitlab-ce!22538
parents c12a4a9a 40397f35
...@@ -115,6 +115,7 @@ module ApplicationSettingsHelper ...@@ -115,6 +115,7 @@ module ApplicationSettingsHelper
:akismet_api_key, :akismet_api_key,
:akismet_enabled, :akismet_enabled,
:allow_local_requests_from_hooks_and_services, :allow_local_requests_from_hooks_and_services,
:archive_builds_in_human_readable,
:authorized_keys_enabled, :authorized_keys_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:auto_devops_domain, :auto_devops_domain,
......
...@@ -5,6 +5,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -5,6 +5,7 @@ class ApplicationSetting < ActiveRecord::Base
include CacheMarkdownField include CacheMarkdownField
include TokenAuthenticatable include TokenAuthenticatable
include IgnorableColumn include IgnorableColumn
include ChronicDurationAttribute
add_authentication_token_field :runners_registration_token add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token add_authentication_token_field :health_check_access_token
...@@ -45,6 +46,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -45,6 +46,8 @@ class ApplicationSetting < ActiveRecord::Base
default_value_for :id, 1 default_value_for :id, 1
chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
validates :uuid, presence: true validates :uuid, presence: true
validates :session_expire_delay, validates :session_expire_delay,
...@@ -184,6 +187,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -184,6 +187,10 @@ class ApplicationSetting < ActiveRecord::Base
validates :user_default_internal_regex, js_regex: true, allow_nil: true validates :user_default_internal_regex, js_regex: true, allow_nil: true
validates :archive_builds_in_seconds,
allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds }
SUPPORTED_KEY_TYPES.each do |type| SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end end
...@@ -441,6 +448,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -441,6 +448,10 @@ class ApplicationSetting < ActiveRecord::Base
latest_terms latest_terms
end end
def archive_builds_older_than
archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds
end
private private
def ensure_uuid! def ensure_uuid!
......
...@@ -258,8 +258,24 @@ module Ci ...@@ -258,8 +258,24 @@ module Ci
self.name == 'pages' self.name == 'pages'
end end
# degenerated build is one that cannot be run by Runner
def degenerated?
self.options.nil?
end
def degenerate!
self.update!(options: nil, yaml_variables: nil, commands: nil)
end
def archived?
return true if degenerated?
archive_builds_older_than = Gitlab::CurrentSettings.current_application_settings.archive_builds_older_than
archive_builds_older_than.present? && created_at < archive_builds_older_than
end
def playable? def playable?
action? && (manual? || scheduled? || retryable?) action? && !archived? && (manual? || scheduled? || retryable?)
end end
def schedulable? def schedulable?
...@@ -287,7 +303,7 @@ module Ci ...@@ -287,7 +303,7 @@ module Ci
end end
def retryable? def retryable?
success? || failed? || canceled? !archived? && (success? || failed? || canceled?)
end end
def retries_count def retries_count
...@@ -295,7 +311,7 @@ module Ci ...@@ -295,7 +311,7 @@ module Ci
end end
def retries_max def retries_max
self.options.fetch(:retry, 0).to_i self.options.to_h.fetch(:retry, 0).to_i
end end
def latest? def latest?
......
...@@ -51,7 +51,8 @@ class CommitStatus < ActiveRecord::Base ...@@ -51,7 +51,8 @@ class CommitStatus < ActiveRecord::Base
missing_dependency_failure: 5, missing_dependency_failure: 5,
runner_unsupported: 6, runner_unsupported: 6,
stale_schedule: 7, stale_schedule: 7,
job_execution_timeout: 8 job_execution_timeout: 8,
archived_failure: 9
} }
## ##
...@@ -167,16 +168,18 @@ class CommitStatus < ActiveRecord::Base ...@@ -167,16 +168,18 @@ class CommitStatus < ActiveRecord::Base
false false
end end
# To be overridden when inherrited from
def retryable? def retryable?
false false
end end
# To be overridden when inherrited from
def cancelable? def cancelable?
false false
end end
def archived?
false
end
def stuck? def stuck?
false false
end end
......
...@@ -20,12 +20,17 @@ module Ci ...@@ -20,12 +20,17 @@ module Ci
@subject.project.branch_allows_collaboration?(@user, @subject.ref) @subject.project.branch_allows_collaboration?(@user, @subject.ref)
end end
condition(:archived, scope: :subject) do
@subject.archived?
end
condition(:terminal, scope: :subject) do condition(:terminal, scope: :subject) do
@subject.has_terminal? @subject.has_terminal?
end end
rule { protected_ref }.policy do rule { protected_ref | archived }.policy do
prevent :update_build prevent :update_build
prevent :update_commit_status
prevent :erase_build prevent :erase_build
end end
......
...@@ -2,4 +2,13 @@ ...@@ -2,4 +2,13 @@
class DeploymentPolicy < BasePolicy class DeploymentPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
condition(:can_retry_deployable) do
can?(:update_build, @subject.deployable)
end
rule { ~can_retry_deployable }.policy do
prevent :create_deployment
prevent :update_deployment
end
end end
...@@ -10,7 +10,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated ...@@ -10,7 +10,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
missing_dependency_failure: 'There has been a missing dependency failure', missing_dependency_failure: 'There has been a missing dependency failure',
runner_unsupported: 'Your runner is outdated, please upgrade your runner', runner_unsupported: 'Your runner is outdated, please upgrade your runner',
stale_schedule: 'Delayed job could not be executed by some reason, please try again', stale_schedule: 'Delayed job could not be executed by some reason, please try again',
job_execution_timeout: 'The script exceeded the maximum execution time set for the job' job_execution_timeout: 'The script exceeded the maximum execution time set for the job',
archived_failure: 'The job is archived and cannot be run'
}.freeze }.freeze
private_constant :CALLOUT_FAILURE_MESSAGES private_constant :CALLOUT_FAILURE_MESSAGES
...@@ -30,6 +31,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated ...@@ -30,6 +31,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
end end
def unrecoverable? def unrecoverable?
script_failure? || missing_dependency_failure? script_failure? || missing_dependency_failure? || archived_failure?
end end
end end
...@@ -7,6 +7,7 @@ class JobEntity < Grape::Entity ...@@ -7,6 +7,7 @@ class JobEntity < Grape::Entity
expose :name expose :name
expose :started?, as: :started expose :started?, as: :started
expose :archived?, as: :archived
expose :build_path do |build| expose :build_path do |build|
build_path(build) build_path(build)
......
...@@ -82,6 +82,11 @@ module Ci ...@@ -82,6 +82,11 @@ module Ci
return false return false
end end
if build.archived?
build.drop!(:archived_failure)
return false
end
build.run! build.run!
true true
end end
......
...@@ -41,5 +41,13 @@ ...@@ -41,5 +41,13 @@
The default unit is in seconds, but you can define an alternative. For example: The default unit is in seconds, but you can define an alternative. For example:
<code>4 mins 2 sec</code>, <code>2h42min</code>. <code>4 mins 2 sec</code>, <code>2h42min</code>.
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
.form-group
= f.label :archive_builds_in_human_readable, 'Archive builds in', class: 'label-bold'
= f.text_field :archive_builds_in_human_readable, class: 'form-control', placeholder: 'never'
.form-text.text-muted
Set the duration when build gonna be considered old. Archived builds cannot be retried.
Make it empty to never expire builds. It has to be larger than 1 day.
The default unit is in seconds, but you can define an alternative. For example:
<code>4 mins 2 sec</code>, <code>2h42min</code>.
= f.submit 'Save changes', class: "btn btn-success" = f.submit 'Save changes', class: "btn btn-success"
- if can?(current_user, :create_deployment, deployment) && deployment.deployable - if can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment') - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do
- if deployment.last? - if deployment.last?
......
...@@ -61,12 +61,14 @@ ...@@ -61,12 +61,14 @@
%td.responsive-table-cell.build-failure{ data: { column: _("Failure")} } %td.responsive-table-cell.build-failure{ data: { column: _("Failure")} }
= build.present.callout_failure_message = build.present.callout_failure_message
%td.responsive-table-cell.build-actions %td.responsive-table-cell.build-actions
= link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do - if can?(current_user, :update_build, job)
= icon('repeat') = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
%tr.build-trace-row.responsive-table-border-end = icon('repeat')
%td - if can?(current_user, :read_build, job)
%td.responsive-table-cell.build-trace-container{ colspan: 4 } %tr.build-trace-row.responsive-table-border-end
%pre.build-trace.build-trace-rounded %td
%code.bash.js-build-output %td.responsive-table-cell.build-trace-container{ colspan: 4 }
= build_summary(build) %pre.build-trace.build-trace-rounded
%code.bash.js-build-output
= build_summary(build)
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project = render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
---
title: Soft-archive old jobs
merge_request:
author:
type: added
# frozen_string_literal: true
class AddArchiveBuildsDurationToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column(:application_settings, :archive_builds_in_seconds, :integer, allow_null: true)
end
end
...@@ -165,6 +165,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do ...@@ -165,6 +165,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
t.integer "usage_stats_set_by_user_id" t.integer "usage_stats_set_by_user_id"
t.integer "receive_max_input_size" t.integer "receive_max_input_size"
t.integer "diff_max_patch_bytes", default: 102400, null: false t.integer "diff_max_patch_bytes", default: 102400, null: false
t.integer "archive_builds_in_seconds"
end end
create_table "audit_events", force: :cascade do |t| create_table "audit_events", force: :cascade do |t|
......
...@@ -14,7 +14,8 @@ module Gitlab ...@@ -14,7 +14,8 @@ module Gitlab
missing_dependency_failure: 'missing dependency failure', missing_dependency_failure: 'missing dependency failure',
runner_unsupported: 'unsupported runner', runner_unsupported: 'unsupported runner',
stale_schedule: 'stale schedule', stale_schedule: 'stale schedule',
job_execution_timeout: 'job execution timeout' job_execution_timeout: 'job execution timeout',
archived_failure: 'archived failure'
}.freeze }.freeze
private_constant :REASONS private_constant :REASONS
......
...@@ -27,6 +27,12 @@ FactoryBot.define do ...@@ -27,6 +27,12 @@ FactoryBot.define do
pipeline factory: :ci_pipeline pipeline factory: :ci_pipeline
trait :degenerated do
commands nil
options nil
yaml_variables nil
end
trait :started do trait :started do
started_at 'Di 29. Okt 09:51:28 CET 2013' started_at 'Di 29. Okt 09:51:28 CET 2013'
end end
......
...@@ -53,10 +53,21 @@ describe 'Environment' do ...@@ -53,10 +53,21 @@ describe 'Environment' do
it 'does show build name' do it 'does show build name' do
expect(page).to have_link("#{build.name} (##{build.id})") expect(page).to have_link("#{build.name} (##{build.id})")
expect(page).to have_link('Re-deploy') expect(page).not_to have_link('Re-deploy')
expect(page).not_to have_terminal_button expect(page).not_to have_terminal_button
end end
context 'when user has ability to re-deploy' do
let(:permissions) do
create(:protected_branch, :developers_can_merge,
name: build.ref, project: project)
end
it 'does show re-deploy' do
expect(page).to have_link('Re-deploy')
end
end
context 'with manual action' do context 'with manual action' do
let(:action) do let(:action) do
create(:ci_build, :manual, pipeline: pipeline, create(:ci_build, :manual, pipeline: pipeline,
......
...@@ -388,54 +388,83 @@ describe 'Pipeline', :js do ...@@ -388,54 +388,83 @@ describe 'Pipeline', :js do
let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) } let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) } let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) }
subject { visit pipeline_failures_page }
context 'with failed build' do context 'with failed build' do
before do before do
failed_build.trace.set('4 examples, 1 failure') failed_build.trace.set('4 examples, 1 failure')
visit pipeline_failures_page
end end
it 'shows jobs tab pane as active' do it 'shows jobs tab pane as active' do
subject
expect(page).to have_content('Failed Jobs') expect(page).to have_content('Failed Jobs')
expect(page).to have_css('#js-tab-failures.active') expect(page).to have_css('#js-tab-failures.active')
end end
it 'lists failed builds' do it 'lists failed builds' do
subject
expect(page).to have_content(failed_build.name) expect(page).to have_content(failed_build.name)
expect(page).to have_content(failed_build.stage) expect(page).to have_content(failed_build.stage)
end end
it 'shows build failure logs' do it 'shows build failure logs' do
subject
expect(page).to have_content('4 examples, 1 failure') expect(page).to have_content('4 examples, 1 failure')
end end
it 'shows the failure reason' do it 'shows the failure reason' do
subject
expect(page).to have_content('There is an unknown failure, please try again') expect(page).to have_content('There is an unknown failure, please try again')
end end
it 'shows retry button for failed build' do context 'when user does not have permission to retry build' do
page.within(find('.build-failures', match: :first)) do it 'shows retry button for failed build' do
expect(page).to have_link('Retry') subject
page.within(find('.build-failures', match: :first)) do
expect(page).not_to have_link('Retry')
end
end end
end end
end
context 'when missing build logs' do context 'when user does have permission to retry build' do
before do before do
visit pipeline_failures_page create(:protected_branch, :developers_can_merge,
name: pipeline.ref, project: project)
end
it 'shows retry button for failed build' do
subject
page.within(find('.build-failures', match: :first)) do
expect(page).to have_link('Retry')
end
end
end end
end
context 'when missing build logs' do
it 'shows jobs tab pane as active' do it 'shows jobs tab pane as active' do
subject
expect(page).to have_content('Failed Jobs') expect(page).to have_content('Failed Jobs')
expect(page).to have_css('#js-tab-failures.active') expect(page).to have_css('#js-tab-failures.active')
end end
it 'lists failed builds' do it 'lists failed builds' do
subject
expect(page).to have_content(failed_build.name) expect(page).to have_content(failed_build.name)
expect(page).to have_content(failed_build.stage) expect(page).to have_content(failed_build.stage)
end end
it 'does not show trace' do it 'does not show trace' do
subject
expect(page).to have_content('No job trace') expect(page).to have_content('No job trace')
end end
end end
...@@ -448,11 +477,9 @@ describe 'Pipeline', :js do ...@@ -448,11 +477,9 @@ describe 'Pipeline', :js do
end end
context 'when accessing failed jobs page' do context 'when accessing failed jobs page' do
before do
visit pipeline_failures_page
end
it 'fails to access the page' do it 'fails to access the page' do
subject
expect(page).to have_title('Access Denied') expect(page).to have_title('Access Denied')
end end
end end
...@@ -461,11 +488,11 @@ describe 'Pipeline', :js do ...@@ -461,11 +488,11 @@ describe 'Pipeline', :js do
context 'without failures' do context 'without failures' do
before do before do
failed_build.update!(status: :success) failed_build.update!(status: :success)
visit pipeline_failures_page
end end
it 'displays the pipeline graph' do it 'displays the pipeline graph' do
subject
expect(current_path).to eq(pipeline_path(pipeline)) expect(current_path).to eq(pipeline_path(pipeline))
expect(page).not_to have_content('Failed Jobs') expect(page).not_to have_content('Failed Jobs')
expect(page).to have_selector('.pipeline-visualization') expect(page).to have_selector('.pipeline-visualization')
......
...@@ -9,7 +9,8 @@ ...@@ -9,7 +9,8 @@
"playable", "playable",
"created_at", "created_at",
"updated_at", "updated_at",
"status" "status",
"archived"
], ],
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
...@@ -27,7 +28,8 @@ ...@@ -27,7 +28,8 @@
"updated_at": { "type": "string" }, "updated_at": { "type": "string" },
"status": { "$ref": "../status/ci_detailed_status.json" }, "status": { "$ref": "../status/ci_detailed_status.json" },
"callout_message": { "type": "string" }, "callout_message": { "type": "string" },
"recoverable": { "type": "boolean" } "recoverable": { "type": "boolean" },
"archived": { "type": "boolean" }
}, },
"additionalProperties": true "additionalProperties": true
} }
...@@ -594,4 +594,24 @@ describe ApplicationSetting do ...@@ -594,4 +594,24 @@ describe ApplicationSetting do
end end
end end
end end
describe '#archive_builds_older_than' do
subject { setting.archive_builds_older_than }
context 'when the archive_builds_in_seconds is set' do
before do
setting.archive_builds_in_seconds = 3600
end
it { is_expected.to be_within(1.minute).of(1.hour.ago) }
end
context 'when the archive_builds_in_seconds is set' do
before do
setting.archive_builds_in_seconds = nil
end
it { is_expected.to be_nil }
end
end
end end
...@@ -1314,6 +1314,14 @@ describe Ci::Build do ...@@ -1314,6 +1314,14 @@ describe Ci::Build do
it { is_expected.not_to be_retryable } it { is_expected.not_to be_retryable }
end end
context 'when build is degenerated' do
before do
build.degenerate!
end
it { is_expected.not_to be_retryable }
end
end end
end end
...@@ -1396,6 +1404,14 @@ describe Ci::Build do ...@@ -1396,6 +1404,14 @@ describe Ci::Build do
expect(subject.retries_max).to eq 0 expect(subject.retries_max).to eq 0
end end
end end
context 'when build is degenerated' do
subject { create(:ci_build, :degenerated) }
it 'returns zero' do
expect(subject.retries_max).to eq 0
end
end
end end
end end
...@@ -1659,6 +1675,12 @@ describe Ci::Build do ...@@ -1659,6 +1675,12 @@ describe Ci::Build do
it { is_expected.to be_playable } it { is_expected.to be_playable }
end end
context 'when build is a manual and degenerated' do
subject { build_stubbed(:ci_build, :manual, :degenerated, status: :manual) }
it { is_expected.not_to be_playable }
end
end end
context 'when build is scheduled' do context 'when build is scheduled' do
...@@ -3227,4 +3249,54 @@ describe Ci::Build do ...@@ -3227,4 +3249,54 @@ describe Ci::Build do
it { expect(build.deployment_status).to eq(:creating) } it { expect(build.deployment_status).to eq(:creating) }
end end
end end
describe '#degenerated?' do
context 'when build is degenerated' do
subject { create(:ci_build, :degenerated) }
it { is_expected.to be_degenerated }
end
context 'when build is valid' do
subject { create(:ci_build) }
it { is_expected.not_to be_degenerated }
context 'and becomes degenerated' do
before do
subject.degenerate!
end
it { is_expected.to be_degenerated }
end
end
end
describe '#archived?' do
context 'when build is degenerated' do
subject { create(:ci_build, :degenerated) }
it { is_expected.to be_archived }
end
context 'for old build' do
subject { create(:ci_build, created_at: 1.day.ago) }
context 'when archive_builds_in is set' do
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it { is_expected.to be_archived }
end
context 'when archive_builds_in is not set' do
before do
stub_application_setting(archive_builds_in_seconds: nil)
end
it { is_expected.not_to be_archived }
end
end
end
end end
...@@ -267,7 +267,7 @@ describe Ci::BuildPresenter do ...@@ -267,7 +267,7 @@ describe Ci::BuildPresenter do
let(:build) { create(:ci_build, :failed, :script_failure) } let(:build) { create(:ci_build, :failed, :script_failure) }
context 'when is a script or missing dependency failure' do context 'when is a script or missing dependency failure' do
let(:failure_reasons) { %w(script_failure missing_dependency_failure) } let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure) }
it 'should return false' do it 'should return false' do
failure_reasons.each do |failure_reason| failure_reasons.each do |failure_reason|
......
...@@ -478,6 +478,20 @@ module Ci ...@@ -478,6 +478,20 @@ module Ci
it_behaves_like 'validation is not active' it_behaves_like 'validation is not active'
end end
end end
context 'when build is degenerated' do
let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) }
subject { execute(specific_runner, {}) }
it 'does not pick the build and drops the build' do
expect(subject).to be_nil
pending_job.reload
expect(pending_job).to be_failed
expect(pending_job).to be_archived_failure
end
end
end end
describe '#register_success' do describe '#register_success' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment