Commit 7c526a73 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2017-12-05

# Conflicts:
#	app/assets/javascripts/sidebar/sidebar_bundle.js
#	app/views/shared/issuable/_nav.html.haml
#	lib/gitlab/git/repository.rb
#	spec/models/namespace_spec.rb

[ci skip]
parents 97f24dbd 9f75b7a4
......@@ -416,7 +416,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.54.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.58.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -300,7 +300,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.54.0)
gitaly-proto (0.58.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -1072,7 +1072,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.54.0)
gitaly-proto (~> 0.58.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......
......@@ -25,6 +25,7 @@ class ListIssue {
this.isLoading = {
weight: false,
};
this.isLoading = {};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id;
......
......@@ -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');
......
......@@ -3,3 +3,4 @@ import './polyfills';
import './jquery';
import './bootstrap';
import './vue';
import '../lib/utils/axios_utils';
......@@ -78,11 +78,13 @@
<div class="ci-job-component">
<a
v-tooltip
v-if="job.status.details_path"
v-if="job.status.has_details"
:href="job.status.details_path"
:title="tooltipText"
:class="cssClassJobName"
data-container="body">
data-container="body"
class="js-pipeline-graph-job-link"
>
<job-name-component
:name="job.name"
......@@ -95,7 +97,8 @@
v-tooltip
:title="tooltipText"
:class="cssClassJobName"
data-container="body">
data-container="body"
>
<job-name-component
:name="job.name"
......
<<<<<<< HEAD
import mountSidebarEE from 'ee/sidebar/mount_sidebar';
import Mediator from 'ee/sidebar/sidebar_mediator';
=======
import Mediator from './sidebar_mediator';
>>>>>>> upstream/master
import mountSidebar from './mount_sidebar';
function domContentLoaded() {
......@@ -8,7 +12,10 @@ function domContentLoaded() {
mediator.fetch();
mountSidebar(mediator);
<<<<<<< HEAD
mountSidebarEE(mediator);
=======
>>>>>>> upstream/master
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
......
......@@ -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 ');
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
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
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)
@cluster ||= project.clusters.find(params[:id])
.present(current_user: current_user)
end
def create_params
def update_params
if cluster.managed?
params.require(:cluster).permit(
:enabled,
platform_kubernetes_attributes: [
:namespace
]
)
else
params.require(:cluster).permit(
:enabled,
:name,
:provider_type,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type
])
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
platform_kubernetes_attributes: [
:api_url,
:token,
:ca_cert,
:namespace
]
)
end
end
......
......@@ -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)
......
......@@ -36,12 +36,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?
......@@ -175,6 +178,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
......@@ -140,8 +140,18 @@ class Namespace < ActiveRecord::Base
def find_fork_of(project)
return nil unless project.fork_network
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?
# User namespace will always default to the global setting
......
......@@ -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|
......
......@@ -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
= render 'devise/shared/tab_single', tab_title:'Change your password'
.login-box
.login-body
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'gl-show-field-errors' }) do |f|
= form_for(resource, as: resource_name, url: password_path(:user), html: { method: :put, class: 'gl-show-field-errors' }) do |f|
.devise-errors
= devise_error_messages!
= f.hidden_field :reset_password_token
......@@ -17,5 +17,5 @@
.clearfix.prepend-top-20
%p
%span.light Didn't receive a confirmation email?
= link_to "Request a new one", new_confirmation_path(resource_name)
= link_to "Request a new one", new_confirmation_path(:user)
= render 'devise/shared/sign_in_link'
......@@ -11,6 +11,6 @@
= f.check_box :remember_me, class: 'remember-me-checkbox'
%span Remember me
.pull-right.forgot-password
= link_to "Forgot your password?", new_password_path(resource_name)
= link_to "Forgot your password?", new_password_path(:user)
.submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save"
<%- if controller_name != 'sessions' %>
<%= link_to "Sign in", new_session_path(resource_name), class: "btn" %><br />
<%= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: "btn" %><br />
<% end -%>
<%- if devise_mapping.registerable? && controller_name != 'registrations' && allow_signup? %>
<%= link_to "Sign up", new_registration_path(resource_name) %><br />
<%= link_to "Sign up", new_registration_path(:user) %><br />
<% end -%>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' %>
<%= link_to "Forgot your password?", new_password_path(resource_name), class: "btn" %><br />
<%= link_to "Forgot your password?", new_password_path(:user), class: "btn" %><br />
<% end -%>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(:user) %><br />
<% end -%>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(:user) %><br />
<% end -%>
%p
%span.light
Already have login and password?
= link_to "Sign in", new_session_path(resource_name)
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes')
......@@ -32,4 +32,4 @@
%p
%span.light Didn't receive a confirmation email?
= succeed '.' do
= link_to "Request a new one", new_confirmation_path(resource_name)
= link_to "Request a new one", new_confirmation_path(:user)
......@@ -9,7 +9,7 @@
%p Try logging in using your username or email. If you have forgotten your password, try recovering it
= link_to "Sign in", new_session_path(:user), class: 'btn primary'
= link_to "Recover password", new_password_path(resource_name), class: 'btn secondary'
= link_to "Recover password", new_password_path(:user), class: 'btn secondary'
%hr
%p.light If none of the options work, try contacting a GitLab administrator.
......@@ -160,7 +160,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')
......@@ -168,7 +168,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') }
......@@ -197,18 +197,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
......
- if can?(current_user, :admin_cluster, @cluster)
- if @cluster.managed?
.append-bottom-20
%label.append-bottom-10
= s_('ClusterIntegration|Google Container Engine')
......
%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.')
%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))
= 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'
.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'
%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'
%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
......
.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'
......@@ -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
......
......@@ -3,8 +3,8 @@
.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'
= render 'form'
= render 'form'
......@@ -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')
%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|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'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20'
= link_to s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
......@@ -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'
= 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'
%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 }
= 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'
- 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'
- type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false)
<<<<<<< HEAD
- issuables = @issues || @merge_requests || @epics
=======
>>>>>>> upstream/master
%ul.nav-links.issues-state-filters
- if type != :epics
......@@ -21,6 +24,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)
- 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}
---
title: Create a new form to add Existing Kubernetes Cluster
merge_request: 14805
author:
type: added
---
title: Confirming email with invalid token should no longer generate an error
merge_request: 15726
author:
type: fixed
---
title: Add axios to common file
merge_request:
author:
type: performance
---
title: Reduce requests for project forks on show page of projects that have forks
merge_request: 15663
author:
type: performance
---
title: Use custom user agent header in all GCP API requests.
merge_request: 15705
author:
type: changed
......@@ -218,10 +218,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
......
......@@ -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:
......
......@@ -418,6 +418,20 @@ module Gitlab
parent_ids.size > 1
end
def to_gitaly_commit
return raw_commit if raw_commit.is_a?(Gitaly::GitCommit)
message_split = raw_commit.message.split("\n", 2)
Gitaly::GitCommit.new(
id: raw_commit.oid,
subject: message_split[0] ? message_split[0].chomp.b : "",
body: raw_commit.message.b,
parent_ids: raw_commit.parent_ids,
author: gitaly_commit_author_from_rugged(raw_commit.author),
committer: gitaly_commit_author_from_rugged(raw_commit.committer)
)
end
private
def init_from_hash(hash)
......@@ -463,6 +477,14 @@ module Gitlab
def serialize_keys
SERIALIZE_KEYS
end
def gitaly_commit_author_from_rugged(author_or_committer)
Gitaly::CommitAuthor.new(
name: author_or_committer[:name].b,
email: author_or_committer[:email].b,
date: Google::Protobuf::Timestamp.new(seconds: author_or_committer[:time].to_i)
)
end
end
end
end
......@@ -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
......
......@@ -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
......
......@@ -18,7 +18,12 @@ module Gitlab
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
].freeze
SEARCH_CONTEXT_LINES = 3
<<<<<<< HEAD
GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze
=======
REBASE_WORKTREE_PREFIX = 'rebase'.freeze
SQUASH_WORKTREE_PREFIX = 'squash'.freeze
>>>>>>> upstream/master
NoRepository = Class.new(StandardError)
InvalidBlobName = Class.new(StandardError)
......@@ -810,42 +815,22 @@ module Gitlab
end
def cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch(
branch_name,
gitaly_migrate(:cherry_pick) do |is_enabled|
args = {
user: user,
commit: commit,
branch_name: branch_name,
message: message,
start_branch_name: start_branch_name,
start_repository: start_repository
) do |start_commit|
Gitlab::Git.check_namespace!(commit, start_repository)
cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha)
raise CreateTreeError unless cherry_pick_tree_id
committer = user_to_committer(user)
}
create_commit(message: message,
author: {
email: commit.author_email,
name: commit.author_name,
time: commit.authored_date
},
committer: committer,
tree: cherry_pick_tree_id,
parents: [start_commit.sha])
if is_enabled
gitaly_operations_client.user_cherry_pick(args)
else
rugged_cherry_pick(args)
end
end
def check_cherry_pick_content(target_commit, source_sha)
args = [target_commit.sha, source_sha]
args << 1 if target_commit.merge_commit?
cherry_pick_index = rugged.cherrypick_commit(*args)
return false if cherry_pick_index.conflicts?
tree_id = cherry_pick_index.write_tree(rugged)
return false unless diff_exists?(source_sha, tree_id)
tree_id
end
def diff_exists?(sha1, sha2)
......@@ -1091,13 +1076,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:)
......@@ -1119,14 +1099,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
......@@ -1209,6 +1197,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
......@@ -1245,6 +1291,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
......@@ -1256,6 +1353,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
......@@ -1687,6 +1802,45 @@ module Gitlab
raise InvalidRef, ex
end
def rugged_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch(
branch_name,
start_branch_name: start_branch_name,
start_repository: start_repository
) do |start_commit|
Gitlab::Git.check_namespace!(commit, start_repository)
cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha)
raise CreateTreeError unless cherry_pick_tree_id
committer = user_to_committer(user)
create_commit(message: message,
author: {
email: commit.author_email,
name: commit.author_name,
time: commit.authored_date
},
committer: committer,
tree: cherry_pick_tree_id,
parents: [start_commit.sha])
end
end
def check_cherry_pick_content(target_commit, source_sha)
args = [target_commit.sha, source_sha]
args << 1 if target_commit.merge_commit?
cherry_pick_index = rugged.cherrypick_commit(*args)
return false if cherry_pick_index.conflicts?
tree_id = cherry_pick_index.write_tree(rugged)
return false unless diff_exists?(source_sha, tree_id)
tree_id
end
def local_fetch_ref(source_path, source_ref:, target_ref:)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
run_git(args)
......
......@@ -122,6 +122,36 @@ module Gitlab
).branch_update
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update)
end
def user_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
request = Gitaly::UserCherryPickRequest.new(
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit: commit.to_gitaly_commit,
branch_name: GitalyClient.encode(branch_name),
message: GitalyClient.encode(message),
start_branch_name: GitalyClient.encode(start_branch_name.to_s),
start_repository: start_repository.gitaly_repository
)
response = GitalyClient.call(
@repository.storage,
:operation_service,
:user_cherry_pick,
request,
remote_storage: start_repository.storage
)
if response.pre_receive_error.presence
raise Gitlab::Git::HooksService::PreReceiveError, response.pre_receive_error
elsif response.commit_error.presence
raise Gitlab::Git::CommitError, response.commit_error
elsif response.create_tree_error.presence
raise Gitlab::Git::Repository::CreateTreeError, response.create_tree_error
else
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
end
end
end
end
end
......@@ -44,7 +44,7 @@ module GoogleApi
service = Google::Apis::ContainerV1::ContainerService.new
service.authorization = access_token
service.get_zone_cluster(project_id, zone, cluster_id)
service.get_zone_cluster(project_id, zone, cluster_id, options: user_agent_header)
end
def projects_zones_clusters_create(project_id, zone, cluster_name, cluster_size, machine_type:)
......@@ -62,14 +62,14 @@ module GoogleApi
}
} )
service.create_cluster(project_id, zone, request_body)
service.create_cluster(project_id, zone, request_body, options: user_agent_header)
end
def projects_zones_operations(project_id, zone, operation_id)
service = Google::Apis::ContainerV1::ContainerService.new
service.authorization = access_token
service.get_zone_operation(project_id, zone, operation_id)
service.get_zone_operation(project_id, zone, operation_id, options: user_agent_header)
end
def parse_operation_id(self_link)
......@@ -82,6 +82,12 @@ module GoogleApi
def token_life_time(expires_at)
DateTime.strptime(expires_at, '%s').to_time.utc - Time.now.utc
end
def user_agent_header
Google::Apis::RequestOptions.new.tap do |options|
options.header = { 'User-Agent': "GitLab/#{Gitlab::VERSION.match('(\d+\.\d+)').captures.first} (GPN:GitLab;)" }
end
end
end
end
end
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
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
......@@ -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
describe 'security' do
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 }
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 } }
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,6 +239,10 @@ describe Projects::ClustersController do
sign_in(user)
end
context 'when cluster is provided by GCP' do
context 'when cluster is created' do
let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
it "destroys and redirects back to clusters list" do
expect { go }
.to change { Clusters::Cluster.count }.by(-1)
......@@ -399,9 +252,10 @@ describe Projects::ClustersController do
expect(response).to redirect_to(project_clusters_path(project))
expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
end
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 "destroys and redirects back to clusters list" do
expect { go }
......@@ -412,9 +266,10 @@ describe Projects::ClustersController do
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) }
......
......@@ -284,6 +284,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
......
......@@ -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
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
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
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
......@@ -3,22 +3,14 @@ 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)
......@@ -29,178 +21,5 @@ feature 'Clusters', :js do
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
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
......@@ -185,6 +185,36 @@ describe 'Pipeline', :js do
end
end
context 'when user does not have access to read jobs' do
before do
project.update(public_builds: false)
end
describe 'GET /:project/pipelines/:id' do
include_context 'pipeline builds'
let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }
before do
visit project_pipeline_path(project, pipeline)
end
it 'shows the pipeline graph' do
expect(page).to have_selector('.pipeline-visualization')
expect(page).to have_content('Build')
expect(page).to have_content('Test')
expect(page).to have_content('Deploy')
expect(page).to have_content('Retry')
expect(page).to have_content('Cancel running')
end
it 'should not link to job' do
expect(page).not_to have_selector('.js-pipeline-graph-job-link')
end
end
end
describe 'GET /:project/pipelines/:id/builds' do
include_context 'pipeline builds'
......
......@@ -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(
......
import Vue from 'vue';
import jobComponent from '~/pipelines/components/graph/job_component.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('pipeline graph job component', () => {
let JobComponent;
let component;
const mockJob = {
id: 4256,
......@@ -13,6 +15,7 @@ describe('pipeline graph job component', () => {
label: 'passed',
group: 'success',
details_path: '/root/ci-mock/builds/4256',
has_details: true,
action: {
icon: 'retry',
title: 'Retry',
......@@ -26,13 +29,13 @@ describe('pipeline graph job component', () => {
JobComponent = Vue.extend(jobComponent);
});
afterEach(() => {
component.$destroy();
});
describe('name with link', () => {
it('should render the job name and status with a link', (done) => {
const component = new JobComponent({
propsData: {
job: mockJob,
},
}).$mount();
component = mountComponent(JobComponent, { job: mockJob });
Vue.nextTick(() => {
const link = component.$el.querySelector('a');
......@@ -56,8 +59,7 @@ describe('pipeline graph job component', () => {
describe('name without link', () => {
it('it should render status and name', () => {
const component = new JobComponent({
propsData: {
component = mountComponent(JobComponent, {
job: {
id: 4256,
name: 'test',
......@@ -67,12 +69,13 @@ describe('pipeline graph job component', () => {
label: 'passed',
group: 'success',
details_path: '/root/ci-mock/builds/4256',
has_details: false,
},
},
},
}).$mount();
});
expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined();
expect(component.$el.querySelector('a')).toBeNull();
expect(
component.$el.querySelector('.ci-status-text').textContent.trim(),
......@@ -82,11 +85,7 @@ describe('pipeline graph job component', () => {
describe('action icon', () => {
it('it should render the action icon', () => {
const component = new JobComponent({
propsData: {
job: mockJob,
},
}).$mount();
component = mountComponent(JobComponent, { job: mockJob });
expect(component.$el.querySelector('a.ci-action-icon-container')).toBeDefined();
expect(component.$el.querySelector('i.ci-action-icon-wrapper')).toBeDefined();
......@@ -95,24 +94,20 @@ describe('pipeline graph job component', () => {
describe('dropdown', () => {
it('should render the dropdown action icon', () => {
const component = new JobComponent({
propsData: {
component = mountComponent(JobComponent, {
job: mockJob,
isDropdown: true,
},
}).$mount();
});
expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined();
});
});
it('should render provided class name', () => {
const component = new JobComponent({
propsData: {
component = mountComponent(JobComponent, {
job: mockJob,
cssClassJobName: 'css-class-job-name',
},
}).$mount();
});
expect(
component.$el.querySelector('a').classList.contains('css-class-job-name'),
......
......@@ -3,6 +3,7 @@ require 'spec_helper'
describe GoogleApi::CloudPlatform::Client do
let(:token) { 'token' }
let(:client) { described_class.new(token, nil) }
let(:user_agent_options) { client.instance_eval { user_agent_header } }
describe '.session_key_for_redirect_uri' do
let(:state) { 'random_string' }
......@@ -55,7 +56,8 @@ describe GoogleApi::CloudPlatform::Client do
before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
.to receive(:get_zone_cluster).and_return(gke_cluster)
.to receive(:get_zone_cluster).with(any_args, options: user_agent_options)
.and_return(gke_cluster)
end
it { is_expected.to eq(gke_cluster) }
......@@ -74,7 +76,8 @@ describe GoogleApi::CloudPlatform::Client do
before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
.to receive(:create_cluster).and_return(operation)
.to receive(:create_cluster).with(any_args, options: user_agent_options)
.and_return(operation)
end
it { is_expected.to eq(operation) }
......@@ -102,7 +105,8 @@ describe GoogleApi::CloudPlatform::Client do
before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
.to receive(:get_zone_operation).and_return(operation)
.to receive(:get_zone_operation).with(any_args, options: user_agent_options)
.and_return(operation)
end
it { is_expected.to eq(operation) }
......@@ -125,4 +129,18 @@ describe GoogleApi::CloudPlatform::Client do
it { is_expected.to be_nil }
end
end
describe '#user_agent_header' do
subject { client.instance_eval { user_agent_header } }
it 'returns a RequestOptions object' do
expect(subject).to be_instance_of(Google::Apis::RequestOptions)
end
it 'has the correct GitLab version in User-Agent header' do
stub_const('Gitlab::VERSION', '10.3.0-pre')
expect(subject.header).to eq({ 'User-Agent': 'GitLab/10.3 (GPN:GitLab;)' })
end
end
end
......@@ -543,6 +543,7 @@ describe Namespace do
end
end
<<<<<<< HEAD
describe '#share_with_group_lock with subgroups', :nested_groups do
context 'when creating a subgroup' do
let(:subgroup) { create(:group, parent: root_group )}
......@@ -656,6 +657,9 @@ describe Namespace do
end
describe '#has_forks_of?' do
=======
describe '#find_fork_of?' do
>>>>>>> upstream/master
let(:project) { create(:project, :public) }
let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) }
......@@ -674,6 +678,14 @@ 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
describe '#root_ancestor' do
......
......@@ -1408,6 +1408,7 @@ describe Repository do
end
describe '#cherry_pick' do
shared_examples 'cherry-picking a commit' do
let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
......@@ -1446,6 +1447,15 @@ describe Repository do
end
end
context 'when Gitaly cherry_pick feature is enabled' do
it_behaves_like 'cherry-picking a commit'
end
context 'when Gitaly cherry_pick feature is disabled', :disable_gitaly do
it_behaves_like 'cherry-picking a commit'
end
end
describe '#before_delete' do
describe 'when a repository does not exist' do
before do
......
......@@ -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)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment