Commit 16363f28 authored by Stan Hu's avatar Stan Hu

Merge branch '118844-one-button-review-app-be' into 'master'

One-button review app creation (backend)

See merge request gitlab-org/gitlab!22085
parents a55cfdcb 3a871bb3
...@@ -23,6 +23,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -23,6 +23,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def index def index
@environments = project.environments @environments = project.environments
.with_state(params[:scope] || :available) .with_state(params[:scope] || :available)
@project = ProjectPresenter.new(project, current_user: current_user)
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -31,6 +32,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -31,6 +32,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
render json: { render json: {
environments: serialize_environments(request, response, params[:nested]), environments: serialize_environments(request, response, params[:nested]),
review_app: serialize_review_app,
available_count: project.environments.available.count, available_count: project.environments.available.count,
stopped_count: project.environments.stopped.count stopped_count: project.environments.stopped.count
} }
...@@ -242,6 +244,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -242,6 +244,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
.represent(@environments) .represent(@environments)
end end
def serialize_review_app
ReviewAppSetupSerializer.new(current_user: @current_user).represent(@project)
end
def authorize_stop_environment! def authorize_stop_environment!
access_denied! unless can?(current_user, :stop_environment, environment) access_denied! unless can?(current_user, :stop_environment, environment)
end end
......
...@@ -276,8 +276,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -276,8 +276,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def kubernetes_cluster_anchor_data def kubernetes_cluster_anchor_data
if current_user && can?(current_user, :create_cluster, project) if can_instantiate_cluster?
if clusters.empty? if clusters.empty?
AnchorData.new(false, AnchorData.new(false,
statistic_icon + _('Add Kubernetes cluster'), statistic_icon + _('Add Kubernetes cluster'),
...@@ -294,7 +293,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -294,7 +293,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def gitlab_ci_anchor_data def gitlab_ci_anchor_data
if current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled? if cicd_missing?
AnchorData.new(false, AnchorData.new(false,
statistic_icon + _('Set up CI/CD'), statistic_icon + _('Set up CI/CD'),
add_ci_yml_path) add_ci_yml_path)
...@@ -326,8 +325,28 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -326,8 +325,28 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
count_of_extra_topics_not_shown > 0 count_of_extra_topics_not_shown > 0
end end
def can_setup_review_app?
strong_memoize(:can_setup_review_app) do
(can_instantiate_cluster? && all_clusters_empty?) || cicd_missing?
end
end
def all_clusters_empty?
strong_memoize(:all_clusters_empty) do
project.all_clusters.empty?
end
end
private private
def cicd_missing?
current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled?
end
def can_instantiate_cluster?
current_user && can?(current_user, :create_cluster, project)
end
def filename_path(filename) def filename_path(filename)
if blob = repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend if blob = repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend
project_blob_path( project_blob_path(
......
# frozen_string_literal: true
class ReviewAppSetupEntity < Grape::Entity
include RequestAwareEntity
expose :can_setup_review_app?, as: :can_setup_review_app
expose :all_clusters_empty?, as: :all_clusters_empty, if: -> (_, _) { project.can_setup_review_app? } do |project|
project.all_clusters_empty?
end
expose :review_snippet, if: -> (_, _) { project.can_setup_review_app? } do |_|
YAML.safe_load(File.read(Rails.root.join('lib', 'gitlab', 'ci', 'snippets', 'review_app_default.yml'))).to_s
end
private
def current_user
request.current_user
end
def project
object
end
end
# frozen_string_literal: true
class ReviewAppSetupSerializer < BaseSerializer
entity ReviewAppSetupEntity
end
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
"minItems": 1, "minItems": 1,
"type": "array" "type": "array"
}, },
"review_app": { "$ref": "review_app.json" },
"available_count": { "available_count": {
"type": "integer" "type": "integer"
}, },
......
{
"type": "object",
"required": ["can_setup_review_app"],
"properties": {
"can_setup_review_app": { "type": "boolean" },
"all_clusters_empty": { "type": "boolean" },
"review_snippet": { "type": "string "}
},
"additionalProperties": false
}
deploy_review:
stage: deploy
script:
- echo "Deploy a review app"
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
only:
- branches
...@@ -4,11 +4,10 @@ require 'spec_helper' ...@@ -4,11 +4,10 @@ require 'spec_helper'
describe ProjectPresenter do describe ProjectPresenter do
let(:user) { create(:user) } let(:user) { create(:user) }
describe '#license_short_name' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:presenter) { described_class.new(project, current_user: user) } let(:presenter) { described_class.new(project, current_user: user) }
describe '#license_short_name' do
context 'when project.repository has a license_key' do context 'when project.repository has a license_key' do
it 'returns the nickname of the license if present' do it 'returns the nickname of the license if present' do
allow(project.repository).to receive(:license_key).and_return('agpl-3.0') allow(project.repository).to receive(:license_key).and_return('agpl-3.0')
...@@ -33,8 +32,6 @@ describe ProjectPresenter do ...@@ -33,8 +32,6 @@ describe ProjectPresenter do
end end
describe '#default_view' do describe '#default_view' do
let(:presenter) { described_class.new(project, current_user: user) }
context 'user not signed in' do context 'user not signed in' do
let(:user) { nil } let(:user) { nil }
...@@ -125,7 +122,6 @@ describe ProjectPresenter do ...@@ -125,7 +122,6 @@ describe ProjectPresenter do
describe '#can_current_user_push_code?' do describe '#can_current_user_push_code?' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:presenter) { described_class.new(project, current_user: user) }
context 'empty repo' do context 'empty repo' do
let(:project) { create(:project) } let(:project) { create(:project) }
...@@ -163,7 +159,6 @@ describe ProjectPresenter do ...@@ -163,7 +159,6 @@ describe ProjectPresenter do
context 'statistics anchors (empty repo)' do context 'statistics anchors (empty repo)' do
let(:project) { create(:project, :empty_repo) } let(:project) { create(:project, :empty_repo) }
let(:presenter) { described_class.new(project, current_user: user) }
describe '#files_anchor_data' do describe '#files_anchor_data' do
it 'returns files data' do it 'returns files data' do
...@@ -200,7 +195,6 @@ describe ProjectPresenter do ...@@ -200,7 +195,6 @@ describe ProjectPresenter do
context 'statistics anchors' do context 'statistics anchors' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:presenter) { described_class.new(project, current_user: user) }
describe '#files_anchor_data' do describe '#files_anchor_data' do
it 'returns files data' do it 'returns files data' do
...@@ -416,7 +410,6 @@ describe ProjectPresenter do ...@@ -416,7 +410,6 @@ describe ProjectPresenter do
describe '#statistics_buttons' do describe '#statistics_buttons' do
let(:project) { build(:project) } let(:project) { build(:project) }
let(:presenter) { described_class.new(project, current_user: user) }
it 'orders the items correctly' do it 'orders the items correctly' do
allow(project.repository).to receive(:readme).and_return(double(name: 'readme')) allow(project.repository).to receive(:readme).and_return(double(name: 'readme'))
...@@ -435,8 +428,6 @@ describe ProjectPresenter do ...@@ -435,8 +428,6 @@ describe ProjectPresenter do
end end
describe '#repo_statistics_buttons' do describe '#repo_statistics_buttons' do
let(:presenter) { described_class.new(project, current_user: user) }
subject(:empty_repo_statistics_buttons) { presenter.empty_repo_statistics_buttons } subject(:empty_repo_statistics_buttons) { presenter.empty_repo_statistics_buttons }
before do before do
...@@ -485,4 +476,73 @@ describe ProjectPresenter do ...@@ -485,4 +476,73 @@ describe ProjectPresenter do
end end
end end
end end
describe '#can_setup_review_app?' do
subject { presenter.can_setup_review_app? }
context 'when the ci/cd file is missing' do
before do
allow(presenter).to receive(:cicd_missing?).and_return(true)
end
it { is_expected.to be_truthy }
end
context 'when the ci/cd file is not missing' do
before do
allow(presenter).to receive(:cicd_missing?).and_return(false)
end
context 'and the user can create a cluster' do
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user, :create_cluster, project).and_return(true)
end
context 'and there is no cluster associated to this project' do
let(:project) { create(:project, clusters: []) }
it { is_expected.to be_truthy }
end
context 'and there is already a cluster associated to this project' do
let(:project) { create(:project, clusters: [build(:cluster, :providing_by_gcp)]) }
it { is_expected.to be_falsey }
end
context 'when a group cluster is instantiated' do
let_it_be(:cluster) { create(:cluster, :group) }
let_it_be(:group) { cluster.group }
context 'and the project belongs to this group' do
let!(:project) { create(:project, group: group) }
it { is_expected.to be_falsey }
end
context 'and the project does not belong to this group' do
it { is_expected.to be_truthy }
end
end
context 'and there is already an instance cluster' do
it 'is false' do
create(:cluster, :instance)
is_expected.to be_falsey
end
end
end
context 'and the user cannot create a cluster' do
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user, :create_cluster, project).and_return(false)
end
it { is_expected.to be_falsey }
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe ReviewAppSetupEntity do
let_it_be(:user) { create(:admin) }
let(:project) { create(:project) }
let(:presenter) { ProjectPresenter.new(project, current_user: user) }
let(:entity) { described_class.new(presenter) }
let(:request) { double('request') }
before do
allow(request).to receive(:current_user).and_return(user)
allow(request).to receive(:project).and_return(project)
end
subject { entity.as_json }
describe '#as_json' do
it 'contains can_setup_review_app' do
expect(subject).to include(:can_setup_review_app)
end
context 'when the user can setup a review app' do
before do
allow(presenter).to receive(:can_setup_review_app?).and_return(true)
end
it 'contains relevant fields' do
expect(subject.keys).to include(:all_clusters_empty, :review_snippet)
end
it 'exposes the relevant review snippet' do
review_app_snippet = YAML.safe_load(File.read(Rails.root.join('lib', 'gitlab', 'ci', 'snippets', 'review_app_default.yml'))).to_s
expect(subject[:review_snippet]).to eq(review_app_snippet)
end
it 'exposes whether the project has associated clusters' do
expect(subject[:all_clusters_empty]).to be_truthy
end
end
context 'when the user cannot setup a review app' do
before do
allow(presenter).to receive(:can_setup_review_app?).and_return(false)
end
it 'does not expose certain fields' do
expect(subject.keys).not_to include(:all_clusters_empty, :review_snippet)
end
end
end
end
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