summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js15
-rw-r--r--app/assets/javascripts/issuable_index.js2
-rw-r--r--app/assets/stylesheets/pages/clusters.scss6
-rw-r--r--app/assets/stylesheets/pages/issues.scss7
-rw-r--r--app/assets/stylesheets/pages/projects.scss5
-rw-r--r--app/controllers/projects/clusters/gcp_controller.rb75
-rw-r--r--app/controllers/projects/clusters/user_controller.rb39
-rw-r--r--app/controllers/projects/clusters_controller.rb96
-rw-r--r--app/controllers/projects_controller.rb4
-rw-r--r--app/models/clusters/cluster.rb6
-rw-r--r--app/models/clusters/platforms/kubernetes.rb14
-rw-r--r--app/models/namespace.rb12
-rw-r--r--app/models/project.rb5
-rw-r--r--app/services/clusters/create_service.rb2
-rw-r--r--app/services/merge_requests/build_service.rb8
-rw-r--r--app/services/merge_requests/create_service.rb6
-rw-r--r--app/validators/cluster_name_validator.rb10
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml16
-rw-r--r--app/views/projects/_issuable_by_email.html.haml30
-rw-r--r--app/views/projects/clusters/_advanced_settings.html.haml13
-rw-r--r--app/views/projects/clusters/_banner.html.haml21
-rw-r--r--app/views/projects/clusters/_dropdown.html.haml12
-rw-r--r--app/views/projects/clusters/_enabled.html.haml16
-rw-r--r--app/views/projects/clusters/_form.html.haml35
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml32
-rw-r--r--app/views/projects/clusters/gcp/_header.html.haml (renamed from app/views/projects/clusters/_header.html.haml)4
-rw-r--r--app/views/projects/clusters/gcp/_show.html.haml40
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml (renamed from app/views/projects/clusters/login.html.haml)3
-rw-r--r--app/views/projects/clusters/gcp/new.html.haml10
-rw-r--r--app/views/projects/clusters/new.html.haml17
-rw-r--r--app/views/projects/clusters/new_gcp.html.haml10
-rw-r--r--app/views/projects/clusters/show.html.haml56
-rw-r--r--app/views/projects/clusters/user/_form.html.haml25
-rw-r--r--app/views/projects/clusters/user/_header.html.haml5
-rw-r--r--app/views/projects/clusters/user/_show.html.haml29
-rw-r--r--app/views/projects/clusters/user/new.html.haml11
-rw-r--r--app/views/projects/issues/_by_email_description.html.haml6
-rw-r--r--app/views/projects/issues/_issue_by_email.html.haml34
-rw-r--r--app/views/projects/issues/index.html.haml4
-rw-r--r--app/views/projects/merge_requests/_by_email_description.html.haml1
-rw-r--r--app/views/projects/merge_requests/index.html.haml3
-rw-r--r--app/views/shared/issuable/_nav.html.haml5
-rw-r--r--app/views/shared/issuable/nav_links/_all.html.haml6
-rw-r--r--app/workers/email_receiver_worker.rb3
-rw-r--r--changelogs/unreleased/32878-merge-request-from-email.yml5
-rw-r--r--changelogs/unreleased/35616-move-k8-to-cluster-page.yml5
-rw-r--r--changelogs/unreleased/bvl-limit-fork-queries-on-project-show.yml5
-rw-r--r--config/routes/project.rb14
-rw-r--r--doc/development/writing_documentation.md13
-rw-r--r--doc/user/project/merge_requests/index.md10
-rw-r--r--lib/gitlab/email/handler.rb2
-rw-r--r--lib/gitlab/email/handler/create_merge_request_handler.rb67
-rw-r--r--lib/gitlab/email/receiver.rb6
-rw-r--r--lib/gitlab/git/conflict/file.rb2
-rw-r--r--lib/gitlab/git/conflict/resolver.rb2
-rw-r--r--lib/gitlab/git/repository.rb148
-rw-r--r--spec/controllers/projects/clusters/gcp_controller_spec.rb185
-rw-r--r--spec/controllers/projects/clusters/user_controller_spec.rb87
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb319
-rw-r--r--spec/controllers/projects_controller_spec.rb58
-rw-r--r--spec/factories/clusters/clusters.rb (renamed from spec/factories/clusters/cluster.rb)15
-rw-r--r--spec/features/issues_spec.rb14
-rw-r--r--spec/features/projects/clusters/applications_spec.rb107
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb136
-rw-r--r--spec/features/projects/clusters/user_spec.rb101
-rw-r--r--spec/features/projects/clusters_spec.rb191
-rw-r--r--spec/fixtures/emails/valid_new_merge_request.eml18
-rw-r--r--spec/fixtures/emails/valid_new_merge_request_no_subject.eml18
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js16
-rw-r--r--spec/javascripts/issuable_spec.js2
-rw-r--r--spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb84
-rw-r--r--spec/lib/gitlab/email/handler_spec.rb17
-rw-r--r--spec/models/namespace_spec.rb10
-rw-r--r--spec/models/project_spec.rb16
-rw-r--r--spec/services/merge_requests/build_service_spec.rb1
-rw-r--r--spec/support/query_recorder.rb19
76 files changed, 1728 insertions, 724 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index dc443475952..cdb5c430aa9 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -48,6 +48,7 @@ export default class Clusters {
this.toggle = this.toggle.bind(this);
this.installApplication = this.installApplication.bind(this);
+ this.showToken = this.showToken.bind(this);
this.toggleButton = document.querySelector('.js-toggle-cluster');
this.toggleInput = document.querySelector('.js-toggle-input');
@@ -56,6 +57,8 @@ export default class Clusters {
this.creatingContainer = document.querySelector('.js-cluster-creating');
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
+ this.showTokenButton = document.querySelector('.js-show-cluster-token');
+ this.tokenField = document.querySelector('.js-cluster-token');
initSettingsPanels();
this.initApplications();
@@ -97,11 +100,13 @@ export default class Clusters {
addListeners() {
this.toggleButton.addEventListener('click', this.toggle);
+ if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
}
removeListeners() {
this.toggleButton.removeEventListener('click', this.toggle);
+ if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
}
@@ -149,6 +154,16 @@ export default class Clusters {
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
}
+ showToken() {
+ const type = this.tokenField.getAttribute('type');
+
+ if (type === 'password') {
+ this.tokenField.setAttribute('type', 'text');
+ } else {
+ this.tokenField.setAttribute('type', 'password');
+ }
+ }
+
hideAll() {
this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden');
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index 0b123a11a3b..c3e0acdff66 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -28,7 +28,7 @@ export default class IssuableIndex {
url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json',
success(response) {
- $('#issue_email').val(response.new_issue_address).focus();
+ $('#issuable_email').val(response.new_address).focus();
},
beforeSend() {
$('.incoming-email-token-reset').text('resetting...');
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index e5b9e1f2de6..83e211d6086 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -8,3 +8,9 @@
// Wait for the Vue to kick-in and render the applications block
min-height: 302px;
}
+
+.clusters-dropdown-menu {
+ max-width: 100%;
+}
+
+@include new-style-dropdown('.clusters-dropdown ');
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index d3dda2e7d25..8218326ae8f 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -164,12 +164,7 @@ ul.related-merge-requests > li {
}
}
-.issues-footer {
- padding-top: $gl-padding;
- padding-bottom: 37px;
-}
-
-.issue-email-modal-btn {
+.issuable-email-modal-btn {
padding: 0;
color: $gl-link-color;
background-color: transparent;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 4c1e6d46242..9345177a4dc 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -1210,3 +1210,8 @@ pre.light-well {
border-color: $border-color;
}
}
+
+.issuable-footer {
+ padding-top: $gl-padding;
+ padding-bottom: 37px;
+}
diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb
new file mode 100644
index 00000000000..b64f7a2a6bd
--- /dev/null
+++ b/app/controllers/projects/clusters/gcp_controller.rb
@@ -0,0 +1,75 @@
+class Projects::Clusters::GcpController < Projects::ApplicationController
+ before_action :authorize_read_cluster!
+ before_action :authorize_google_api, except: [:login]
+ before_action :authorize_create_cluster!, only: [:new, :create]
+
+ def login
+ begin
+ state = generate_session_key_redirect(gcp_new_namespace_project_clusters_path.to_s)
+
+ @authorize_url = GoogleApi::CloudPlatform::Client.new(
+ nil, callback_google_api_auth_url,
+ state: state).authorize_url
+ rescue GoogleApi::Auth::ConfigMissingError
+ # no-op
+ end
+ end
+
+ def new
+ @cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_provider_gcp
+ end
+ end
+
+ def create
+ @cluster = ::Clusters::CreateService
+ .new(project, current_user, create_params)
+ .execute(token_in_session)
+
+ if @cluster.persisted?
+ redirect_to project_cluster_path(project, @cluster)
+ else
+ render :new
+ end
+ end
+
+ private
+
+ def create_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type
+ ]).merge(
+ provider_type: :gcp,
+ platform_type: :kubernetes
+ )
+ end
+
+ def authorize_google_api
+ unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
+ .validate_token(expires_at_in_session)
+ redirect_to action: 'login'
+ end
+ end
+
+ def token_in_session
+ @token_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token]
+ end
+
+ def expires_at_in_session
+ @expires_at_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
+ end
+
+ def generate_session_key_redirect(uri)
+ GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
+ session[key] = uri
+ end
+ end
+end
diff --git a/app/controllers/projects/clusters/user_controller.rb b/app/controllers/projects/clusters/user_controller.rb
new file mode 100644
index 00000000000..d7678512073
--- /dev/null
+++ b/app/controllers/projects/clusters/user_controller.rb
@@ -0,0 +1,39 @@
+class Projects::Clusters::UserController < Projects::ApplicationController
+ before_action :authorize_read_cluster!
+ before_action :authorize_create_cluster!, only: [:new, :create]
+
+ def new
+ @cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_platform_kubernetes
+ end
+ end
+
+ def create
+ @cluster = ::Clusters::CreateService
+ .new(project, current_user, create_params)
+ .execute
+
+ if @cluster.persisted?
+ redirect_to project_cluster_path(project, @cluster)
+ else
+ render :new
+ end
+ end
+
+ private
+
+ def create_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ platform_kubernetes_attributes: [
+ :namespace,
+ :api_url,
+ :token,
+ :ca_cert
+ ]).merge(
+ provider_type: :user,
+ platform_type: :kubernetes
+ )
+ end
+end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 9a56c9de858..d18b6d4b78c 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -1,11 +1,12 @@
class Projects::ClustersController < Projects::ApplicationController
- before_action :cluster, except: [:login, :index, :new, :new_gcp, :create]
+ before_action :cluster, except: [:index, :new]
before_action :authorize_read_cluster!
- before_action :authorize_create_cluster!, only: [:new, :new_gcp, :create]
- before_action :authorize_google_api, only: [:new_gcp, :create]
+ before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
+ STATUS_POLLING_INTERVAL = 10_000
+
def index
if project.cluster
redirect_to project_cluster_path(project, project.cluster)
@@ -14,43 +15,13 @@ class Projects::ClustersController < Projects::ApplicationController
end
end
- def login
- begin
- state = generate_session_key_redirect(providers_gcp_new_namespace_project_clusters_url.to_s)
-
- @authorize_url = GoogleApi::CloudPlatform::Client.new(
- nil, callback_google_api_auth_url,
- state: state).authorize_url
- rescue GoogleApi::Auth::ConfigMissingError
- # no-op
- end
- end
-
def new
end
- def new_gcp
- @cluster = Clusters::Cluster.new.tap do |cluster|
- cluster.build_provider_gcp
- end
- end
-
- def create
- @cluster = Clusters::CreateService
- .new(project, current_user, create_params)
- .execute(token_in_session)
-
- if @cluster.persisted?
- redirect_to project_cluster_path(project, @cluster)
- else
- render :new_gcp
- end
- end
-
def status
respond_to do |format|
format.json do
- Gitlab::PollingInterval.set_header(response, interval: 10_000)
+ Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL)
render json: ClusterSerializer
.new(project: @project, current_user: @current_user)
@@ -88,46 +59,29 @@ class Projects::ClustersController < Projects::ApplicationController
private
def cluster
- @cluster ||= project.cluster.present(current_user: current_user)
- end
-
- def create_params
- params.require(:cluster).permit(
- :enabled,
- :name,
- :provider_type,
- provider_gcp_attributes: [
- :gcp_project_id,
- :zone,
- :num_nodes,
- :machine_type
- ])
+ @cluster ||= project.clusters.find(params[:id])
+ .present(current_user: current_user)
end
def update_params
- params.require(:cluster).permit(:enabled)
- end
-
- def authorize_google_api
- unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
- .validate_token(expires_at_in_session)
- redirect_to action: 'login'
- end
- end
-
- def token_in_session
- @token_in_session ||=
- session[GoogleApi::CloudPlatform::Client.session_key_for_token]
- end
-
- def expires_at_in_session
- @expires_at_in_session ||=
- session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
- end
-
- def generate_session_key_redirect(uri)
- GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
- session[key] = uri
+ if cluster.managed?
+ params.require(:cluster).permit(
+ :enabled,
+ platform_kubernetes_attributes: [
+ :namespace
+ ]
+ )
+ else
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ platform_kubernetes_attributes: [
+ :api_url,
+ :token,
+ :ca_cert,
+ :namespace
+ ]
+ )
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a784c6f402a..3882fa4791d 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -133,11 +133,11 @@ class ProjectsController < Projects::ApplicationController
redirect_to edit_project_path(@project), status: 302, alert: ex.message
end
- def new_issue_address
+ def new_issuable_address
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
current_user.reset_incoming_email_token!
- render json: { new_issue_address: @project.new_issue_address(current_user) }
+ render json: { new_address: @project.new_issuable_address(current_user, params[:issuable_type]) }
end
def archive
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 6d7fb4b7dbf..45beced1427 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -17,7 +17,7 @@ module Clusters
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
- has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes'
+ has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true
has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
@@ -70,6 +70,10 @@ module Clusters
return platform_kubernetes if kubernetes?
end
+ def managed?
+ !user?
+ end
+
def first_project
return @first_project if defined?(@first_project)
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 7ab670cf1ef..9160a169452 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -34,12 +34,15 @@ module Clusters
validates :api_url, url: true, presence: true
validates :token, presence: true
+ validate :prevent_modification, on: :update
+
after_save :clear_reactive_cache!
alias_attribute :ca_pem, :ca_cert
delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, to: :cluster, allow_nil: true
+ delegate :managed?, to: :cluster, allow_nil: true
alias_method :active?, :enabled?
@@ -173,6 +176,17 @@ module Clusters
def enforce_namespace_to_lower_case
self.namespace = self.namespace&.downcase
end
+
+ def prevent_modification
+ return unless managed?
+
+ if api_url_changed? || token_changed? || ca_pem_changed?
+ errors.add(:base, "cannot modify managed cluster")
+ return false
+ end
+
+ true
+ end
end
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index fa76729a702..901dbf2ba69 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -139,7 +139,17 @@ class Namespace < ActiveRecord::Base
def find_fork_of(project)
return nil unless project.fork_network
- project.fork_network.find_forks_in(projects).first
+ if RequestStore.active?
+ forks_in_namespace = RequestStore.fetch("namespaces:#{id}:forked_projects") do
+ Hash.new do |found_forks, project|
+ found_forks[project] = project.fork_network.find_forks_in(projects).first
+ end
+ end
+
+ forks_in_namespace[project]
+ else
+ project.fork_network.find_forks_in(projects).first
+ end
end
def lfs_enabled?
diff --git a/app/models/project.rb b/app/models/project.rb
index 0a7e7617d9b..cc530076bf7 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -752,13 +752,14 @@ class Project < ActiveRecord::Base
Gitlab::Routing.url_helpers.project_url(self)
end
- def new_issue_address(author)
+ def new_issuable_address(author, address_type)
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
author.ensure_incoming_email_token!
+ suffix = address_type == 'merge_request' ? '+merge-request' : ''
Gitlab::IncomingEmail.reply_address(
- "#{full_path}+#{author.incoming_email_token}")
+ "#{full_path}#{suffix}+#{author.incoming_email_token}")
end
def build_commit_note(commit)
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
index 1d407739b21..7b697f6d807 100644
--- a/app/services/clusters/create_service.rb
+++ b/app/services/clusters/create_service.rb
@@ -2,7 +2,7 @@ module Clusters
class CreateService < BaseService
attr_reader :access_token
- def execute(access_token)
+ def execute(access_token = nil)
@access_token = access_token
create_cluster.tap do |cluster|
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index c2fb01466df..9622a5c5462 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -10,8 +10,12 @@ module MergeRequests
merge_request.target_branch = find_target_branch
merge_request.can_be_created = branches_valid?
- compare_branches if branches_present?
- assign_title_and_description if merge_request.can_be_created
+ # compare branches only if branches are valid, otherwise
+ # compare_branches may raise an error
+ if merge_request.can_be_created
+ compare_branches
+ assign_title_and_description
+ end
merge_request
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 820709583fa..49cf534dc0d 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -35,6 +35,12 @@ module MergeRequests
super
end
+ # expose issuable create method so it can be called from email
+ # handler CreateMergeRequestHandler
+ def create(merge_request)
+ super
+ end
+
private
def update_merge_requests_head_pipeline(merge_request)
diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb
index 13ec342f399..e7d32550176 100644
--- a/app/validators/cluster_name_validator.rb
+++ b/app/validators/cluster_name_validator.rb
@@ -3,11 +3,7 @@
# Custom validator for ClusterName.
class ClusterNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- if record.user?
- unless value.present?
- record.errors.add(attribute, " has to be present")
- end
- elsif record.gcp?
+ if record.managed?
if record.persisted? && record.name_changed?
record.errors.add(attribute, " can not be changed because it's synchronized with provider")
end
@@ -19,6 +15,10 @@ class ClusterNameValidator < ActiveModel::EachValidator
unless value =~ Gitlab::Regex.kubernetes_namespace_regex
record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message)
end
+ else
+ unless value.present?
+ record.errors.add(attribute, " has to be present")
+ end
end
end
end
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 2ce960df13c..8b2d2a5c74d 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -146,7 +146,7 @@
= number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters]) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp]) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container
= sprite_icon('pipeline')
@@ -154,7 +154,7 @@
CI / CD
%ul.sidebar-sub-level-items
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
= link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name
#{ _('CI / CD') }
@@ -183,18 +183,18 @@
%span
Environments
+ - if project_nav_tab? :clusters
+ = nav_link(controller: [:clusters, :user, :gcp]) do
+ = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
+ %span
+ Cluster
+
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do
= link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
%span
Charts
- - if project_nav_tab? :clusters
- = nav_link(controller: :clusters) do
- = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
- %span
- Cluster
-
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml
new file mode 100644
index 00000000000..749e273b2e2
--- /dev/null
+++ b/app/views/projects/_issuable_by_email.html.haml
@@ -0,0 +1,30 @@
+- name = issuable_type == 'issue' ? 'issue' : 'merge request'
+
+.issuable-footer.text-center
+ %button.issuable-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issuable-email-modal" } }
+ Email a new #{name} to this project
+
+#issuable-email-modal.modal.fade{ tabindex: "-1", role: "dialog" }
+ .modal-dialog{ role: "document" }
+ .modal-content
+ .modal-header
+ %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
+ %span{ aria: { hidden: "true" } }= icon("times")
+ %h4.modal-title
+ Create new #{name} by email
+ .modal-body
+ %p
+ You can create a new #{name} inside this project by sending an email to the following email address:
+ .email-modal-input-group.input-group
+ = text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
+ .input-group-btn
+ = clipboard_button(target: '#issuable_email')
+ %p
+ = render 'by_email_description'
+ %p
+ This is a private email address, generated just for you.
+
+ Anyone who gets ahold of it can create issues or merge requests as if they were you.
+ You should
+ = link_to 'reset it', new_issuable_address_project_path(@project, issuable_type: issuable_type), class: 'incoming-email-token-reset'
+ if that ever happens.
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml
index 97532f1e2bd..2b3095eb94b 100644
--- a/app/views/projects/clusters/_advanced_settings.html.haml
+++ b/app/views/projects/clusters/_advanced_settings.html.haml
@@ -1,10 +1,11 @@
- if can?(current_user, :admin_cluster, @cluster)
- .append-bottom-20
- %label.append-bottom-10
- = s_('ClusterIntegration|Google Container Engine')
- %p
- - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
+ - if @cluster.managed?
+ .append-bottom-20
+ %label.append-bottom-10
+ = s_('ClusterIntegration|Google Container Engine')
+ %p
+ - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
.well.form-group
%label.text-danger
diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml
new file mode 100644
index 00000000000..a1cc66eac92
--- /dev/null
+++ b/app/views/projects/clusters/_banner.html.haml
@@ -0,0 +1,21 @@
+%h4= s_('ClusterIntegration|Enable cluster integration')
+.settings-content
+
+ .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
+ = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
+ %p.js-error-reason
+
+ .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
+ = s_('ClusterIntegration|Cluster is being created on Google Container Engine...')
+
+ .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
+ = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine. Refresh the page to see cluster\'s details')
+
+ %p
+ - if @cluster.enabled?
+ - if can?(current_user, :update_cluster, @cluster)
+ = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
+ - else
+ = s_('ClusterIntegration|Cluster integration is enabled for this project.')
+ - else
+ = s_('ClusterIntegration|Cluster integration is disabled for this project.')
diff --git a/app/views/projects/clusters/_dropdown.html.haml b/app/views/projects/clusters/_dropdown.html.haml
new file mode 100644
index 00000000000..39188c7ca27
--- /dev/null
+++ b/app/views/projects/clusters/_dropdown.html.haml
@@ -0,0 +1,12 @@
+%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
+
+.dropdown.clusters-dropdown
+ %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
+ %span.dropdown-toggle-text
+ = dropdown_text
+ = icon('chevron-down')
+ %ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width
+ %li
+ = link_to(s_('ClusterIntegration|Create cluster on Google Container Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
+ %li
+ = link_to(s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
diff --git a/app/views/projects/clusters/_enabled.html.haml b/app/views/projects/clusters/_enabled.html.haml
new file mode 100644
index 00000000000..f4d261df8f5
--- /dev/null
+++ b/app/views/projects/clusters/_enabled.html.haml
@@ -0,0 +1,16 @@
+= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+ = form_errors(@cluster)
+ .form-group.append-bottom-20
+ %label.append-bottom-10
+ = field.hidden_field :enabled, { class: 'js-toggle-input'}
+
+ %button{ type: 'button',
+ class: "js-toggle-cluster project-feature-toggle #{'checked' unless !@cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, @cluster)}",
+ 'aria-label': s_('ClusterIntegration|Toggle Cluster'),
+ disabled: !can?(current_user, :update_cluster, @cluster),
+ data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled' } }
+
+ - if can?(current_user, :update_cluster, @cluster)
+ .form-group
+ = field.submit _('Save'), class: 'btn btn-success'
+
diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml
deleted file mode 100644
index 1f8ae463d0f..00000000000
--- a/app/views/projects/clusters/_form.html.haml
+++ /dev/null
@@ -1,35 +0,0 @@
-.row
- .col-sm-8.col-sm-offset-4
- %p
- - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
-
- = form_for @cluster, url: namespace_project_clusters_path(@project.namespace, @project, @cluster), as: :cluster do |field|
- = field.hidden_field :provider_type, value: :gcp
- = form_errors(@cluster)
- .form-group
- = field.label :name, s_('ClusterIntegration|Cluster name')
- = field.text_field :name, class: 'form-control'
-
- = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
- .form-group
- = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
- = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
- = provider_gcp_field.text_field :gcp_project_id, class: 'form-control'
-
- .form-group
- = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
- = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
- = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a'
-
- .form-group
- = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
- = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
-
- .form-group
- = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
- = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
- = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-2'
-
- .form-group
- = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save'
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
new file mode 100644
index 00000000000..0f6bae97571
--- /dev/null
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -0,0 +1,32 @@
+%p
+ - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
+
+= form_for @cluster, html: { class: 'prepend-top-20' }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+ = form_errors(@cluster)
+ .form-group
+ = field.label :name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+
+ = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
+ .form-group
+ = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
+ = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :gcp_project_id, class: 'form-control', placeholder: s_('ClusterIntegration|Project ID')
+
+ .form-group
+ = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
+ = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a'
+
+ .form-group
+ = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
+ = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
+
+ .form-group
+ = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
+ = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4'
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/_header.html.haml b/app/views/projects/clusters/gcp/_header.html.haml
index beb798e7154..cddb53c2688 100644
--- a/app/views/projects/clusters/_header.html.haml
+++ b/app/views/projects/clusters/gcp/_header.html.haml
@@ -1,5 +1,5 @@
-%h4.prepend-top-0
- = s_('ClusterIntegration|Create new cluster on Google Container Engine')
+%h4.prepend-top-20
+ = s_('ClusterIntegration|Enter the details for your cluster')
%p
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
%ul
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml
new file mode 100644
index 00000000000..3fa9f69708a
--- /dev/null
+++ b/app/views/projects/clusters/gcp/_show.html.haml
@@ -0,0 +1,40 @@
+.form-group
+ %label.append-bottom-10{ for: 'cluster-name' }
+ = s_('ClusterIntegration|Cluster name')
+ .input-group
+ %input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true }
+ %span.input-group-btn
+ = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name'), class: 'btn-default')
+
+= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+ = form_errors(@cluster)
+ = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
+ .form-group
+ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ .input-group
+ = platform_kubernetes_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: true
+ %span.input-group-btn
+ = clipboard_button(text: @cluster.platform_kubernetes.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'btn-default')
+
+ .form-group
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ .input-group
+ = platform_kubernetes_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: true
+ %span.input-group-addon.clipboard-addon
+ = clipboard_button(text: @cluster.platform_kubernetes.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'btn-blank')
+
+ .form-group
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ .input-group
+ = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: true
+ %span.input-group-btn
+ %button.btn.btn-default.js-show-cluster-token{ type: 'button' }
+ = s_('ClusterIntegration|Show')
+ = clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
+
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
index fde030b500b..790ba61fd86 100644
--- a/app/views/projects/clusters/login.html.haml
+++ b/app/views/projects/clusters/gcp/login.html.haml
@@ -3,8 +3,9 @@
.row.prepend-top-default
.col-sm-4
- = render 'sidebar'
+ = render 'projects/clusters/sidebar'
.col-sm-8
+ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine')
= render 'header'
.row
.col-sm-8.col-sm-offset-4.signin-with-google
diff --git a/app/views/projects/clusters/gcp/new.html.haml b/app/views/projects/clusters/gcp/new.html.haml
new file mode 100644
index 00000000000..9a79480c82f
--- /dev/null
+++ b/app/views/projects/clusters/gcp/new.html.haml
@@ -0,0 +1,10 @@
+- breadcrumb_title "Cluster"
+- page_title _("New Cluster")
+
+.row.prepend-top-default
+ .col-sm-4
+ = render 'projects/clusters/sidebar'
+ .col-sm-8
+ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine')
+ = render 'header'
+ = render 'form'
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index 665120c7e49..2e5bc34f64a 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -5,16 +5,9 @@
.col-sm-4
= render 'sidebar'
.col-sm-8
- - if @project.deployment_platform&.active?
- %h4.prepend-top-0= s_('ClusterIntegration|Cluster management')
+ %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
- %p= s_('ClusterIntegration|A cluster has been set up on this project through the Kubernetes integration page')
- = link_to s_('ClusterIntegration|Manage Kubernetes integration'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20'
-
- - else
- %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
-
- %p= s_('ClusterIntegration|Create a new cluster on Google Container Engine right from GitLab')
- = link_to s_('ClusterIntegration|Create on GKE'), providers_gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
- %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
- = link_to s_('ClusterIntegration|Add an existing cluster'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20'
+ %p= s_('ClusterIntegration|Create a new cluster on Google Engine right from GitLab')
+ = link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
+ %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
+ = link_to s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
diff --git a/app/views/projects/clusters/new_gcp.html.haml b/app/views/projects/clusters/new_gcp.html.haml
deleted file mode 100644
index 48e6b6ae8e8..00000000000
--- a/app/views/projects/clusters/new_gcp.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-- breadcrumb_title "Cluster"
-- page_title _("New Cluster")
-
-.row.prepend-top-default
- .col-sm-4
- = render 'sidebar'
- .col-sm-8
- = render 'header'
-
-= render 'form'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index b7671f5e3c4..d23efe4d9aa 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -13,52 +13,16 @@
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications') } }
-
.js-cluster-application-notice
.flash-container
%section.settings.no-animate.expanded
- %h4= s_('ClusterIntegration|Enable cluster integration')
- .settings-content
-
- .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
- %p.js-error-reason
-
- .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Cluster is being created on Google Container Engine...')
-
- .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine')
-
- %p
- - if @cluster.enabled?
- - if can?(current_user, :update_cluster, @cluster)
- = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
- - else
- = s_('ClusterIntegration|Cluster integration is enabled for this project.')
- - else
- = s_('ClusterIntegration|Cluster integration is disabled for this project.')
-
- = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
- = form_errors(@cluster)
- .form-group.append-bottom-20
- %label.append-bottom-10
- = field.hidden_field :enabled, { class: 'js-toggle-input'}
-
- %button{ type: 'button',
- class: "js-toggle-cluster project-feature-toggle #{'checked' unless !@cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, @cluster)}",
- 'aria-label': s_('ClusterIntegration|Toggle Cluster'),
- disabled: !can?(current_user, :update_cluster, @cluster),
- data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled' } }
-
- - if can?(current_user, :update_cluster, @cluster)
- .form-group
- = field.submit _('Save'), class: 'btn btn-success'
+ = render 'banner'
+ = render 'enabled'
.cluster-applications-table#js-cluster-applications
- %section.settings#js-cluster-details
+ %section.settings#js-cluster-details{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('ClusterIntegration|Cluster details')
%button.btn.js-settings-toggle
@@ -66,20 +30,16 @@
%p= s_('ClusterIntegration|See and edit the details for your cluster')
.settings-content
-
- .form_group.append-bottom-20
- %label.append-bottom-10{ for: 'cluster-name' }
- = s_('ClusterIntegration|Cluster name')
- .input-group
- %input.form-control.cluster-name{ value: @cluster.name, disabled: true }
- %span.input-group-addon.clipboard-addon
- = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name'))
+ - if @cluster.managed?
+ = render 'projects/clusters/gcp/show'
+ - else
+ = render 'projects/clusters/user/show'
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Advanced settings')
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
- %p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project')
+ %p= s_('ClusterIntegration|Manage cluster integration on your GitLab project')
.settings-content
= render 'advanced_settings'
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
new file mode 100644
index 00000000000..4a9bd5186c6
--- /dev/null
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -0,0 +1,25 @@
+= form_for @cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+ = form_errors(@cluster)
+ .form-group
+ = field.label :name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+
+ = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
+ .form-group
+ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
+
+ .form-group
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
+
+ .form-group
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ = platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
+
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Add cluster'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/user/_header.html.haml b/app/views/projects/clusters/user/_header.html.haml
new file mode 100644
index 00000000000..06ac210a06d
--- /dev/null
+++ b/app/views/projects/clusters/user/_header.html.haml
@@ -0,0 +1,5 @@
+%h4.prepend-top-20
+ = s_('ClusterIntegration|Enter the details for your cluster')
+%p
+ - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters').html_safe % { link_to_help_page: link_to_help_page }
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
new file mode 100644
index 00000000000..5931e0b7f17
--- /dev/null
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -0,0 +1,29 @@
+= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+ = form_errors(@cluster)
+ .form-group
+ = field.label :name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+
+ = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
+ .form-group
+ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
+
+ .form-group
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
+
+ .form-group
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ .input-group
+ = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off'
+ %span.input-group-addon.clipboard-addon
+ %button.js-show-cluster-token.btn-blank{ type: 'button' }
+ = s_('ClusterIntegration|Show')
+
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/clusters/user/new.html.haml b/app/views/projects/clusters/user/new.html.haml
new file mode 100644
index 00000000000..68f38f83453
--- /dev/null
+++ b/app/views/projects/clusters/user/new.html.haml
@@ -0,0 +1,11 @@
+- breadcrumb_title "Cluster"
+- page_title _("New Cluster")
+
+.row.prepend-top-default
+ .col-sm-4
+ = render 'projects/clusters/sidebar'
+ .col-sm-8
+ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Add an existing cluster')
+ = render 'header'
+ .prepend-top-20
+ = render 'form'
diff --git a/app/views/projects/issues/_by_email_description.html.haml b/app/views/projects/issues/_by_email_description.html.haml
new file mode 100644
index 00000000000..f2d58534903
--- /dev/null
+++ b/app/views/projects/issues/_by_email_description.html.haml
@@ -0,0 +1,6 @@
+The subject will be used as the title of the new issue, and the message will be the description.
+
+= link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
+and styling with
+= link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
+are supported.
diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml
deleted file mode 100644
index 264032a3a31..00000000000
--- a/app/views/projects/issues/_issue_by_email.html.haml
+++ /dev/null
@@ -1,34 +0,0 @@
-.issues-footer.text-center
- %button.issue-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issue-email-modal" } }
- Email a new issue to this project
-
-#issue-email-modal.modal.fade{ tabindex: "-1", role: "dialog" }
- .modal-dialog{ role: "document" }
- .modal-content
- .modal-header
- %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
- %span{ aria: { hidden: "true" } }= icon("times")
- %h4.modal-title
- Create new issue by email
- .modal-body
- %p
- You can create a new issue inside this project by sending an email to the following email address:
- .email-modal-input-group.input-group
- = text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-btn
- = clipboard_button(target: '#issue_email')
- %p
- The subject will be used as the title of the new issue, and the message will be the description.
-
- = link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
- and styling with
- = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
- are supported.
-
- %p
- This is a private email address, generated just for you.
-
- Anyone who gets ahold of it can create issues as if they were you.
- You should
- = link_to 'reset it', new_issue_address_project_path(@project), class: 'incoming-email-token-reset'
- if that ever happens.
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index bfaf024428d..193111b4cee 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -2,7 +2,7 @@
- @can_bulk_update = can?(current_user, :admin_issue, @project)
- page_title "Issues"
-- new_issue_email = @project.new_issue_address(current_user)
+- new_issue_email = @project.new_issuable_address(current_user, 'issue')
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
@@ -25,6 +25,6 @@
.issues-holder
= render 'issues'
- if new_issue_email
- = render 'issue_by_email', email: new_issue_email
+ = render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue'
- else
= render 'shared/empty_states/issues', button_path: new_project_issue_path(@project)
diff --git a/app/views/projects/merge_requests/_by_email_description.html.haml b/app/views/projects/merge_requests/_by_email_description.html.haml
new file mode 100644
index 00000000000..8ba251749b8
--- /dev/null
+++ b/app/views/projects/merge_requests/_by_email_description.html.haml
@@ -0,0 +1 @@
+The subject will be used as the source branch name for the new merge request and the target branch will be the default branch for the project.
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 8da2243adef..2ded7484151 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -4,6 +4,7 @@
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- page_title "Merge Requests"
+- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request')
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
@@ -25,5 +26,7 @@
.merge-requests-holder
= render 'merge_requests'
+ - if new_merge_request_email
+ = render 'projects/issuable_by_email', email: new_merge_request_email, issuable_type: 'merge_request'
- else
= render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 3f03cc7a275..6d8a4668cec 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -1,6 +1,5 @@
- type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false)
-- issuables = @issues || @merge_requests
%ul.nav-links.issues-state-filters
%li{ class: active_when(params[:state] == 'opened') }>
@@ -20,6 +19,4 @@
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed)}
- %li{ class: active_when(params[:state] == 'all') }>
- = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do
- #{issuables_state_counter_text(type, :all)}
+ = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all)
diff --git a/app/views/shared/issuable/nav_links/_all.html.haml b/app/views/shared/issuable/nav_links/_all.html.haml
new file mode 100644
index 00000000000..d7ad7090a45
--- /dev/null
+++ b/app/views/shared/issuable/nav_links/_all.html.haml
@@ -0,0 +1,6 @@
+- page_context_word = local_assigns.fetch(:page_context_word)
+- counter = local_assigns.fetch(:counter)
+
+%li{ class: active_when(params[:state] == 'all') }>
+ = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do
+ #{counter}
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index fff7cf9d26c..dd8a6cbbef1 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -38,8 +38,7 @@ class EmailReceiverWorker
"You are not allowed to perform this action. If you believe this is in error, contact a staff member."
when Gitlab::Email::NoteableNotFoundError
"The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
- when Gitlab::Email::InvalidNoteError,
- Gitlab::Email::InvalidIssueError
+ when Gitlab::Email::InvalidRecordError
can_retry = true
e.message
end
diff --git a/changelogs/unreleased/32878-merge-request-from-email.yml b/changelogs/unreleased/32878-merge-request-from-email.yml
new file mode 100644
index 00000000000..2df148d5a81
--- /dev/null
+++ b/changelogs/unreleased/32878-merge-request-from-email.yml
@@ -0,0 +1,5 @@
+---
+title: Allow creation of merge request from email
+merge_request: 13817
+author: janp
+type: added
diff --git a/changelogs/unreleased/35616-move-k8-to-cluster-page.yml b/changelogs/unreleased/35616-move-k8-to-cluster-page.yml
new file mode 100644
index 00000000000..032a39608ce
--- /dev/null
+++ b/changelogs/unreleased/35616-move-k8-to-cluster-page.yml
@@ -0,0 +1,5 @@
+---
+title: Create a new form to add Existing Kubernetes Cluster
+merge_request: 14805
+author:
+type: added
diff --git a/changelogs/unreleased/bvl-limit-fork-queries-on-project-show.yml b/changelogs/unreleased/bvl-limit-fork-queries-on-project-show.yml
new file mode 100644
index 00000000000..299d9bf6b9c
--- /dev/null
+++ b/changelogs/unreleased/bvl-limit-fork-queries-on-project-show.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce requests for project forks on show page of projects that have forks
+merge_request: 15663
+author:
+type: performance
diff --git a/config/routes/project.rb b/config/routes/project.rb
index bdafaba3ab3..093da10f57f 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -183,10 +183,16 @@ constraints(ProjectUrlConstrainer.new) do
end
end
- resources :clusters, except: [:edit] do
+ resources :clusters, except: [:edit, :create] do
collection do
- get :login
- get '/providers/gcp/new', action: :new_gcp
+ scope :providers do
+ get '/user/new', to: 'clusters/user#new'
+ post '/user', to: 'clusters/user#create'
+
+ get '/gcp/new', to: 'clusters/gcp#new'
+ get '/gcp/login', to: 'clusters/gcp#login'
+ post '/gcp', to: 'clusters/gcp#create'
+ end
end
member do
@@ -429,7 +435,7 @@ constraints(ProjectUrlConstrainer.new) do
get :download_export
get :activity
get :refs
- put :new_issue_address
+ put :new_issuable_address
end
end
end
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index 68ba3dd2da3..b6def7ef541 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -152,12 +152,23 @@ CE and EE.
## Previewing the changes live
If you want to preview the doc changes of your merge request live, you can use
-the manual `review-docs-deploy` job in your merge request.
+the manual `review-docs-deploy` job in your merge request. You will need at
+least Master permissions to be able to run it and is currently enabled for the
+following projects:
+
+- https://gitlab.com/gitlab-org/gitlab-ce
+- https://gitlab.com/gitlab-org/gitlab-ee
+
+NOTE: **Note:**
+You will need to push a branch to those repositories, it doesn't work for forks.
TIP: **Tip:**
If your branch contains only documentation changes, you can use
[special branch names](#testing) to avoid long running pipelines.
+In the mini pipeline graph, you should see an `>>` icon. Clicking on it will
+reveal the `review-docs-deploy` job. Hit the play button for the job to start.
+
![Manual trigger a docs build](img/manual_build_docs.png)
This job will:
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 4b2e042251b..d76ea259301 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -27,7 +27,7 @@ With GitLab merge requests, you can:
- [Resolve merge conflicts from the UI](#resolve-conflicts)
- Enable [fast-forward merge requests](#fast-forward-merge-requests)
- Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch
-
+- [Create new merge requests by email](#create_by_email)
With **[GitLab Enterprise Edition][ee]**, you can also:
@@ -132,6 +132,14 @@ those conflicts in the GitLab UI.
[Learn more about resolving merge conflicts in the UI.](resolve_conflicts.md)
+## Create new merge requests by email
+
+You can create a new merge request by sending an email to a user-specific email
+address. The address can be obtained on the merge requests page by clicking on
+a **Email a new merge request to this project** button. The subject will be
+used as the source branch name for the new merge request and the target branch
+will be the default branch for the project.
+
## Revert changes
GitLab implements Git's powerful feature to revert any commit with introducing
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index b07c68d1498..e08b5be8984 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -1,3 +1,4 @@
+require 'gitlab/email/handler/create_merge_request_handler'
require 'gitlab/email/handler/create_note_handler'
require 'gitlab/email/handler/create_issue_handler'
require 'gitlab/email/handler/unsubscribe_handler'
@@ -8,6 +9,7 @@ module Gitlab
HANDLERS = [
UnsubscribeHandler,
CreateNoteHandler,
+ CreateMergeRequestHandler,
CreateIssueHandler
].freeze
diff --git a/lib/gitlab/email/handler/create_merge_request_handler.rb b/lib/gitlab/email/handler/create_merge_request_handler.rb
new file mode 100644
index 00000000000..c63666b98c1
--- /dev/null
+++ b/lib/gitlab/email/handler/create_merge_request_handler.rb
@@ -0,0 +1,67 @@
+require 'gitlab/email/handler/base_handler'
+require 'gitlab/email/handler/reply_processing'
+
+module Gitlab
+ module Email
+ module Handler
+ class CreateMergeRequestHandler < BaseHandler
+ include ReplyProcessing
+ attr_reader :project_path, :incoming_email_token
+
+ def initialize(mail, mail_key)
+ super(mail, mail_key)
+ if m = /\A([^\+]*)\+merge-request\+(.*)/.match(mail_key.to_s)
+ @project_path, @incoming_email_token = m.captures
+ end
+ end
+
+ def can_handle?
+ @project_path && @incoming_email_token
+ end
+
+ def execute
+ raise ProjectNotFound unless project
+
+ validate_permission!(:create_merge_request)
+
+ verify_record!(
+ record: create_merge_request,
+ invalid_exception: InvalidMergeRequestError,
+ record_name: 'merge_request')
+ end
+
+ def author
+ @author ||= User.find_by(incoming_email_token: incoming_email_token)
+ end
+
+ def project
+ @project ||= Project.find_by_full_path(project_path)
+ end
+
+ def metrics_params
+ super.merge(project: project&.full_path)
+ end
+
+ private
+
+ def create_merge_request
+ merge_request = MergeRequests::BuildService.new(project, author, merge_request_params).execute
+
+ if merge_request.errors.any?
+ merge_request
+ else
+ MergeRequests::CreateService.new(project, author).create(merge_request)
+ end
+ end
+
+ def merge_request_params
+ {
+ source_project_id: project.id,
+ source_branch: mail.subject,
+ target_project_id: project.id
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index c8f4591d060..d8c594ad0e7 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -13,8 +13,10 @@ module Gitlab
UserBlockedError = Class.new(ProcessingError)
UserNotAuthorizedError = Class.new(ProcessingError)
NoteableNotFoundError = Class.new(ProcessingError)
- InvalidNoteError = Class.new(ProcessingError)
- InvalidIssueError = Class.new(ProcessingError)
+ InvalidRecordError = Class.new(ProcessingError)
+ InvalidNoteError = Class.new(InvalidRecordError)
+ InvalidIssueError = Class.new(InvalidRecordError)
+ InvalidMergeRequestError = Class.new(InvalidRecordError)
UnknownIncomingEmail = Class.new(ProcessingError)
class Receiver
diff --git a/lib/gitlab/git/conflict/file.rb b/lib/gitlab/git/conflict/file.rb
index fc1595f1faf..b2a625e08fa 100644
--- a/lib/gitlab/git/conflict/file.rb
+++ b/lib/gitlab/git/conflict/file.rb
@@ -2,7 +2,7 @@ module Gitlab
module Git
module Conflict
class File
- attr_reader :content, :their_path, :our_path, :our_mode, :repository
+ attr_reader :content, :their_path, :our_path, :our_mode, :repository, :commit_oid
def initialize(repository, commit_oid, conflict, content)
@repository = repository
diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb
index df509c5f4ce..de8cce41b6d 100644
--- a/lib/gitlab/git/conflict/resolver.rb
+++ b/lib/gitlab/git/conflict/resolver.rb
@@ -75,7 +75,7 @@ module Gitlab
resolved_lines = file.resolve_lines(params[:sections])
new_file = resolved_lines.map { |line| line[:full_line] }.join("\n")
- new_file << "\n" if file.our_blob.data.ends_with?("\n")
+ new_file << "\n" if file.our_blob.data.end_with?("\n")
elsif params[:content]
new_file = file.resolve_content(params[:content])
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index f2bbaed64a7..eab04bcac65 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -18,6 +18,8 @@ module Gitlab
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
].freeze
SEARCH_CONTEXT_LINES = 3
+ REBASE_WORKTREE_PREFIX = 'rebase'.freeze
+ SQUASH_WORKTREE_PREFIX = 'squash'.freeze
NoRepository = Class.new(StandardError)
InvalidBlobName = Class.new(StandardError)
@@ -1070,13 +1072,8 @@ module Gitlab
raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ')
raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00")
- command = [Gitlab.config.git.bin_path] + %w[update-ref --stdin -z]
input = "update #{ref_path}\x00#{ref}\x00\x00"
- output, status = circuit_breaker.perform do
- popen(command, path) { |stdin| stdin.write(input) }
- end
-
- raise GitError, output unless status.zero?
+ run_git!(%w[update-ref --stdin -z]) { |stdin| stdin.write(input) }
end
def fetch_ref(source_repository, source_ref:, target_ref:)
@@ -1098,14 +1095,22 @@ module Gitlab
end
# Refactoring aid; allows us to copy code from app/models/repository.rb
- def run_git(args, env: {}, nice: false)
+ def run_git(args, chdir: path, env: {}, nice: false, &block)
cmd = [Gitlab.config.git.bin_path, *args]
cmd.unshift("nice") if nice
circuit_breaker.perform do
- popen(cmd, path, env)
+ popen(cmd, chdir, env, &block)
end
end
+ def run_git!(args, chdir: path, env: {}, nice: false, &block)
+ output, status = run_git(args, chdir: chdir, env: env, nice: nice, &block)
+
+ raise GitError, output unless status.zero?
+
+ output
+ end
+
# Refactoring aid; allows us to copy code from app/models/repository.rb
def run_git_with_timeout(args, timeout, env: {})
circuit_breaker.perform do
@@ -1175,6 +1180,64 @@ module Gitlab
raise GitError.new("Could not fsck repository:\n#{output}") unless status.zero?
end
+ def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
+ rebase_path = worktree_path(REBASE_WORKTREE_PREFIX, rebase_id)
+ env = git_env_for_user(user)
+
+ with_worktree(rebase_path, branch, env: env) do
+ run_git!(
+ %W(pull --rebase #{remote_repository.path} #{remote_branch}),
+ chdir: rebase_path, env: env
+ )
+
+ rebase_sha = run_git!(%w(rev-parse HEAD), chdir: rebase_path, env: env).strip
+
+ Gitlab::Git::OperationService.new(user, self)
+ .update_branch(branch, rebase_sha, branch_sha)
+
+ rebase_sha
+ end
+ end
+
+ def rebase_in_progress?(rebase_id)
+ fresh_worktree?(worktree_path(REBASE_WORKTREE_PREFIX, rebase_id))
+ end
+
+ def squash(user, squash_id, branch:, start_sha:, end_sha:, author:, message:)
+ squash_path = worktree_path(SQUASH_WORKTREE_PREFIX, squash_id)
+ env = git_env_for_user(user).merge(
+ 'GIT_AUTHOR_NAME' => author.name,
+ 'GIT_AUTHOR_EMAIL' => author.email
+ )
+ diff_range = "#{start_sha}...#{end_sha}"
+ diff_files = run_git!(
+ %W(diff --name-only --diff-filter=a --binary #{diff_range})
+ ).chomp
+
+ with_worktree(squash_path, branch, sparse_checkout_files: diff_files, env: env) do
+ # Apply diff of the `diff_range` to the worktree
+ diff = run_git!(%W(diff --binary #{diff_range}))
+ run_git!(%w(apply --index), chdir: squash_path, env: env) do |stdin|
+ stdin.write(diff)
+ end
+
+ # Commit the `diff_range` diff
+ run_git!(%W(commit --no-verify --message #{message}), chdir: squash_path, env: env)
+
+ # Return the squash sha. May print a warning for ambiguous refs, but
+ # we can ignore that with `--quiet` and just take the SHA, if present.
+ # HEAD here always refers to the current HEAD commit, even if there is
+ # another ref called HEAD.
+ run_git!(
+ %w(rev-parse --quiet --verify HEAD), chdir: squash_path, env: env
+ ).chomp
+ end
+ end
+
+ def squash_in_progress?(squash_id)
+ fresh_worktree?(worktree_path(SQUASH_WORKTREE_PREFIX, squash_id))
+ end
+
def gitaly_repository
Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
end
@@ -1211,6 +1274,57 @@ module Gitlab
private
+ def fresh_worktree?(path)
+ File.exist?(path) && !clean_stuck_worktree(path)
+ end
+
+ def with_worktree(worktree_path, branch, sparse_checkout_files: nil, env:)
+ base_args = %w(worktree add --detach)
+
+ # Note that we _don't_ want to test for `.present?` here: If the caller
+ # passes an non nil empty value it means it still wants sparse checkout
+ # but just isn't interested in any file, perhaps because it wants to
+ # checkout files in by a changeset but that changeset only adds files.
+ if sparse_checkout_files
+ # Create worktree without checking out
+ run_git!(base_args + ['--no-checkout', worktree_path], env: env)
+ worktree_git_path = run_git!(%w(rev-parse --git-dir), chdir: worktree_path)
+
+ configure_sparse_checkout(worktree_git_path, sparse_checkout_files)
+
+ # After sparse checkout configuration, checkout `branch` in worktree
+ run_git!(%W(checkout --detach #{branch}), chdir: worktree_path, env: env)
+ else
+ # Create worktree and checkout `branch` in it
+ run_git!(base_args + [worktree_path, branch], env: env)
+ end
+
+ yield
+ ensure
+ FileUtils.rm_rf(worktree_path) if File.exist?(worktree_path)
+ FileUtils.rm_rf(worktree_git_path) if worktree_git_path && File.exist?(worktree_git_path)
+ end
+
+ def clean_stuck_worktree(path)
+ return false unless File.mtime(path) < 15.minutes.ago
+
+ FileUtils.rm_rf(path)
+ true
+ end
+
+ # Adding a worktree means checking out the repository. For large repos,
+ # this can be very expensive, so set up sparse checkout for the worktree
+ # to only check out the files we're interested in.
+ def configure_sparse_checkout(worktree_git_path, files)
+ run_git!(%w(config core.sparseCheckout true))
+
+ return if files.empty?
+
+ worktree_info_path = File.join(worktree_git_path, 'info')
+ FileUtils.mkdir_p(worktree_info_path)
+ File.write(File.join(worktree_info_path, 'sparse-checkout'), files)
+ end
+
def rugged_fetch_source_branch(source_repository, source_branch, local_ref)
with_repo_branch_commit(source_repository, source_branch) do |commit|
if commit
@@ -1222,6 +1336,24 @@ module Gitlab
end
end
+ def worktree_path(prefix, id)
+ id = id.to_s
+ raise ArgumentError, "worktree id can't be empty" unless id.present?
+ raise ArgumentError, "worktree id can't contain slashes " if id.include?("/")
+
+ File.join(path, 'gitlab-worktree', "#{prefix}-#{id}")
+ end
+
+ def git_env_for_user(user)
+ {
+ 'GIT_COMMITTER_NAME' => user.name,
+ 'GIT_COMMITTER_EMAIL' => user.email,
+ 'GL_ID' => Gitlab::GlId.gl_id(user),
+ 'GL_PROTOCOL' => Gitlab::Git::Hook::GL_PROTOCOL,
+ 'GL_REPOSITORY' => gl_repository
+ }
+ end
+
# Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'.
def branches_filter(filter: nil, sort_by: nil)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37464
diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb
new file mode 100644
index 00000000000..bb5ef7bb365
--- /dev/null
+++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb
@@ -0,0 +1,185 @@
+require 'spec_helper'
+
+describe Projects::Clusters::GcpController do
+ include AccessMatchersForController
+ include GoogleApi::CloudPlatformHelpers
+
+ set(:project) { create(:project) }
+
+ describe 'GET login' do
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when omniauth has been configured' do
+ let(:key) { 'secret-key' }
+
+ let(:session_key_for_redirect_uri) do
+ GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
+ end
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(key)
+ end
+
+ it 'has authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to include(key)
+ expect(session[session_key_for_redirect_uri]).to eq(gcp_new_project_clusters_path(project))
+ end
+ end
+
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'does not have authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to be_nil
+ end
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :login, namespace_id: project.namespace, project_id: project
+ end
+ end
+
+ describe 'GET new' do
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'has new object' do
+ go
+
+ expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(go).to redirect_to(gcp_login_project_clusters_path(project)) }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(go).to redirect_to(gcp_login_project_clusters_path(project)) }
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :new, namespace_id: project.namespace, project_id: project
+ end
+ end
+
+ describe 'POST create' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_gcp_attributes: {
+ gcp_project_id: '111'
+ }
+ }
+ }
+ end
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ context 'when creates a cluster on gke' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ expect(project.cluster).to be_gcp
+ expect(project.cluster).to be_kubernetes
+ end
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it 'redirects to login page' do
+ expect(go).to redirect_to(gcp_login_project_clusters_path(project))
+ end
+ end
+
+ context 'when access token is not stored in session' do
+ it 'redirects to login page' do
+ expect(go).to redirect_to(gcp_login_project_clusters_path(project))
+ end
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(namespace_id: project.namespace, project_id: project)
+ end
+ end
+end
diff --git a/spec/controllers/projects/clusters/user_controller_spec.rb b/spec/controllers/projects/clusters/user_controller_spec.rb
new file mode 100644
index 00000000000..22005e0dc66
--- /dev/null
+++ b/spec/controllers/projects/clusters/user_controller_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Projects::Clusters::UserController do
+ include AccessMatchersForController
+
+ set(:project) { create(:project) }
+
+ describe 'GET new' do
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it 'has new object' do
+ go
+
+ expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :new, namespace_id: project.namespace, project_id: project
+ end
+ end
+
+ describe 'POST create' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test',
+ namespace: 'aaa'
+ }
+ }
+ }
+ end
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when creates a cluster' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ end
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(namespace_id: project.namespace, project_id: project)
+ end
+ end
+end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index ca2bcb2b5ae..66e67652dad 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -4,6 +4,8 @@ describe Projects::ClustersController do
include AccessMatchersForController
include GoogleApi::CloudPlatformHelpers
+ set(:project) { create(:project) }
+
describe 'GET index' do
describe 'functionality' do
let(:user) { create(:user) }
@@ -14,22 +16,18 @@ describe Projects::ClustersController do
end
context 'when project has a cluster' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
it { expect(go).to redirect_to(project_cluster_path(project, project.cluster)) }
end
context 'when project does not have a cluster' do
- let(:project) { create(:project) }
-
it { expect(go).to redirect_to(new_project_cluster_path(project)) }
end
end
describe 'security' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
@@ -46,198 +44,8 @@ describe Projects::ClustersController do
end
end
- describe 'GET login' do
- let(:project) { create(:project) }
-
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when omniauth has been configured' do
- let(:key) { 'secere-key' }
-
- let(:session_key_for_redirect_uri) do
- GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
- end
-
- before do
- allow(SecureRandom).to receive(:hex).and_return(key)
- end
-
- it 'has authorize_url' do
- go
-
- expect(assigns(:authorize_url)).to include(key)
- expect(session[session_key_for_redirect_uri]).to eq(providers_gcp_new_project_clusters_url(project))
- end
- end
-
- context 'when omniauth has not configured' do
- before do
- stub_omniauth_setting(providers: [])
- end
-
- it 'does not have authorize_url' do
- go
-
- expect(assigns(:authorize_url)).to be_nil
- end
- end
- end
-
- describe 'security' do
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
- end
-
- def go
- get :login, namespace_id: project.namespace, project_id: project
- end
- end
-
- shared_examples 'requires to login' do
- it 'redirects to create a cluster' do
- subject
-
- expect(response).to redirect_to(login_project_clusters_path(project))
- end
- end
-
- describe 'GET new_gcp' do
- let(:project) { create(:project) }
-
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when access token is valid' do
- before do
- stub_google_api_validate_token
- end
-
- it 'has new object' do
- go
-
- expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
- end
- end
-
- context 'when access token is expired' do
- before do
- stub_google_api_expired_token
- end
-
- it { expect(go).to redirect_to(login_project_clusters_path(project)) }
- end
-
- context 'when access token is not stored in session' do
- it { expect(go).to redirect_to(login_project_clusters_path(project)) }
- end
- end
-
- describe 'security' do
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
- end
-
- def go
- get :new_gcp, namespace_id: project.namespace, project_id: project
- end
- end
-
- describe 'POST create' do
- let(:project) { create(:project) }
-
- let(:params) do
- {
- cluster: {
- name: 'new-cluster',
- provider_type: :gcp,
- provider_gcp_attributes: {
- gcp_project_id: '111'
- }
- }
- }
- end
-
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when access token is valid' do
- before do
- stub_google_api_validate_token
- end
-
- context 'when creates a cluster on gke' do
- it 'creates a new cluster' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
- expect { go }.to change { Clusters::Cluster.count }
- expect(response).to redirect_to(project_cluster_path(project, project.cluster))
- end
- end
- end
-
- context 'when access token is expired' do
- before do
- stub_google_api_expired_token
- end
-
- it 'redirects to login page' do
- expect(go).to redirect_to(login_project_clusters_path(project))
- end
- end
-
- context 'when access token is not stored in session' do
- it 'redirects to login page' do
- expect(go).to redirect_to(login_project_clusters_path(project))
- end
- end
- end
-
- describe 'security' do
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
- end
-
- def go
- post :create, params.merge(namespace_id: project.namespace, project_id: project)
- end
- end
-
describe 'GET status' do
- let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
- let(:project) { cluster.project }
+ let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
describe 'functionality' do
let(:user) { create(:user) }
@@ -275,8 +83,7 @@ describe Projects::ClustersController do
end
describe 'GET show' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
describe 'functionality' do
let(:user) { create(:user) }
@@ -313,10 +120,8 @@ describe Projects::ClustersController do
end
describe 'PUT update' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
-
- describe 'functionality' do
+ context 'when cluster is provided by GCP' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let(:user) { create(:user) }
before do
@@ -324,10 +129,16 @@ describe Projects::ClustersController do
sign_in(user)
end
- context 'when update enabled' do
+ context 'when changing parameters' do
let(:params) do
{
- cluster: { enabled: false }
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
+ }
+ }
}
end
@@ -340,8 +151,14 @@ describe Projects::ClustersController do
expect(cluster.enabled).to be_falsey
end
+ it "does not change cluster name" do
+ go
+
+ expect(cluster.name).to eq('test-cluster')
+ end
+
context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
it "rejects changes" do
go
@@ -354,11 +171,46 @@ describe Projects::ClustersController do
end
end
+ context 'when cluster is provided by user' do
+ let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when changing parameters' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
+ }
+ }
+ }
+ end
+
+ it "updates and redirects back to show page" do
+ go
+
+ cluster.reload
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
+ end
+ end
+ end
+
describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
let(:params) do
- {
- cluster: { enabled: false }
- }
+ { cluster: { enabled: false } }
end
it { expect { go }.to be_allowed_for(:admin) }
@@ -378,10 +230,7 @@ describe Projects::ClustersController do
end
end
- describe 'delete update' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
-
+ describe 'DELETE destroy' do
describe 'functionality' do
let(:user) { create(:user) }
@@ -390,31 +239,37 @@ describe Projects::ClustersController do
sign_in(user)
end
- it "destroys and redirects back to clusters list" do
- expect { go }
- .to change { Clusters::Cluster.count }.by(-1)
- .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
- .and change { Clusters::Providers::Gcp.count }.by(-1)
+ context 'when cluster is provided by GCP' do
+ context 'when cluster is created' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- expect(response).to redirect_to(project_clusters_path(project))
- expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
- end
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
- context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
+ end
- it "destroys and redirects back to clusters list" do
- expect { go }
- .to change { Clusters::Cluster.count }.by(-1)
- .and change { Clusters::Providers::Gcp.count }.by(-1)
+ context 'when cluster is being created' do
+ let!(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
- expect(response).to redirect_to(project_clusters_path(project))
- expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
end
end
- context 'when provider is user' do
- let(:cluster) { create(:cluster, :project, :provided_by_user) }
+ context 'when cluster is provided by user' do
+ let!(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
it "destroys and redirects back to clusters list" do
expect { go }
@@ -429,6 +284,8 @@ describe Projects::ClustersController do
end
describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:master).of(project) }
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index e7ab714c550..e61187fb518 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -261,6 +261,27 @@ describe ProjectsController do
expect(response).to redirect_to(namespace_project_path)
end
end
+
+ context 'when the project is forked and has a repository', :request_store do
+ let(:public_project) { create(:project, :public, :repository) }
+ let(:other_user) { create(:user) }
+
+ render_views
+
+ before do
+ # View the project as a user that does not have any rights
+ sign_in(other_user)
+
+ fork_project(public_project)
+ end
+
+ it 'does not increase the number of queries when the project is forked' do
+ expected_query = /#{public_project.fork_network.find_forks_in(other_user.namespace).to_sql}/
+
+ expect { get(:show, namespace_id: public_project.namespace, id: public_project) }
+ .not_to exceed_query_limit(1).for_query(expected_query)
+ end
+ end
end
describe "#update" do
@@ -405,11 +426,12 @@ describe ProjectsController do
end
end
- describe 'PUT #new_issue_address' do
+ describe 'PUT #new_issuable_address for issue' do
subject do
- put :new_issue_address,
+ put :new_issuable_address,
namespace_id: project.namespace,
- id: project
+ id: project,
+ issuable_type: 'issue'
user.reload
end
@@ -428,7 +450,35 @@ describe ProjectsController do
end
it 'changes projects new issue address' do
- expect { subject }.to change { project.new_issue_address(user) }
+ expect { subject }.to change { project.new_issuable_address(user, 'issue') }
+ end
+ end
+
+ describe 'PUT #new_issuable_address for merge request' do
+ subject do
+ put :new_issuable_address,
+ namespace_id: project.namespace,
+ id: project,
+ issuable_type: 'merge_request'
+ user.reload
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
+ end
+
+ it 'has http status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'changes the user incoming email token' do
+ expect { subject }.to change { user.incoming_email_token }
+ end
+
+ it 'changes projects new merge request address' do
+ expect { subject }.to change { project.new_issuable_address(user, 'merge_request') }
end
end
diff --git a/spec/factories/clusters/cluster.rb b/spec/factories/clusters/clusters.rb
index c4261178f2d..81866845a20 100644
--- a/spec/factories/clusters/cluster.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -13,27 +13,20 @@ FactoryGirl.define do
provider_type :user
platform_type :kubernetes
- platform_kubernetes do
- create(:cluster_platform_kubernetes, :configured)
- end
+ platform_kubernetes factory: [:cluster_platform_kubernetes, :configured]
end
trait :provided_by_gcp do
provider_type :gcp
platform_type :kubernetes
- before(:create) do |cluster, evaluator|
- cluster.platform_kubernetes = build(:cluster_platform_kubernetes, :configured)
- cluster.provider_gcp = build(:cluster_provider_gcp, :created)
- end
+ provider_gcp factory: [:cluster_provider_gcp, :created]
+ platform_kubernetes factory: [:cluster_platform_kubernetes, :configured]
end
trait :providing_by_gcp do
provider_type :gcp
-
- provider_gcp do
- create(:cluster_provider_gcp, :creating)
- end
+ provider_gcp factory: [:cluster_provider_gcp, :creating]
end
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index b9af77f918a..852d9e368aa 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -365,16 +365,16 @@ describe 'Issues' do
end
it 'changes incoming email address token', :js do
- find('.issue-email-modal-btn').click
- previous_token = find('input#issue_email').value
+ find('.issuable-email-modal-btn').click
+ previous_token = find('input#issuable_email').value
find('.incoming-email-token-reset').click
wait_for_requests
- expect(page).to have_no_field('issue_email', with: previous_token)
- new_token = project1.new_issue_address(user.reload)
+ expect(page).to have_no_field('issuable_email', with: previous_token)
+ new_token = project1.new_issuable_address(user.reload, 'issue')
expect(page).to have_field(
- 'issue_email',
+ 'issuable_email',
with: new_token
)
end
@@ -630,8 +630,8 @@ describe 'Issues' do
end
it 'click the button to show modal for the new email' do
- page.within '#issue-email-modal' do
- email = project.new_issue_address(user)
+ page.within '#issuable-email-modal' do
+ email = project.new_issuable_address(user, 'issue')
expect(page).to have_selector("input[value='#{email}']")
end
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
new file mode 100644
index 00000000000..b34cd061ec6
--- /dev/null
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+feature 'Clusters Applications', :js do
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ describe 'Installing applications' do
+ before do
+ visit project_cluster_path(project, cluster)
+ end
+
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project])}
+
+ scenario 'user is unable to install applications' do
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install')
+ end
+ end
+ end
+
+ context 'when cluster is created' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project])}
+
+ scenario 'user can install applications' do
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+ end
+ end
+
+ context 'when user installs Helm' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+
+ page.within('.js-cluster-application-row-helm') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+ end
+
+ it 'he sees status transition' do
+ page.within('.js-cluster-application-row-helm') do
+ # FE sends request and gets the response, then the buttons is "Install"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+
+ Clusters::Cluster.last.application_helm.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+
+ Clusters::Cluster.last.application_helm.make_installed!
+
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ end
+
+ expect(page).to have_content('Helm Tiller was successfully installed on your cluster')
+ end
+ end
+
+ context 'when user installs Ingress' do
+ context 'when user installs application: Ingress' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+
+ create(:cluster_applications_helm, :installed, cluster: cluster)
+
+ page.within('.js-cluster-application-row-ingress') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+ end
+
+ it 'he sees status transition' do
+ page.within('.js-cluster-application-row-ingress') do
+ # FE sends request and gets the response, then the buttons is "Install"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+
+ Clusters::Cluster.last.application_ingress.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+
+ Clusters::Cluster.last.application_ingress.make_installed!
+
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ end
+
+ expect(page).to have_content('Ingress was successfully installed on your cluster')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
new file mode 100644
index 00000000000..8a0da669147
--- /dev/null
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -0,0 +1,136 @@
+require 'spec_helper'
+
+feature 'Gcp Cluster', :js do
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ gitlab_sign_in(user)
+ allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
+ end
+
+ context 'when user has signed with Google' do
+ before do
+ allow_any_instance_of(Projects::Clusters::GcpController)
+ .to receive(:token_in_session).and_return('token')
+ allow_any_instance_of(Projects::Clusters::GcpController)
+ .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
+ end
+
+ context 'when user does not have a cluster and visits cluster index page' do
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Create on GKE'
+ end
+
+ context 'when user filled form with valid parameters' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create) do
+ OpenStruct.new(
+ self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
+ status: 'RUNNING'
+ )
+ end
+
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+
+ fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
+ fill_in 'cluster_name', with: 'dev-cluster'
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a cluster details page and creation status' do
+ expect(page).to have_content('Cluster is being created on Google Container Engine...')
+
+ Clusters::Cluster.last.provider.make_created!
+
+ expect(page).to have_content('Cluster was successfully created on Google Container Engine')
+ end
+
+ it 'user sees a error if something worng during creation' do
+ expect(page).to have_content('Cluster is being created on Google Container Engine...')
+
+ Clusters::Cluster.last.provider.make_errored!('Something wrong!')
+
+ expect(page).to have_content('Something wrong!')
+ end
+ end
+
+ context 'when user filled form with invalid parameters' do
+ before do
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
+ end
+ end
+ end
+
+ context 'when user does have a cluster and visits cluster page' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
+ before do
+ visit project_cluster_path(project, cluster)
+ end
+
+ it 'user sees a cluster details page' do
+ expect(page).to have_button('Save')
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
+ end
+
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-toggle-cluster').click
+ click_button 'Save'
+ end
+
+ it 'user sees the successful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ end
+ end
+
+ context 'when user changes cluster parameters' do
+ before do
+ fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
+ click_button 'Save changes'
+ end
+
+ it 'user sees the successful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
+ end
+ end
+
+ context 'when user destroy the cluster' do
+ before do
+ page.accept_confirm do
+ click_link 'Remove integration'
+ end
+ end
+
+ it 'user sees creation form with the successful message' do
+ expect(page).to have_content('Cluster integration was successfully removed.')
+ expect(page).to have_link('Create on GKE')
+ end
+ end
+ end
+ end
+
+ context 'when user has not signed with Google' do
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Create on GKE'
+ end
+
+ it 'user sees a login page' do
+ expect(page).to have_css('.signin-with-google')
+ end
+ end
+end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
new file mode 100644
index 00000000000..e97ba88f2f4
--- /dev/null
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+feature 'User Cluster', :js do
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ gitlab_sign_in(user)
+ allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
+ end
+
+ context 'when user does not have a cluster and visits cluster index page' do
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Add an existing cluster'
+ end
+
+ context 'when user filled form with valid parameters' do
+ before do
+ fill_in 'cluster_name', with: 'dev-cluster'
+ fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com'
+ fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token'
+ click_button 'Add cluster'
+ end
+
+ it 'user sees a cluster details page' do
+ expect(page).to have_content('Enable cluster integration')
+ expect(page.find_field('cluster[name]').value).to eq('dev-cluster')
+ expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
+ .to have_content('http://example.com')
+ expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value)
+ .to have_content('my-token')
+ end
+ end
+
+ context 'when user filled form with invalid parameters' do
+ before do
+ click_button 'Add cluster'
+ end
+
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
+ end
+ end
+ end
+
+ context 'when user does have a cluster and visits cluster page' do
+ let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
+
+ before do
+ visit project_cluster_path(project, cluster)
+ end
+
+ it 'user sees a cluster details page' do
+ expect(page).to have_button('Save')
+ end
+
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-toggle-cluster').click
+ fill_in 'cluster_name', with: 'dev-cluster'
+ click_button 'Save'
+ end
+
+ it 'user sees the successful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ end
+ end
+
+ context 'when user changes cluster parameters' do
+ before do
+ fill_in 'cluster_name', with: 'my-dev-cluster'
+ fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
+ click_button 'Save changes'
+ end
+
+ it 'user sees the successful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ expect(cluster.reload.name).to eq('my-dev-cluster')
+ expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
+ end
+ end
+
+ context 'when user destroy the cluster' do
+ before do
+ page.accept_confirm do
+ click_link 'Remove integration'
+ end
+ end
+
+ it 'user sees creation form with the successful message' do
+ expect(page).to have_content('Cluster integration was successfully removed.')
+ expect(page).to have_link('Add an existing cluster')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 197e6df4997..4243c4fd266 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -3,204 +3,23 @@ require 'spec_helper'
feature 'Clusters', :js do
include GoogleApi::CloudPlatformHelpers
- let!(:project) { create(:project, :repository) }
- let!(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
before do
project.add_master(user)
gitlab_sign_in(user)
end
- context 'when user has signed in Google' do
- before do
- allow_any_instance_of(Projects::ClustersController)
- .to receive(:token_in_session).and_return('token')
- allow_any_instance_of(Projects::ClustersController)
- .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
- end
-
- context 'when user does not have a cluster and visits cluster index page' do
- before do
- visit project_clusters_path(project)
-
- click_link 'Create on GKE'
- end
-
- it 'user sees a new page' do
- expect(page).to have_button('Create cluster')
- end
-
- context 'when user filled form with valid parameters' do
- before do
- double.tap do |dbl|
- allow(dbl).to receive(:status).and_return('RUNNING')
- allow(dbl).to receive(:self_link)
- .and_return('projects/gcp-project-12345/zones/us-central1-a/operations/ope-123')
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create).and_return(dbl)
- end
-
- allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
-
- fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create cluster'
- end
-
- it 'user sees a cluster details page and creation status' do
- expect(page).to have_content('Cluster is being created on Google Container Engine...')
-
- # Application Installation buttons
- page.within('.js-cluster-application-row-helm') do
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install')
- end
-
- Clusters::Cluster.last.provider.make_created!
-
- expect(page).to have_content('Cluster was successfully created on Google Container Engine')
- end
-
- it 'user sees a error if something worng during creation' do
- expect(page).to have_content('Cluster is being created on Google Container Engine...')
-
- Clusters::Cluster.last.provider.make_errored!('Something wrong!')
-
- expect(page).to have_content('Something wrong!')
- end
- end
-
- context 'when user filled form with invalid parameters' do
- before do
- click_button 'Create cluster'
- end
-
- it 'user sees a validation error' do
- expect(page).to have_css('#error_explanation')
- end
- end
- end
-
- context 'when user has a cluster and visits cluster index page' do
- let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
-
- before do
- visit project_clusters_path(project)
- end
-
- it 'user sees an cluster details page' do
- expect(page).to have_button('Save')
- expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
-
- # Application Installation buttons
- page.within('.js-cluster-application-row-helm') do
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
- end
- end
-
- context 'when user installs application: Helm Tiller' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
-
- page.within('.js-cluster-application-row-helm') do
- page.find(:css, '.js-cluster-application-install-button').click
- end
- end
-
- it 'user sees status transition' do
- page.within('.js-cluster-application-row-helm') do
- # FE sends request and gets the response, then the buttons is "Install"
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
-
- Clusters::Cluster.last.application_helm.make_installing!
-
- # FE starts polling and update the buttons to "Installing"
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
-
- Clusters::Cluster.last.application_helm.make_installed!
-
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
- end
-
- expect(page).to have_content('Helm Tiller was successfully installed on your cluster')
- end
- end
-
- context 'when user installs application: Ingress' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
- # Helm Tiller needs to be installed before you can install Ingress
- create(:cluster_applications_helm, :installed, cluster: cluster)
-
- visit project_clusters_path(project)
-
- page.within('.js-cluster-application-row-ingress') do
- page.find(:css, '.js-cluster-application-install-button').click
- end
- end
-
- it 'user sees status transition' do
- page.within('.js-cluster-application-row-ingress') do
- # FE sends request and gets the response, then the buttons is "Install"
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
-
- Clusters::Cluster.last.application_ingress.make_installing!
-
- # FE starts polling and update the buttons to "Installing"
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
-
- Clusters::Cluster.last.application_ingress.make_installed!
-
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
- end
-
- expect(page).to have_content('Ingress was successfully installed on your cluster')
- end
- end
-
- context 'when user disables the cluster' do
- before do
- page.find(:css, '.js-toggle-cluster').click
- click_button 'Save'
- end
-
- it 'user sees the succeccful message' do
- expect(page).to have_content('Cluster was successfully updated.')
- end
- end
-
- context 'when user destory the cluster' do
- before do
- page.accept_confirm do
- click_link 'Remove integration'
- end
- end
-
- it 'user sees creation form with the succeccful message' do
- expect(page).to have_content('Cluster integration was successfully removed.')
- expect(page).to have_link('Create on GKE')
- end
- end
- end
- end
-
- context 'when user has not signed in Google' do
+ context 'when user does not have a cluster and visits cluster index page' do
before do
visit project_clusters_path(project)
click_link 'Create on GKE'
end
- it 'user sees a login page' do
- expect(page).to have_css('.signin-with-google')
+ it 'user sees a new page' do
+ expect(page).to have_button('Create cluster')
end
end
end
diff --git a/spec/fixtures/emails/valid_new_merge_request.eml b/spec/fixtures/emails/valid_new_merge_request.eml
new file mode 100644
index 00000000000..480675a6d7e
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_merge_request.eml
@@ -0,0 +1,18 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: feature
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
diff --git a/spec/fixtures/emails/valid_new_merge_request_no_subject.eml b/spec/fixtures/emails/valid_new_merge_request_no_subject.eml
new file mode 100644
index 00000000000..27eb1b7d922
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_merge_request_no_subject.eml
@@ -0,0 +1,18 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject:
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
index 027e8001053..6d6e71cc215 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -36,6 +36,20 @@ describe('Clusters', () => {
});
});
+ describe('showToken', () => {
+ it('should update tye field type', () => {
+ cluster.showTokenButton.click();
+ expect(
+ cluster.tokenField.getAttribute('type'),
+ ).toEqual('text');
+
+ cluster.showTokenButton.click();
+ expect(
+ cluster.tokenField.getAttribute('type'),
+ ).toEqual('password');
+ });
+ });
+
describe('checkForNewInstalls', () => {
const INITIAL_APP_MAP = {
helm: { status: null, title: 'Helm Tiller' },
@@ -113,7 +127,7 @@ describe('Clusters', () => {
});
describe('when cluster is created', () => {
- it('should show the success container', () => {
+ it('should show the success container and fresh the page', () => {
cluster.updateContainer(null, 'created');
expect(
diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js
index ceee08d47c5..5a9112716f4 100644
--- a/spec/javascripts/issuable_spec.js
+++ b/spec/javascripts/issuable_spec.js
@@ -26,7 +26,7 @@ describe('Issuable', () => {
document.body.appendChild(element);
const input = document.createElement('input');
- input.setAttribute('id', 'issue_email');
+ input.setAttribute('id', 'issuable_email');
document.body.appendChild(input);
Issuable = new IssuableIndex('issue_');
diff --git a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
new file mode 100644
index 00000000000..e361d1a7393
--- /dev/null
+++ b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+require_relative '../email_shared_blocks'
+
+describe Gitlab::Email::Handler::CreateMergeRequestHandler do
+ include_context :email_shared_context
+ it_behaves_like :reply_processing_shared_examples
+
+ before do
+ stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
+ stub_config_setting(host: 'localhost')
+ end
+
+ let(:email_raw) { fixture_file('emails/valid_new_merge_request.eml') }
+ let(:namespace) { create(:namespace, path: 'gitlabhq') }
+
+ # project's git repository is not deleted when project is deleted
+ # between tests. Then tests fail because re-creation of the project with
+ # the same name fails on existing git repository -> skip_disk_validation
+ # ignores repository existence on disk
+ let!(:project) { create(:project, :public, :repository, skip_disk_validation: true, namespace: namespace, path: 'gitlabhq') }
+ let!(:user) do
+ create(
+ :user,
+ email: 'jake@adventuretime.ooo',
+ incoming_email_token: 'auth_token'
+ )
+ end
+
+ context "as a non-developer" do
+ before do
+ project.add_guest(user)
+ end
+
+ it "raises UserNotAuthorizedError if the user is not a member" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError)
+ end
+ end
+
+ context "as a developer" do
+ before do
+ project.add_developer(user)
+ end
+
+ context "when everything is fine" do
+ it "creates a new merge request" do
+ expect { receiver.execute }.to change { project.merge_requests.count }.by(1)
+ merge_request = project.merge_requests.last
+
+ expect(merge_request.author).to eq(user)
+ expect(merge_request.source_branch).to eq('feature')
+ expect(merge_request.title).to eq('Feature added')
+ expect(merge_request.target_branch).to eq(project.default_branch)
+ end
+ end
+
+ context "something is wrong" do
+ context "when the merge request could not be saved" do
+ before do
+ allow_any_instance_of(MergeRequest).to receive(:save).and_return(false)
+ end
+
+ it "raises an InvalidMergeRequestError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidMergeRequestError)
+ end
+ end
+
+ context "when we can't find the incoming_email_token" do
+ let(:email_raw) { fixture_file("emails/wrong_incoming_email_token.eml") }
+
+ it "raises an UserNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError)
+ end
+ end
+
+ context "when the subject is blank" do
+ let(:email_raw) { fixture_file("emails/valid_new_merge_request_no_subject.eml") }
+
+ it "raises an InvalidMergeRequestError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidMergeRequestError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb
new file mode 100644
index 00000000000..650b01c4df4
--- /dev/null
+++ b/spec/lib/gitlab/email/handler_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::Email::Handler do
+ describe '.for' do
+ it 'picks issue handler if there is not merge request prefix' do
+ expect(described_class.for('email', 'project+key')).to be_an_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
+ end
+
+ it 'picks merge request handler if there is merge request key' do
+ expect(described_class.for('email', 'project+merge-request+key')).to be_an_instance_of(Gitlab::Email::Handler::CreateMergeRequestHandler)
+ end
+
+ it 'returns nil if no handler is found' do
+ expect(described_class.for('email', '')).to be_nil
+ end
+ end
+end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 90b768f595e..3817f20bfe7 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -531,7 +531,7 @@ describe Namespace do
end
end
- describe '#has_forks_of?' do
+ describe '#find_fork_of?' do
let(:project) { create(:project, :public) }
let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) }
@@ -550,5 +550,13 @@ describe Namespace do
expect(other_namespace.find_fork_of(project)).to eq(other_fork)
end
+
+ context 'with request store enabled', :request_store do
+ it 'only queries once' do
+ expect(project.fork_network).to receive(:find_forks_in).once.and_call_original
+
+ 2.times { namespace.find_fork_of(project) }
+ end
+ end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 000d5b7126d..6bda1eb15a8 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -451,7 +451,7 @@ describe Project do
end
end
- describe "#new_issue_address" do
+ describe "#new_issuable_address" do
let(:project) { create(:project, path: "somewhere") }
let(:user) { create(:user) }
@@ -463,7 +463,13 @@ describe Project do
it 'returns the address to create a new issue' do
address = "p+#{project.full_path}+#{user.incoming_email_token}@gl.ab"
- expect(project.new_issue_address(user)).to eq(address)
+ expect(project.new_issuable_address(user, 'issue')).to eq(address)
+ end
+
+ it 'returns the address to create a new merge request' do
+ address = "p+#{project.full_path}+merge-request+#{user.incoming_email_token}@gl.ab"
+
+ expect(project.new_issuable_address(user, 'merge_request')).to eq(address)
end
end
@@ -473,7 +479,11 @@ describe Project do
end
it 'returns nil' do
- expect(project.new_issue_address(user)).to be_nil
+ expect(project.new_issuable_address(user, 'issue')).to be_nil
+ end
+
+ it 'returns nil' do
+ expect(project.new_issuable_address(user, 'merge_request')).to be_nil
end
end
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index fee293760f5..b5c92e681fb 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -39,6 +39,7 @@ describe MergeRequests::BuildService do
describe '#execute' do
it 'calls the compare service with the correct arguments' do
+ allow_any_instance_of(described_class).to receive(:branches_valid?).and_return(true)
expect(CompareService).to receive(:new)
.with(project, Gitlab::Git::BRANCH_REF_PREFIX + source_branch)
.and_call_original
diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb
index 369775db462..8cf8f45a8b2 100644
--- a/spec/support/query_recorder.rb
+++ b/spec/support/query_recorder.rb
@@ -41,7 +41,8 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
supports_block_expectations
match do |block|
- query_count(&block) > expected_count + threshold
+ @subject_block = block
+ actual_count > expected_count + threshold
end
failure_message_when_negated do |actual|
@@ -55,6 +56,11 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
self
end
+ def for_query(query)
+ @query = query
+ self
+ end
+
def threshold
@threshold.to_i
end
@@ -68,12 +74,15 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
end
def actual_count
- @recorder.count
+ @actual_count ||= if @query
+ recorder.log.select { |recorded| recorded =~ @query }.size
+ else
+ recorder.count
+ end
end
- def query_count(&block)
- @recorder = ActiveRecord::QueryRecorder.new(&block)
- @recorder.count
+ def recorder
+ @recorder ||= ActiveRecord::QueryRecorder.new(&@subject_block)
end
def count_queries(queries)