Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-12 03:08:56 +00:00
parent 723dc8aced
commit f5703a054c
33 changed files with 602 additions and 86 deletions

View file

@ -25,8 +25,7 @@ gem 'marginalia', '~> 1.10.0'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.7.2' gem 'devise', '~> 4.7.2'
# TODO: verify ARM compile issue on 3.1.13+ version (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18828) gem 'bcrypt', '~> 3.1', '>= 3.1.14'
gem 'bcrypt', '3.1.12'
gem 'doorkeeper', '~> 5.5.0.rc2' gem 'doorkeeper', '~> 5.5.0.rc2'
gem 'doorkeeper-openid_connect', '~> 1.7.5' gem 'doorkeeper-openid_connect', '~> 1.7.5'
gem 'omniauth', '~> 1.8' gem 'omniauth', '~> 1.8'

View file

@ -128,7 +128,7 @@ GEM
babosa (1.0.2) babosa (1.0.2)
base32 (0.3.2) base32 (0.3.2)
batch-loader (2.0.1) batch-loader (2.0.1)
bcrypt (3.1.12) bcrypt (3.1.16)
bcrypt_pbkdf (1.0.0) bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0) benchmark-ips (2.3.0)
benchmark-memory (0.1.2) benchmark-memory (0.1.2)
@ -1346,7 +1346,7 @@ DEPENDENCIES
babosa (~> 1.0.2) babosa (~> 1.0.2)
base32 (~> 0.3.0) base32 (~> 0.3.0)
batch-loader (~> 2.0.1) batch-loader (~> 2.0.1)
bcrypt (= 3.1.12) bcrypt (~> 3.1, >= 3.1.14)
bcrypt_pbkdf (~> 1.0) bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0) benchmark-ips (~> 2.3.0)
benchmark-memory (~> 0.1) benchmark-memory (~> 0.1)

View file

@ -2,30 +2,8 @@ import $ from 'jquery';
import { escape } from 'lodash'; import { escape } from 'lodash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Api from './api'; import Api from './api';
import axios from './lib/utils/axios_utils';
import { normalizeHeaders } from './lib/utils/common_utils';
import { loadCSSFile } from './lib/utils/css_utils'; import { loadCSSFile } from './lib/utils/css_utils';
import { select2AxiosTransport } from './lib/utils/select2_utils';
const fetchGroups = (params) => {
axios[params.type.toLowerCase()](params.url, {
params: params.data,
})
.then((res) => {
const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
params.success({
results,
pagination: {
more,
},
});
})
.catch(params.error);
};
const groupsSelect = () => { const groupsSelect = () => {
loadCSSFile(gon.select2_css_path) loadCSSFile(gon.select2_css_path)
@ -51,9 +29,7 @@ const groupsSelect = () => {
url: Api.buildUrl(groupsPath), url: Api.buildUrl(groupsPath),
dataType: 'json', dataType: 'json',
quietMillis: 250, quietMillis: 250,
transport(params) { transport: select2AxiosTransport,
fetchGroups(params);
},
data(search, page) { data(search, page) {
return { return {
search, search,
@ -63,8 +39,6 @@ const groupsSelect = () => {
}; };
}, },
results(data, page) { results(data, page) {
if (data.length) return { results: [] };
const groups = data.length ? data : data.results || []; const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false; const more = data.pagination ? data.pagination.more : false;
const results = groups.filter((group) => skipGroups.indexOf(group.id) === -1); const results = groups.filter((group) => skipGroups.indexOf(group.id) === -1);

View file

@ -5,6 +5,7 @@ import Autosave from './autosave';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import { loadCSSFile } from './lib/utils/css_utils'; import { loadCSSFile } from './lib/utils/css_utils';
import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility'; import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import { select2AxiosTransport } from './lib/utils/select2_utils';
import { queryToObject, objectToQuery } from './lib/utils/url_utility'; import { queryToObject, objectToQuery } from './lib/utils/url_utility';
import UsersSelect from './users_select'; import UsersSelect from './users_select';
import ZenMode from './zen_mode'; import ZenMode from './zen_mode';
@ -199,15 +200,16 @@ export default class IssuableForm {
search, search,
}; };
}, },
results(data) { results({ results }) {
return { return {
// `data` keys are translated so we can't just access them with a string based key // `data` keys are translated so we can't just access them with a string based key
results: data[Object.keys(data)[0]].map((name) => ({ results: results[Object.keys(results)[0]].map((name) => ({
id: name, id: name,
text: name, text: name,
})), })),
}; };
}, },
transport: select2AxiosTransport,
}, },
initSelection(el, callback) { initSelection(el, callback) {
const val = el.val(); const val = el.val();

View file

@ -0,0 +1,25 @@
import axios from './axios_utils';
import { normalizeHeaders, parseIntPagination } from './common_utils';
// This is used in the select2 config to replace jQuery.ajax with axios
export const select2AxiosTransport = (params) => {
axios({
method: params.type?.toLowerCase() || 'get',
url: params.url,
params: params.data,
})
.then((res) => {
const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const pagination = parseIntPagination(headers);
const more = pagination.nextPage > pagination.page;
params.success({
results,
pagination: {
more,
},
});
})
.catch(params.error);
};

View file

@ -96,6 +96,22 @@ class Environment < ApplicationRecord
end end
scope :for_id, -> (id) { where(id: id) } scope :for_id, -> (id) { where(id: id) }
scope :stopped_review_apps, -> (before, limit) do
stopped
.in_review_folder
.where("created_at < ?", before)
.order("created_at ASC")
.limit(limit)
end
scope :scheduled_for_deletion, -> do
where.not(auto_delete_at: nil)
end
scope :not_scheduled_for_deletion, -> do
where(auto_delete_at: nil)
end
enum tier: { enum tier: {
production: 0, production: 0,
staging: 1, staging: 1,
@ -147,6 +163,10 @@ class Environment < ApplicationRecord
self.state_machine.states.map(&:name) self.state_machine.states.map(&:name)
end end
def self.schedule_to_delete(at_time = 1.week.from_now)
update_all(auto_delete_at: at_time)
end
class << self class << self
## ##
# This method returns stop actions (jobs) for multiple environments within one # This method returns stop actions (jobs) for multiple environments within one

View file

@ -0,0 +1,102 @@
# frozen_string_literal: true
module Environments
class ScheduleToDeleteReviewAppsService < ::BaseService
include ::Gitlab::ExclusiveLeaseHelpers
EXCLUSIVE_LOCK_KEY_BASE = 'environments:delete_review_apps:lock'
LOCK_TIMEOUT = 2.minutes
def execute
if validation_error = validate
return validation_error
end
mark_deletable_environments
end
private
def key
"#{EXCLUSIVE_LOCK_KEY_BASE}:#{project.id}"
end
def dry_run?
return true if params[:dry_run].nil?
params[:dry_run]
end
def validate
return if can?(current_user, :destroy_environment, project)
Result.new(error_message: "You do not have permission to destroy environments in this project", status: :unauthorized)
end
def mark_deletable_environments
in_lock(key, ttl: LOCK_TIMEOUT, retries: 1) do
unsafe_mark_deletable_environments
end
rescue FailedToObtainLockError
Result.new(error_message: "Another process is already processing a delete request. Please retry later.", status: :conflict)
end
def unsafe_mark_deletable_environments
result = Result.new
environments = project.environments
.not_scheduled_for_deletion
.stopped_review_apps(params[:before], params[:limit])
# Check if the actor has write permission to a potentially-protected environment.
deletable, failed = *environments.partition { |env| current_user.can?(:destroy_environment, env) }
if deletable.any? && failed.empty?
mark_for_deletion(deletable) unless dry_run?
result.set_status(:ok)
result.set_scheduled_entries(deletable)
else
result.set_status(
:bad_request,
error_message: "Failed to authorize deletions for some or all of the environments. Ask someone with more permissions to delete the environments."
)
result.set_unprocessable_entries(failed)
end
result
end
def mark_for_deletion(deletable_environments)
Environment.for_id(deletable_environments).schedule_to_delete
end
class Result
attr_accessor :scheduled_entries, :unprocessable_entries, :error_message, :status
def initialize(scheduled_entries: [], unprocessable_entries: [], error_message: nil, status: nil)
self.scheduled_entries = scheduled_entries
self.unprocessable_entries = unprocessable_entries
self.error_message = error_message
self.status = status
end
def success?
status == :ok
end
def set_status(status, error_message: nil)
self.status = status
self.error_message = error_message
end
def set_scheduled_entries(entries)
self.scheduled_entries = entries
end
def set_unprocessable_entries(entries)
self.unprocessable_entries = entries
end
end
end
end

View file

@ -0,0 +1,5 @@
---
title: Add API endpoint for deleting stale review envs
merge_request: 52224
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Update Pages template examples to default branch
merge_request: 56298
author:
type: other

View file

@ -62,25 +62,6 @@ their account.
If an administrator [increases](#set-the-user-cap-number) or [removes](#remove-the-user-cap) the If an administrator [increases](#set-the-user-cap-number) or [removes](#remove-the-user-cap) the
user cap, the users in pending approval state are automatically approved in a background job. user cap, the users in pending approval state are automatically approved in a background job.
### Enable or disable User cap **(FREE SELF)**
User cap is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can opt to disable it.
To disable it:
```ruby
Feature.disable(:admin_new_user_signups_cap)
```
To enable it:
```ruby
Feature.enable(:admin_new_user_signups_cap)
```
### Set the user cap number ### Set the user cap number
1. Go to **Admin Area > Settings > General**. 1. Go to **Admin Area > Settings > General**.

View file

@ -75,6 +75,33 @@ module API
end end
end end
desc "Delete multiple stopped review apps" do
detail "Remove multiple stopped review environments older than a specific age"
success Entities::Environment
end
params do
optional :before, type: Time, desc: "The timestamp before which environments can be deleted. Defaults to 30 days ago.", default: -> { 30.days.ago }
optional :limit, type: Integer, desc: "Maximum number of environments to delete. Defaults to 100.", default: 100, values: 1..1000
optional :dry_run, type: Boolean, desc: "If set, perform a dry run where no actual deletions will be performed. Defaults to true.", default: true
end
delete ":id/environments/review_apps" do
authorize! :read_environment, user_project
result = ::Environments::ScheduleToDeleteReviewAppsService.new(user_project, current_user, params).execute
response = {
scheduled_entries: Entities::Environment.represent(result.scheduled_entries),
unprocessable_entries: Entities::Environment.represent(result.unprocessable_entries)
}
if result.success?
status result.status
present response, current_user: current_user
else
render_api_error!(response.merge!(message: result.error_message), result.status)
end
end
desc 'Deletes an existing environment' do desc 'Deletes an existing environment' do
detail 'This feature was introduced in GitLab 8.11.' detail 'This feature was introduced in GitLab 8.11.'
success Entities::Environment success Entities::Environment

View file

@ -11,5 +11,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -9,5 +9,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -13,5 +13,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -8,5 +8,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -11,5 +11,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -13,5 +13,5 @@ pages:
paths: paths:
- node_modules - node_modules
key: project key: project
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -10,7 +10,8 @@ test:
script: script:
- hugo - hugo
except: except:
- master variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pages: pages:
script: script:
@ -19,4 +20,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -11,7 +11,8 @@ test:
- pip install hyde - pip install hyde
- hyde gen - hyde gen
except: except:
- master variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pages: pages:
stage: deploy stage: deploy
@ -22,4 +23,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -18,7 +18,8 @@ test:
paths: paths:
- test - test
except: except:
- master variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pages: pages:
stage: deploy stage: deploy
@ -28,4 +29,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -33,5 +33,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -8,5 +8,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -12,5 +12,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -12,7 +12,8 @@ test:
- bundle install --path vendor - bundle install --path vendor
- bundle exec middleman build - bundle exec middleman build
except: except:
- master variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pages: pages:
script: script:
@ -23,5 +24,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -8,5 +8,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -11,5 +11,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -25,5 +25,5 @@ pages:
artifacts: artifacts:
paths: paths:
- public - public
only: rules:
- master - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -4666,6 +4666,9 @@ msgstr ""
msgid "Be careful. Renaming a project's repository can have unintended side effects." msgid "Be careful. Renaming a project's repository can have unintended side effects."
msgstr "" msgstr ""
msgid "Before this can be merged, a Jira issue must be linked in the title or description"
msgstr ""
msgid "Begin with the selected commit" msgid "Begin with the selected commit"
msgstr "" msgstr ""

View file

@ -32,6 +32,8 @@ FactoryBot.define do
end end
trait :with_review_app do |environment| trait :with_review_app do |environment|
sequence(:name) { |n| "review/#{n}" }
transient do transient do
ref { 'master' } ref { 'master' }
end end

View file

@ -0,0 +1,100 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { setHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { select2AxiosTransport } from '~/lib/utils/select2_utils';
import 'select2/select2';
const TEST_URL = '/test/api/url';
const TEST_SEARCH_DATA = { extraSearch: 'test' };
const TEST_DATA = [{ id: 1 }];
const TEST_SEARCH = 'FOO';
describe('lib/utils/select2_utils', () => {
let mock;
let resultsSpy;
beforeEach(() => {
setHTMLFixture('<div><input id="root" /></div>');
mock = new MockAdapter(axios);
resultsSpy = jest.fn().mockReturnValue({ results: [] });
});
afterEach(() => {
mock.restore();
});
const setupSelect2 = (input) => {
input.select2({
ajax: {
url: TEST_URL,
quietMillis: 250,
transport: select2AxiosTransport,
data(search, page) {
return {
search,
page,
...TEST_SEARCH_DATA,
};
},
results: resultsSpy,
},
});
};
const setupSelect2AndSearch = async () => {
const $input = $('#root');
setupSelect2($input);
$input.select2('search', TEST_SEARCH);
jest.runOnlyPendingTimers();
await waitForPromises();
};
describe('select2AxiosTransport', () => {
it('uses axios to make request', async () => {
// setup mock response
const replySpy = jest.fn();
mock.onGet(TEST_URL).reply((...args) => replySpy(...args));
await setupSelect2AndSearch();
expect(replySpy).toHaveBeenCalledWith(
expect.objectContaining({
url: TEST_URL,
method: 'get',
params: {
page: 1,
search: TEST_SEARCH,
...TEST_SEARCH_DATA,
},
}),
);
});
it.each`
headers | pagination
${{}} | ${{ more: false }}
${{ 'X-PAGE': '1', 'x-next-page': 2 }} | ${{ more: true }}
`(
'passes results and pagination to results callback, with headers=$headers',
async ({ headers, pagination }) => {
mock.onGet(TEST_URL).reply(200, TEST_DATA, headers);
await setupSelect2AndSearch();
expect(resultsSpy).toHaveBeenCalledWith(
{ results: TEST_DATA, pagination },
1,
expect.anything(),
);
},
);
});
});

View file

@ -84,6 +84,62 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end end
end end
describe ".stopped_review_apps" do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:old_stopped_review_env) { create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project) }
let_it_be(:new_stopped_review_env) { create(:environment, :with_review_app, :stopped, project: project) }
let_it_be(:old_active_review_env) { create(:environment, :with_review_app, :available, created_at: 31.days.ago, project: project) }
let_it_be(:old_stopped_other_env) { create(:environment, :stopped, created_at: 31.days.ago, project: project) }
let_it_be(:new_stopped_other_env) { create(:environment, :stopped, project: project) }
let_it_be(:old_active_other_env) { create(:environment, :available, created_at: 31.days.ago, project: project) }
let(:before) { 30.days.ago }
let(:limit) { 1000 }
subject { project.environments.stopped_review_apps(before, limit) } # rubocop: disable RSpec/SingleLineHook
it { is_expected.to contain_exactly(old_stopped_review_env) }
context "current timestamp" do
let(:before) { Time.zone.now }
it { is_expected.to contain_exactly(old_stopped_review_env, new_stopped_review_env) }
end
end
describe "scheduled deletion" do
let_it_be(:deletable_environment) { create(:environment, auto_delete_at: Time.zone.now) }
let_it_be(:undeletable_environment) { create(:environment, auto_delete_at: nil) }
describe ".scheduled_for_deletion" do
subject { described_class.scheduled_for_deletion }
it { is_expected.to contain_exactly(deletable_environment) }
end
describe ".not_scheduled_for_deletion" do
subject { described_class.not_scheduled_for_deletion }
it { is_expected.to contain_exactly(undeletable_environment) }
end
describe ".schedule_to_delete" do
subject { described_class.for_id(deletable_environment).schedule_to_delete }
it "schedules the record for deletion" do
freeze_time do
subject
deletable_environment.reload
undeletable_environment.reload
expect(deletable_environment.auto_delete_at).to eq(1.week.from_now)
expect(undeletable_environment.auto_delete_at).to be_nil
end
end
end
end
describe 'state machine' do describe 'state machine' do
it 'invalidates the cache after a change' do it 'invalidates the cache after a change' do
expect(environment).to receive(:expire_etag_cache) expect(environment).to receive(:expire_etag_cache)

View file

@ -265,4 +265,76 @@ RSpec.describe API::Environments do
end end
end end
end end
describe "DELETE /projects/:id/environments/review_apps" do
shared_examples "delete stopped review environments" do
around do |example|
freeze_time { example.run }
end
it "deletes the old stopped review apps" do
old_stopped_review_env = create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project)
new_stopped_review_env = create(:environment, :with_review_app, :stopped, project: project)
old_active_review_env = create(:environment, :with_review_app, :available, created_at: 31.days.ago, project: project)
old_stopped_other_env = create(:environment, :stopped, created_at: 31.days.ago, project: project)
new_stopped_other_env = create(:environment, :stopped, project: project)
old_active_other_env = create(:environment, :available, created_at: 31.days.ago, project: project)
delete api("/projects/#{project.id}/environments/review_apps", current_user), params: { dry_run: false }
project.environments.reload
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["scheduled_entries"].size).to eq(1)
expect(json_response["scheduled_entries"].first["id"]).to eq(old_stopped_review_env.id)
expect(json_response["unprocessable_entries"].size).to eq(0)
expect(old_stopped_review_env.reload.auto_delete_at).to eq(1.week.from_now)
expect(new_stopped_review_env.reload.auto_delete_at).to be_nil
expect(old_active_review_env.reload.auto_delete_at).to be_nil
expect(old_stopped_other_env.reload.auto_delete_at).to be_nil
expect(new_stopped_other_env.reload.auto_delete_at).to be_nil
expect(old_active_other_env.reload.auto_delete_at).to be_nil
end
end
context "as a maintainer" do
it_behaves_like "delete stopped review environments" do
let(:current_user) { user }
end
end
context "as a developer" do
let(:developer) { create(:user) }
before do
project.add_developer(developer)
end
it_behaves_like "delete stopped review environments" do
let(:current_user) { developer }
end
end
context "as a reporter" do
let(:reporter) { create(:user) }
before do
project.add_reporter(reporter)
end
it "rejects the request" do
delete api("/projects/#{project.id}/environments/review_apps", reporter)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context "as a non member" do
it "rejects the request" do
delete api("/projects/#{project.id}/environments/review_apps", non_member)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end end

View file

@ -0,0 +1,136 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Environments::ScheduleToDeleteReviewAppsService do
include ExclusiveLeaseHelpers
let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:project) { create(:project, :private, :repository, namespace: maintainer.namespace) }
let(:service) { described_class.new(project, current_user, before: 30.days.ago, dry_run: dry_run) }
let(:dry_run) { false }
let(:current_user) { maintainer }
before do
project.add_maintainer(maintainer)
project.add_developer(developer)
project.add_reporter(reporter)
end
describe "#execute" do
subject { service.execute }
shared_examples "can schedule for deletion" do
let!(:old_stopped_review_env) { create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project) }
let!(:new_stopped_review_env) { create(:environment, :with_review_app, :stopped, project: project) }
let!(:old_active_review_env) { create(:environment, :with_review_app, :available, created_at: 31.days.ago, project: project) }
let!(:old_stopped_other_env) { create(:environment, :stopped, created_at: 31.days.ago, project: project) }
let!(:new_stopped_other_env) { create(:environment, :stopped, project: project) }
let!(:old_active_other_env) { create(:environment, :available, created_at: 31.days.ago, project: project) }
let!(:already_deleting_env) { create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project, auto_delete_at: 1.day.from_now) }
let(:already_deleting_time) { already_deleting_env.reload.auto_delete_at }
context "live run" do
let(:dry_run) { false }
around do |example|
freeze_time { example.run }
end
it "marks the correct environment as scheduled_entries" do
expect(subject.success?).to be_truthy
expect(subject.scheduled_entries).to contain_exactly(old_stopped_review_env)
expect(subject.unprocessable_entries).to be_empty
old_stopped_review_env.reload
new_stopped_review_env.reload
old_active_review_env.reload
old_stopped_other_env.reload
new_stopped_other_env.reload
old_active_other_env.reload
already_deleting_env.reload
expect(old_stopped_review_env.auto_delete_at).to eq(1.week.from_now)
expect(new_stopped_review_env.auto_delete_at).to be_nil
expect(old_active_review_env.auto_delete_at).to be_nil
expect(old_stopped_other_env.auto_delete_at).to be_nil
expect(new_stopped_other_env.auto_delete_at).to be_nil
expect(old_active_other_env.auto_delete_at).to be_nil
expect(already_deleting_env.auto_delete_at).to eq(already_deleting_time)
end
end
context "dry run" do
let(:dry_run) { true }
it "returns the same but doesn't update the record" do
expect(subject.success?).to be_truthy
expect(subject.scheduled_entries).to contain_exactly(old_stopped_review_env)
expect(subject.unprocessable_entries).to be_empty
old_stopped_review_env.reload
new_stopped_review_env.reload
old_active_review_env.reload
old_stopped_other_env.reload
new_stopped_other_env.reload
old_active_other_env.reload
already_deleting_env.reload
expect(old_stopped_review_env.auto_delete_at).to be_nil
expect(new_stopped_review_env.auto_delete_at).to be_nil
expect(old_active_review_env.auto_delete_at).to be_nil
expect(old_stopped_other_env.auto_delete_at).to be_nil
expect(new_stopped_other_env.auto_delete_at).to be_nil
expect(old_active_other_env.auto_delete_at).to be_nil
expect(already_deleting_env.auto_delete_at).to eq(already_deleting_time)
end
end
describe "execution in parallel" do
before do
stub_exclusive_lease_taken(service.send(:key))
end
it "does not execute unsafe_mark_scheduled_entries_environments" do
expect(service).not_to receive(:unsafe_mark_scheduled_entries_environments)
expect(subject.success?).to be_falsey
expect(subject.status).to eq(:conflict)
end
end
end
context "as a maintainer" do
let(:current_user) { maintainer }
it_behaves_like "can schedule for deletion"
end
context "as a developer" do
let(:current_user) { developer }
it_behaves_like "can schedule for deletion"
end
context "as a reporter" do
let(:current_user) { reporter }
it "fails to delete environments" do
old_stopped_review_env = create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project)
expect(subject.success?).to be_falsey
# Both of these should be empty as we fail before testing them
expect(subject.scheduled_entries).to be_empty
expect(subject.unprocessable_entries).to be_empty
old_stopped_review_env.reload
expect(old_stopped_review_env.auto_delete_at).to be_nil
end
end
end
end