Commit 69c8d258 authored by Matija Čupić's avatar Matija Čupić Committed by Kamil Trzciński

Create CI subscriptions migration

Adds a migration that creates the ci_subscriptions_projects table.
parent 197d88f6
# 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 ...@@ -924,6 +924,13 @@ ActiveRecord::Schema.define(version: 2019_11_12_221821) do
t.index ["project_id"], name: "index_ci_stages_on_project_id" t.index ["project_id"], name: "index_ci_stages_on_project_id"
end 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| create_table "ci_trigger_requests", id: :serial, force: :cascade do |t|
t.integer "trigger_id", null: false t.integer "trigger_id", null: false
t.text "variables" t.text "variables"
...@@ -4190,6 +4197,8 @@ ActiveRecord::Schema.define(version: 2019_11_12_221821) do ...@@ -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_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", "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_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_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", "projects", name: "fk_e3e63f966e", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", 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 ...@@ -93,6 +93,11 @@ module EE
has_many :project_aliases 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 :with_shared_runners_limit_enabled, -> { with_shared_runners.non_public_only }
scope :mirror, -> { where(mirror: true) } scope :mirror, -> { where(mirror: true) }
......
...@@ -96,6 +96,7 @@ class License < ApplicationRecord ...@@ -96,6 +96,7 @@ class License < ApplicationRecord
smartcard_auth smartcard_auth
type_of_work_analytics type_of_work_analytics
unprotection_restrictions unprotection_restrictions
ci_project_subscriptions
] ]
EEP_FEATURES.freeze EEP_FEATURES.freeze
......
...@@ -49,6 +49,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -49,6 +49,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get '(*ref)', action: 'show', as: '', constraints: { ref: Gitlab::PathRegex.git_reference_regex } get '(*ref)', action: 'show', as: '', constraints: { ref: Gitlab::PathRegex.git_reference_regex }
end end
end end
resources :subscriptions, only: [:create, :destroy]
end end
# End of the /-/ scope. # 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 ...@@ -38,6 +38,10 @@ describe Project do
it { is_expected.to have_many(:approver_groups).dependent(:destroy) } 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(:packages).class_name('Packages::Package') }
it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile') } 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_one(:github_service) }
it { is_expected.to have_many(:project_aliases) } it { is_expected.to have_many(:project_aliases) }
......
...@@ -16403,6 +16403,15 @@ msgstr "" ...@@ -16403,6 +16403,15 @@ msgstr ""
msgid "Subscription" msgid "Subscription"
msgstr "" msgstr ""
msgid "Subscription deletion failed."
msgstr ""
msgid "Subscription successfully created."
msgstr ""
msgid "Subscription successfully deleted."
msgstr ""
msgid "SubscriptionTable|Billing" msgid "SubscriptionTable|Billing"
msgstr "" msgstr ""
...@@ -17557,6 +17566,9 @@ 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." 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 "" msgstr ""
msgid "This project path either does not exist or is private."
msgstr ""
msgid "This repository" msgid "This repository"
msgstr "" msgstr ""
......
...@@ -426,6 +426,10 @@ project: ...@@ -426,6 +426,10 @@ project:
- grafana_integration - grafana_integration
- remove_source_branch_after_merge - remove_source_branch_after_merge
- deleting_user - deleting_user
- upstream_projects
- downstream_projects
- upstream_project_subscriptions
- downstream_project_subscriptions
award_emoji: award_emoji:
- awardable - awardable
- user - 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