Commit e581bf7d authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'mc/feature/project-subscriptions' into 'master'

Implement Project subscriptions

See merge request gitlab-org/gitlab!18678
parents 197d88f6 69c8d258
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CreateCiSubscriptionsProjects < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
create_table :ci_subscriptions_projects do |t|
t.references :downstream_project, null: false, index: false, foreign_key: { to_table: :projects, on_delete: :cascade }
t.references :upstream_project, null: false, foreign_key: { to_table: :projects, on_delete: :cascade }
end
add_index :ci_subscriptions_projects, [:downstream_project_id, :upstream_project_id],
unique: true, name: 'index_ci_subscriptions_projects_unique_subscription'
end
end
......@@ -924,6 +924,13 @@ ActiveRecord::Schema.define(version: 2019_11_12_221821) do
t.index ["project_id"], name: "index_ci_stages_on_project_id"
end
create_table "ci_subscriptions_projects", force: :cascade do |t|
t.bigint "downstream_project_id", null: false
t.bigint "upstream_project_id", null: false
t.index ["downstream_project_id", "upstream_project_id"], name: "index_ci_subscriptions_projects_unique_subscription", unique: true
t.index ["upstream_project_id"], name: "index_ci_subscriptions_projects_on_upstream_project_id"
end
create_table "ci_trigger_requests", id: :serial, force: :cascade do |t|
t.integer "trigger_id", null: false
t.text "variables"
......@@ -4190,6 +4197,8 @@ ActiveRecord::Schema.define(version: 2019_11_12_221821) do
add_foreign_key "ci_sources_pipelines", "projects", name: "fk_1e53c97c0a", on_delete: :cascade
add_foreign_key "ci_stages", "ci_pipelines", column: "pipeline_id", name: "fk_fb57e6cc56", on_delete: :cascade
add_foreign_key "ci_stages", "projects", name: "fk_2360681d1d", on_delete: :cascade
add_foreign_key "ci_subscriptions_projects", "projects", column: "downstream_project_id", on_delete: :cascade
add_foreign_key "ci_subscriptions_projects", "projects", column: "upstream_project_id", on_delete: :cascade
add_foreign_key "ci_trigger_requests", "ci_triggers", column: "trigger_id", name: "fk_b8ec8b7245", on_delete: :cascade
add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
......
# frozen_string_literal: true
class Projects::SubscriptionsController < Projects::ApplicationController
include ::Gitlab::Utils::StrongMemoize
before_action :authorize_admin_project!
before_action :authorize_read_upstream_project!, only: [:create]
before_action :feature_ci_project_subscriptions!
def create
subscription = project.upstream_project_subscriptions.create(upstream_project: upstream_project)
flash[:notice] = if subscription.persisted?
_('Subscription successfully created.')
else
_('This project path either does not exist or is private.')
end
redirect_to project_settings_ci_cd_path(project)
end
def destroy
flash[:notice] = if project_subscription&.destroy
_('Subscription successfully deleted.')
else
_('Subscription deletion failed.')
end
redirect_to project_settings_ci_cd_path(project), status: :found
end
private
def upstream_project
strong_memoize(:upstream_project) do
Project.find_by_full_path(params[:upstream_project_path])
end
end
def project_subscription
project.upstream_project_subscriptions.find(params[:id])
end
def authorize_read_upstream_project!
render_404 unless can?(current_user, :read_project, upstream_project)
end
def feature_ci_project_subscriptions!
render_404 unless project.feature_available?(:ci_project_subscriptions)
end
end
# frozen_string_literal: true
module Ci
module Subscriptions
class Project < ApplicationRecord
self.table_name = "ci_subscriptions_projects"
belongs_to :downstream_project, class_name: '::Project', optional: false
belongs_to :upstream_project, class_name: '::Project', optional: false
validates :upstream_project_id, uniqueness: { scope: :downstream_project_id }
validate do
errors.add(:upstream_project, 'needs to be public') unless upstream_public?
end
private
def upstream_public?
upstream_project&.public?
end
end
end
end
......@@ -93,6 +93,11 @@ module EE
has_many :project_aliases
has_many :upstream_project_subscriptions, class_name: 'Ci::Subscriptions::Project', foreign_key: :downstream_project_id, inverse_of: :downstream_project
has_many :upstream_projects, class_name: 'Project', through: :upstream_project_subscriptions, source: :upstream_project
has_many :downstream_project_subscriptions, class_name: 'Ci::Subscriptions::Project', foreign_key: :upstream_project_id, inverse_of: :upstream_project
has_many :downstream_projects, class_name: 'Project', through: :downstream_project_subscriptions, source: :downstream_project
scope :with_shared_runners_limit_enabled, -> { with_shared_runners.non_public_only }
scope :mirror, -> { where(mirror: true) }
......
......@@ -96,6 +96,7 @@ class License < ApplicationRecord
smartcard_auth
type_of_work_analytics
unprotection_restrictions
ci_project_subscriptions
]
EEP_FEATURES.freeze
......
......@@ -49,6 +49,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get '(*ref)', action: 'show', as: '', constraints: { ref: Gitlab::PathRegex.git_reference_regex }
end
end
resources :subscriptions, only: [:create, :destroy]
end
# End of the /-/ scope.
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::SubscriptionsController do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.add_developer(user)
sign_in(user)
end
describe 'POST create' do
subject(:post_create) { post :create, params: { namespace_id: project.namespace, project_id: project, upstream_project_path: upstream_project.full_path } }
let(:upstream_project) { create(:project, :public) }
context 'when user is authorized' do
before do
project.add_maintainer(user)
upstream_project.add_developer(user)
end
context 'when feature is available' do
before do
stub_licensed_features(ci_project_subscriptions: true)
end
context 'when project is public' do
it 'creates a new subscription' do
expect { post_create }.to change { project.upstream_project_subscriptions.count }.from(0).to(1)
end
it 'sets the flash' do
post_create
expect(response).to set_flash[:notice].to('Subscription successfully created.')
end
it 'redirects to ci_cd settings' do
post_create
expect(response).to redirect_to project_settings_ci_cd_path(project)
end
end
context 'when project is not public' do
before do
upstream_project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it 'does not create a new subscription' do
expect { post_create }.not_to change { project.upstream_project_subscriptions.count }.from(0)
end
it 'sets the flash' do
post_create
expect(response).to set_flash[:notice].to('This project path either does not exist or is private.')
end
it 'redirects to ci_cd settings' do
post_create
expect(response).to redirect_to project_settings_ci_cd_path(project)
end
end
end
context 'when feature is not available' do
before do
stub_licensed_features(ci_project_subscriptions: false)
end
it 'does not create a new subscription' do
expect { post_create }.not_to change { project.upstream_project_subscriptions.count }.from(0)
end
it 'renders a not found response' do
post_create
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when user is not authorized' do
it 'does not create a new subscription' do
expect { post_create }.not_to change { project.upstream_project_subscriptions.count }.from(0)
end
it 'renders a not found response' do
post_create
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'DELETE destroy' do
subject(:delete_destroy) { delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: subscription.id } }
let!(:subscription) { create(:ci_subscriptions_project, downstream_project: project) }
context 'when user is authorized' do
before do
project.add_maintainer(user)
end
context 'when feature is available' do
before do
stub_licensed_features(ci_project_subscriptions: true)
end
it 'destroys the subscription' do
delete_destroy
expect { subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'sets the flash' do
delete_destroy
expect(response).to set_flash[:notice].to('Subscription successfully deleted.')
end
it 'redirects to ci_cd settings' do
delete_destroy
expect(response).to redirect_to project_settings_ci_cd_path(project)
end
end
context 'when feature is not available' do
before do
stub_licensed_features(ci_project_subscriptions: false)
end
it 'does not destroy the subscription' do
delete_destroy
expect(subscription.reload).to be_persisted
end
it 'renders a not found reseponse' do
delete_destroy
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when user is not authorized' do
it 'does not destroy the subscription' do
delete_destroy
expect(subscription.reload).to be_persisted
end
it 'renders a not found response' do
delete_destroy
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :ci_subscriptions_project, class: Ci::Subscriptions::Project do
downstream_project factory: :project
upstream_project factory: [:project, :public]
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::Subscriptions::Project do
let!(:subscription) { create(:ci_subscriptions_project) }
describe 'Relations' do
it { is_expected.to belong_to(:downstream_project).required }
it { is_expected.to belong_to(:upstream_project).required }
end
describe 'Validations' do
it { is_expected.to validate_uniqueness_of(:upstream_project_id).scoped_to(:downstream_project_id) }
it 'validates that upstream project is public' do
subscription.upstream_project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
expect(subscription).not_to be_valid
end
end
end
......@@ -38,6 +38,10 @@ describe Project do
it { is_expected.to have_many(:approver_groups).dependent(:destroy) }
it { is_expected.to have_many(:packages).class_name('Packages::Package') }
it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile') }
it { is_expected.to have_many(:upstream_project_subscriptions) }
it { is_expected.to have_many(:upstream_projects) }
it { is_expected.to have_many(:downstream_project_subscriptions) }
it { is_expected.to have_many(:downstream_projects) }
it { is_expected.to have_one(:github_service) }
it { is_expected.to have_many(:project_aliases) }
......
......@@ -16403,6 +16403,15 @@ msgstr ""
msgid "Subscription"
msgstr ""
msgid "Subscription deletion failed."
msgstr ""
msgid "Subscription successfully created."
msgstr ""
msgid "Subscription successfully deleted."
msgstr ""
msgid "SubscriptionTable|Billing"
msgstr ""
......@@ -17557,6 +17566,9 @@ msgstr ""
msgid "This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target=\"_blank\" rel=\"noopener noreferrer\">enable billing <i class=\"fa fa-external-link\" aria-hidden=\"true\"></i></a> and try again."
msgstr ""
msgid "This project path either does not exist or is private."
msgstr ""
msgid "This repository"
msgstr ""
......
......@@ -426,6 +426,10 @@ project:
- grafana_integration
- remove_source_branch_after_merge
- deleting_user
- upstream_projects
- downstream_projects
- upstream_project_subscriptions
- downstream_project_subscriptions
award_emoji:
- awardable
- user
......
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