Commit 349faf94 authored by Mayra Cabrera's avatar Mayra Cabrera Committed by Kamil Trzciński

Enable/disable Auto DevOps at Group level

- Includes instance methods on Group model to detect when a group has
AutoDevOps explicitly/implicitly enabled/disabled.
- Includes migration to add a new column to namespaces table
- Add UI necessary modifications
- Add service and controller to update auto devops
related instances
- Updates project and groups auto devops badges

Closes gitlab-ce#52447
parent 5029d416
......@@ -17,6 +17,16 @@ module Groups
redirect_to group_settings_ci_cd_path
end
def update_auto_devops
if auto_devops_service.execute
flash[:notice] = s_('GroupSettings|Auto DevOps pipeline was updated for the group')
else
flash[:alert] = s_("GroupSettings|There was a problem updating Auto DevOps pipeline: %{error_messages}." % { error_messages: group.errors.full_messages })
end
redirect_to group_settings_ci_cd_path
end
private
def define_ci_variables
......@@ -29,6 +39,14 @@ module Groups
def authorize_admin_group!
return render_404 unless can?(current_user, :admin_group, group)
end
def auto_devops_params
params.require(:group).permit(:auto_devops_enabled)
end
def auto_devops_service
Groups::AutoDevopsService.new(group, current_user, auto_devops_params)
end
end
end
end
......@@ -9,4 +9,17 @@ module AutoDevopsHelper
!project.repository.gitlab_ci_yml &&
!project.ci_service
end
def badge_for_auto_devops_scope(auto_devops_receiver)
return unless auto_devops_receiver.auto_devops_enabled?
case auto_devops_receiver.first_auto_devops_config[:scope]
when :project
nil
when :group
s_('CICD|group enabled')
when :instance
s_('CICD|instance enabled')
end
end
end
......@@ -11,6 +11,7 @@ class Namespace < ApplicationRecord
include IgnorableColumn
include FeatureGate
include FromUnion
include Gitlab::Utils::StrongMemoize
ignore_column :deleted_at
......@@ -267,6 +268,22 @@ class Namespace < ApplicationRecord
owner.refresh_authorized_projects
end
def auto_devops_enabled?
first_auto_devops_config[:status]
end
def first_auto_devops_config
return { scope: :group, status: auto_devops_enabled } unless auto_devops_enabled.nil?
strong_memoize(:first_auto_devops_config) do
if has_parent?
parent.first_auto_devops_config
else
{ scope: :instance, status: Gitlab::CurrentSettings.auto_devops_enabled? }
end
end
end
private
def path_or_parent_changed?
......
......@@ -631,12 +631,21 @@ class Project < ActiveRecord::Base
end
def has_auto_devops_implicitly_enabled?
auto_devops&.enabled.nil? &&
(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
auto_devops_config = first_auto_devops_config
auto_devops_config[:scope] != :project && auto_devops_config[:status]
end
def has_auto_devops_implicitly_disabled?
auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
auto_devops_config = first_auto_devops_config
auto_devops_config[:scope] != :project && !auto_devops_config[:status]
end
def first_auto_devops_config
return namespace.first_auto_devops_config if auto_devops&.enabled.nil?
{ scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) }
end
def daily_statistics_enabled?
......
# frozen_string_literal: true
module Groups
class AutoDevopsService < Groups::BaseService
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_group, group)
group.update(auto_devops_enabled: auto_devops_enabled)
end
private
def auto_devops_enabled
params[:auto_devops_enabled]
end
end
end
= form_for group, url: update_auto_devops_group_settings_ci_cd_path(group), method: :patch do |f|
= form_errors(group)
%fieldset
.form-group
.card.auto-devops-card
.card-body
.form-check
= f.check_box :auto_devops_enabled, class: 'form-check-input', checked: group.auto_devops_enabled?
= f.label :auto_devops_enabled, class: 'form-check-label' do
%strong= s_('GroupSettings|Default to Auto DevOps pipeline for all projects within this group')
%span.badge.badge-info#auto-devops-badge= badge_for_auto_devops_scope(group)
.form-text.text-muted
= s_('GroupSettings|The Auto DevOps pipeline will run if no alternative CI configuration file is found.')
= link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank'
= f.submit _('Save changes'), class: 'btn btn-success prepend-top-15'
......@@ -19,3 +19,17 @@
= _('Register and see your runners for this group.')
.settings-content
= render 'groups/runners/index'
%section.settings#auto-devops-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Auto DevOps')
%button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand')
%p
- auto_devops_url = help_page_path('topics/autodevops/index')
- auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
= s_('GroupSettings|Auto DevOps will automatically build, test and deploy your application based on a predefined Continuous Integration and Delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe }
.settings-content
= render 'groups/settings/ci_cd/auto_devops_form', group: @group
......@@ -8,15 +8,15 @@
.card.auto-devops-card
.card-body
.form-check
= form.check_box :enabled, class: 'form-check-input js-toggle-extra-settings', checked: @project.auto_devops_enabled?
= form.check_box :enabled, class: 'form-check-input js-toggle-extra-settings', checked: auto_devops_enabled
= form.label :enabled, class: 'form-check-label' do
%strong= s_('CICD|Default to Auto DevOps pipeline')
- if @project.has_auto_devops_implicitly_enabled?
%span.badge.badge-info.js-instance-default-badge= s_('CICD|instance enabled')
- if auto_devops_enabled
%span.badge.badge-info.js-instance-default-badge= badge_for_auto_devops_scope(@project)
.form-text.text-muted
= s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.')
= link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank'
.card-footer.js-extra-settings{ class: @project.auto_devops_enabled? || 'hidden' }
.card-footer.js-extra-settings{ class: auto_devops_enabled || 'hidden' }
%p.settings-message.text-center
- kubernetes_cluster_link = help_page_path('user/project/clusters/index')
- kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link }
......
......@@ -26,7 +26,7 @@
= s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
= link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
.settings-content
= render 'autodevops_form'
= render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled?
= render_if_exists 'projects/settings/ci_cd/protected_environments', expanded: expanded
......
---
title: Enable/disable Auto DevOps at the Group level
merge_request: 25533
author:
type: added
......@@ -31,6 +31,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
namespace :settings do
resource :ci_cd, only: [:show], controller: 'ci_cd' do
put :reset_registration_token
patch :update_auto_devops
end
end
......
# frozen_string_literal: true
class AddAutoDevOpsEnabledToNamespaces < ActiveRecord::Migration[5.0]
DOWNTIME = false
def change
add_column :namespaces, :auto_devops_enabled, :boolean
end
end
......@@ -1917,6 +1917,7 @@ ActiveRecord::Schema.define(version: 20190305162221) do
t.string "saml_discovery_token"
t.string "runners_token_encrypted"
t.integer "custom_project_templates_group_id"
t.boolean "auto_devops_enabled"
t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree
t.index ["custom_project_templates_group_id", "type"], name: "index_namespaces_on_custom_project_templates_group_id_and_type", where: "(custom_project_templates_group_id IS NOT NULL)", using: :btree
t.index ["file_template_project_id"], name: "index_namespaces_on_file_template_project_id", using: :btree
......
......@@ -1767,6 +1767,9 @@ msgstr ""
msgid "CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly."
msgstr ""
msgid "CICD|group enabled"
msgstr ""
msgid "CICD|instance enabled"
msgstr ""
......@@ -5088,6 +5091,12 @@ msgstr ""
msgid "GroupRoadmap|Until %{dateWord}"
msgstr ""
msgid "GroupSettings|Auto DevOps pipeline was updated for the group"
msgstr ""
msgid "GroupSettings|Auto DevOps will automatically build, test and deploy your application based on a predefined Continuous Integration and Delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end}"
msgstr ""
msgid "GroupSettings|Badges"
msgstr ""
......@@ -5097,6 +5106,9 @@ msgstr ""
msgid "GroupSettings|Customize your group badges."
msgstr ""
msgid "GroupSettings|Default to Auto DevOps pipeline for all projects within this group"
msgstr ""
msgid "GroupSettings|Learn more about badges."
msgstr ""
......@@ -5109,6 +5121,12 @@ msgstr ""
msgid "GroupSettings|Select a sub-group as the custom project template source for this group."
msgstr ""
msgid "GroupSettings|The Auto DevOps pipeline will run if no alternative CI configuration file is found."
msgstr ""
msgid "GroupSettings|There was a problem updating Auto DevOps pipeline: %{error_messages}."
msgstr ""
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
msgstr ""
......
......@@ -66,4 +66,77 @@ describe Groups::Settings::CiCdController do
end
end
end
describe 'PATCH #update_auto_devops' do
let(:auto_devops_param) { '1' }
subject do
patch :update_auto_devops, params: {
group_id: group,
group: { auto_devops_enabled: auto_devops_param }
}
end
context 'when user does not have enough permission' do
before do
group.add_maintainer(user)
end
it { is_expected.to have_gitlab_http_status(404) }
end
context 'when user has enough privileges' do
before do
group.add_owner(user)
end
it { is_expected.to redirect_to(group_settings_ci_cd_path) }
context 'when service execution went wrong' do
before do
allow_any_instance_of(Groups::AutoDevopsService).to receive(:execute).and_return(false)
allow_any_instance_of(Group).to receive_message_chain(:errors, :full_messages)
.and_return(['Error 1'])
subject
end
it 'returns a flash alert' do
expect(response).to set_flash[:alert]
.to eq("There was a problem updating Auto DevOps pipeline: [\"Error 1\"].")
end
end
context 'when service execution was successful' do
it 'returns a flash notice' do
subject
expect(response).to set_flash[:notice]
.to eq('Auto DevOps pipeline was updated for the group')
end
end
context 'when changing auto devops value' do
before do
subject
group.reload
end
context 'when explicitly enabling auto devops' do
it 'should update group attribute' do
expect(group.auto_devops_enabled).to eq(true)
end
end
context 'when explicitly disabling auto devops' do
let(:auto_devops_param) { '0' }
it 'should update group attribute' do
expect(group.auto_devops_enabled).to eq(false)
end
end
end
end
end
end
......@@ -36,5 +36,13 @@ FactoryBot.define do
trait :nested do
parent factory: :group
end
trait :auto_devops_enabled do
auto_devops_enabled true
end
trait :auto_devops_disabled do
auto_devops_enabled false
end
end
end
......@@ -271,6 +271,10 @@ FactoryBot.define do
trait :auto_devops do
association :auto_devops, factory: :project_auto_devops
end
trait :auto_devops_disabled do
association :auto_devops, factory: [:project_auto_devops, :disabled]
end
end
# Project with empty repository
......
......@@ -5,8 +5,8 @@ require 'spec_helper'
describe 'Group CI/CD settings' do
include WaitForRequests
let(:user) {create(:user)}
let(:group) {create(:group)}
let(:user) { create(:user) }
let(:group) { create(:group) }
before do
group.add_owner(user)
......@@ -36,4 +36,45 @@ describe 'Group CI/CD settings' do
end
end
end
describe 'Auto DevOps form' do
before do
stub_application_setting(auto_devops_enabled: true)
end
context 'as owner first visiting group settings' do
it 'should see instance enabled badge' do
visit group_settings_ci_cd_path(group)
page.within '#auto-devops-settings' do
expect(page).to have_content('instance enabled')
end
end
end
context 'when Auto DevOps group has been enabled' do
it 'should see group enabled badge' do
group.update!(auto_devops_enabled: true)
visit group_settings_ci_cd_path(group)
page.within '#auto-devops-settings' do
expect(page).to have_content('group enabled')
end
end
end
context 'when Auto DevOps group has been disabled' do
it 'should not see a badge' do
group.update!(auto_devops_enabled: false)
visit group_settings_ci_cd_path(group)
page.within '#auto-devops-settings' do
expect(page).not_to have_content('instance enabled')
expect(page).not_to have_content('group enabled')
end
end
end
end
end
......@@ -110,6 +110,37 @@ describe "Projects > Settings > Pipelines settings" do
expect(page).not_to have_content('instance enabled')
end
end
context 'when auto devops is turned on group level' do
before do
project.update!(namespace: create(:group, :auto_devops_enabled))
end
it 'renders group enabled badge' do
visit project_settings_ci_cd_path(project)
page.within '#autodevops-settings' do
expect(page).to have_content('group enabled')
expect(find_field('project_auto_devops_attributes_enabled')).to be_checked
end
end
end
context 'when auto devops is turned on group parent level', :nested_groups do
before do
group = create(:group, parent: create(:group, :auto_devops_enabled))
project.update!(namespace: group)
end
it 'renders group enabled badge' do
visit project_settings_ci_cd_path(project)
page.within '#autodevops-settings' do
expect(page).to have_content('group enabled')
expect(find_field('project_auto_devops_attributes_enabled')).to be_checked
end
end
end
end
end
......
......@@ -29,11 +29,11 @@ describe AutoDevopsHelper do
end
context 'when the banner is disabled by feature flag' do
it 'allows the feature flag to disable' do
before do
Feature.get(:auto_devops_banner_disabled).enable
expect(subject).to be(false)
end
it { is_expected.to be_falsy }
end
context 'when dismissed' do
......@@ -90,4 +90,136 @@ describe AutoDevopsHelper do
it { is_expected.to eq(false) }
end
end
describe '#badge_for_auto_devops_scope' do
subject { helper.badge_for_auto_devops_scope(receiver) }
context 'when receiver is a group' do
context 'when explicitly enabled' do
let(:receiver) { create(:group, :auto_devops_enabled) }
it { is_expected.to eq('group enabled') }
end
context 'when explicitly disabled' do
let(:receiver) { create(:group, :auto_devops_disabled) }
it { is_expected.to be_nil }
end
context 'when auto devops is implicitly enabled' do
let(:receiver) { create(:group) }
context 'by instance' do
before do
stub_application_setting(auto_devops_enabled: true)
end
it { is_expected.to eq('instance enabled') }
end
context 'with groups', :nested_groups do
before do
receiver.update(parent: parent)
end
context 'when auto devops is enabled on parent' do
let(:parent) { create(:group, :auto_devops_enabled) }
it { is_expected.to eq('group enabled') }
end
context 'when auto devops is enabled on parent group' do
let(:root_parent) { create(:group, :auto_devops_enabled) }
let(:parent) { create(:group, parent: root_parent) }
it { is_expected.to eq('group enabled') }
end
context 'when auto devops disabled set on parent group' do
let(:root_parent) { create(:group, :auto_devops_disabled) }
let(:parent) { create(:group, parent: root_parent) }
it { is_expected.to be_nil }
end
end
end
end
context 'when receiver is a project' do
context 'when auto devops is enabled at project level' do
let(:receiver) { create(:project, :auto_devops) }
it { is_expected.to be_nil }
end
context 'when auto devops is disabled at project level' do
let(:receiver) { create(:project, :auto_devops_disabled) }
it { is_expected.to be_nil }
end
context 'when auto devops is implicitly enabled' do
let(:receiver) { create(:project) }
context 'by instance' do
before do
stub_application_setting(auto_devops_enabled: true)
end
it { is_expected.to eq('instance enabled') }
end
context 'with groups', :nested_groups do
let(:receiver) { create(:project, :repository, namespace: group) }
before do
stub_application_setting(auto_devops_enabled: false)
end
context 'when auto devops is enabled on group level' do
let(:group) { create(:group, :auto_devops_enabled) }
it { is_expected.to eq('group enabled') }
end
context 'when auto devops is enabled on root group' do
let(:root_parent) { create(:group, :auto_devops_enabled) }
let(:group) { create(:group, parent: root_parent) }
it { is_expected.to eq('group enabled') }
end
end
end
context 'when auto devops is implicitly disabled' do
let(:receiver) { create(:project) }
context 'by instance' do
before do
stub_application_setting(auto_devops_enabled: false)
end
it { is_expected.to be_nil }
end
context 'with groups', :nested_groups do
let(:receiver) { create(:project, :repository, namespace: group) }
context 'when auto devops is disabled on group level' do
let(:group) { create(:group, :auto_devops_disabled) }
it { is_expected.to be_nil }
end
context 'when root group is enabled and parent disabled' do
let(:root_parent) { create(:group, :auto_devops_enabled) }
let(:group) { create(:group, :auto_devops_disabled, parent: root_parent) }
it { is_expected.to be_nil }
end
end
end
end
end
end
......@@ -810,4 +810,125 @@ describe Group do
it { is_expected.to be_truthy }
end
end
describe '#first_auto_devops_config' do
using RSpec::Parameterized::TableSyntax
let(:group) { create(:group) }
subject { group.first_auto_devops_config }
where(:instance_value, :group_value, :config) do
# Instance level enabled
true | nil | { status: true, scope: :instance }
true | true | { status: true, scope: :group }
true | false | { status: false, scope: :group }
# Instance level disabled
false | nil | { status: false, scope: :instance }
false | true | { status: true, scope: :group }
false | false | { status: false, scope: :group }
end
with_them do
before do
stub_application_setting(auto_devops_enabled: instance_value)
group.update_attribute(:auto_devops_enabled, group_value)
end
it { is_expected.to eq(config) }
end
context 'with parent groups', :nested_groups do
where(:instance_value, :parent_value, :group_value, :config) do
# Instance level enabled
true | nil | nil | { status: true, scope: :instance }
true | nil | true | { status: true, scope: :group }
true | nil | false | { status: false, scope: :group }
true | true | nil | { status: true, scope: :group }
true | true | true | { status: true, scope: :group }
true | true | false | { status: false, scope: :group }
true | false | nil | { status: false, scope: :group }
true | false | true | { status: true, scope: :group }
true | false | false | { status: false, scope: :group }
# Instance level disable
false | nil | nil | { status: false, scope: :instance }
false | nil | true | { status: true, scope: :group }
false | nil | false | { status: false, scope: :group }
false | true | nil | { status: true, scope: :group }
false | true | true | { status: true, scope: :group }
false | true | false | { status: false, scope: :group }
false | false | nil | { status: false, scope: :group }
false | false | true | { status: true, scope: :group }
false | false | false | { status: false, scope: :group }
end
with_them do
before do
stub_application_setting(auto_devops_enabled: instance_value)
parent = create(:group, auto_devops_enabled: parent_value)
group.update!(
auto_devops_enabled: group_value,
parent: parent
)
end
it { is_expected.to eq(config) }
end
end
end
describe '#auto_devops_enabled?' do
subject { group.auto_devops_enabled? }
context 'when auto devops is explicitly enabled on group' do
let(:group) { create(:group, :auto_devops_enabled) }
it { is_expected.to be_truthy }
end
context 'when auto devops is explicitly disabled on group' do