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 ...@@ -416,7 +416,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # 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 gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -300,7 +300,7 @@ GEM ...@@ -300,7 +300,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.54.0) gitaly-proto (0.58.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -1072,7 +1072,7 @@ DEPENDENCIES ...@@ -1072,7 +1072,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.54.0) gitaly-proto (~> 0.58.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
......
...@@ -25,6 +25,7 @@ class ListIssue { ...@@ -25,6 +25,7 @@ class ListIssue {
this.isLoading = { this.isLoading = {
weight: false, weight: false,
}; };
this.isLoading = {};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint; this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id; this.milestone_id = obj.milestone_id;
......
...@@ -48,6 +48,7 @@ export default class Clusters { ...@@ -48,6 +48,7 @@ export default class Clusters {
this.toggle = this.toggle.bind(this); this.toggle = this.toggle.bind(this);
this.installApplication = this.installApplication.bind(this); this.installApplication = this.installApplication.bind(this);
this.showToken = this.showToken.bind(this);
this.toggleButton = document.querySelector('.js-toggle-cluster'); this.toggleButton = document.querySelector('.js-toggle-cluster');
this.toggleInput = document.querySelector('.js-toggle-input'); this.toggleInput = document.querySelector('.js-toggle-input');
...@@ -56,6 +57,8 @@ export default class Clusters { ...@@ -56,6 +57,8 @@ export default class Clusters {
this.creatingContainer = document.querySelector('.js-cluster-creating'); this.creatingContainer = document.querySelector('.js-cluster-creating');
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason'); this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
this.successApplicationContainer = document.querySelector('.js-cluster-application-notice'); this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token');
initSettingsPanels(); initSettingsPanels();
this.initApplications(); this.initApplications();
...@@ -97,11 +100,13 @@ export default class Clusters { ...@@ -97,11 +100,13 @@ export default class Clusters {
addListeners() { addListeners() {
this.toggleButton.addEventListener('click', this.toggle); this.toggleButton.addEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication); eventHub.$on('installApplication', this.installApplication);
} }
removeListeners() { removeListeners() {
this.toggleButton.removeEventListener('click', this.toggle); this.toggleButton.removeEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication); eventHub.$off('installApplication', this.installApplication);
} }
...@@ -149,6 +154,16 @@ export default class Clusters { ...@@ -149,6 +154,16 @@ export default class Clusters {
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString()); 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() { hideAll() {
this.errorContainer.classList.add('hidden'); this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden'); this.successContainer.classList.add('hidden');
......
...@@ -3,3 +3,4 @@ import './polyfills'; ...@@ -3,3 +3,4 @@ import './polyfills';
import './jquery'; import './jquery';
import './bootstrap'; import './bootstrap';
import './vue'; import './vue';
import '../lib/utils/axios_utils';
...@@ -78,11 +78,13 @@ ...@@ -78,11 +78,13 @@
<div class="ci-job-component"> <div class="ci-job-component">
<a <a
v-tooltip v-tooltip
v-if="job.status.details_path" v-if="job.status.has_details"
:href="job.status.details_path" :href="job.status.details_path"
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="cssClassJobName"
data-container="body"> data-container="body"
class="js-pipeline-graph-job-link"
>
<job-name-component <job-name-component
:name="job.name" :name="job.name"
...@@ -95,7 +97,8 @@ ...@@ -95,7 +97,8 @@
v-tooltip v-tooltip
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="cssClassJobName"
data-container="body"> data-container="body"
>
<job-name-component <job-name-component
:name="job.name" :name="job.name"
......
<<<<<<< HEAD
import mountSidebarEE from 'ee/sidebar/mount_sidebar'; import mountSidebarEE from 'ee/sidebar/mount_sidebar';
import Mediator from 'ee/sidebar/sidebar_mediator'; import Mediator from 'ee/sidebar/sidebar_mediator';
=======
import Mediator from './sidebar_mediator';
>>>>>>> upstream/master
import mountSidebar from './mount_sidebar'; import mountSidebar from './mount_sidebar';
function domContentLoaded() { function domContentLoaded() {
...@@ -8,7 +12,10 @@ function domContentLoaded() { ...@@ -8,7 +12,10 @@ function domContentLoaded() {
mediator.fetch(); mediator.fetch();
mountSidebar(mediator); mountSidebar(mediator);
<<<<<<< HEAD
mountSidebarEE(mediator); mountSidebarEE(mediator);
=======
>>>>>>> upstream/master
} }
document.addEventListener('DOMContentLoaded', domContentLoaded); document.addEventListener('DOMContentLoaded', domContentLoaded);
......
...@@ -8,3 +8,9 @@ ...@@ -8,3 +8,9 @@
// Wait for the Vue to kick-in and render the applications block // Wait for the Vue to kick-in and render the applications block
min-height: 302px; 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 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_read_cluster!
before_action :authorize_create_cluster!, only: [:new, :new_gcp, :create] before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_google_api, only: [:new_gcp, :create]
before_action :authorize_update_cluster!, only: [:update] before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy] before_action :authorize_admin_cluster!, only: [:destroy]
STATUS_POLLING_INTERVAL = 10_000
def index def index
if project.cluster if project.cluster
redirect_to project_cluster_path(project, project.cluster) redirect_to project_cluster_path(project, project.cluster)
...@@ -14,43 +15,13 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -14,43 +15,13 @@ class Projects::ClustersController < Projects::ApplicationController
end end
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 def new
end 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 def status
respond_to do |format| respond_to do |format|
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000) Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL)
render json: ClusterSerializer render json: ClusterSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
...@@ -88,46 +59,29 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -88,46 +59,29 @@ class Projects::ClustersController < Projects::ApplicationController
private private
def cluster def cluster
@cluster ||= project.cluster.present(current_user: current_user) @cluster ||= project.clusters.find(params[:id])
end .present(current_user: current_user)
def create_params
params.require(:cluster).permit(
:enabled,
:name,
:provider_type,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type
])
end end
def update_params def update_params
params.require(:cluster).permit(:enabled) if cluster.managed?
end params.require(:cluster).permit(
:enabled,
def authorize_google_api platform_kubernetes_attributes: [
unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil) :namespace
.validate_token(expires_at_in_session) ]
redirect_to action: 'login' )
end else
end params.require(:cluster).permit(
:enabled,
def token_in_session :name,
@token_in_session ||= platform_kubernetes_attributes: [
session[GoogleApi::CloudPlatform::Client.session_key_for_token] :api_url,
end :token,
:ca_cert,
def expires_at_in_session :namespace
@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 end
......
...@@ -17,7 +17,7 @@ module Clusters ...@@ -17,7 +17,7 @@ module Clusters
# we force autosave to happen when we save `Cluster` model # we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true 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_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress' has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
...@@ -70,6 +70,10 @@ module Clusters ...@@ -70,6 +70,10 @@ module Clusters
return platform_kubernetes if kubernetes? return platform_kubernetes if kubernetes?
end end
def managed?
!user?
end
def first_project def first_project
return @first_project if defined?(@first_project) return @first_project if defined?(@first_project)
......
...@@ -36,12 +36,15 @@ module Clusters ...@@ -36,12 +36,15 @@ module Clusters
validates :api_url, url: true, presence: true validates :api_url, url: true, presence: true
validates :token, presence: true validates :token, presence: true
validate :prevent_modification, on: :update
after_save :clear_reactive_cache! after_save :clear_reactive_cache!
alias_attribute :ca_pem, :ca_cert alias_attribute :ca_pem, :ca_cert
delegate :project, to: :cluster, allow_nil: true delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, to: :cluster, allow_nil: true delegate :enabled?, to: :cluster, allow_nil: true
delegate :managed?, to: :cluster, allow_nil: true
alias_method :active?, :enabled? alias_method :active?, :enabled?
...@@ -175,6 +178,17 @@ module Clusters ...@@ -175,6 +178,17 @@ module Clusters
def enforce_namespace_to_lower_case def enforce_namespace_to_lower_case
self.namespace = self.namespace&.downcase self.namespace = self.namespace&.downcase
end 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 end
end end
...@@ -140,7 +140,17 @@ class Namespace < ActiveRecord::Base ...@@ -140,7 +140,17 @@ class Namespace < ActiveRecord::Base
def find_fork_of(project) def find_fork_of(project)
return nil unless project.fork_network return nil unless project.fork_network
project.fork_network.find_forks_in(projects).first if RequestStore.active?
forks_in_namespace = RequestStore.fetch("namespaces:#{id}:forked_projects") do
Hash.new do |found_forks, project|
found_forks[project] = project.fork_network.find_forks_in(projects).first
end
end
forks_in_namespace[project]
else
project.fork_network.find_forks_in(projects).first
end
end end
def lfs_enabled? def lfs_enabled?
......
...@@ -2,7 +2,7 @@ module Clusters ...@@ -2,7 +2,7 @@ module Clusters
class CreateService < BaseService class CreateService < BaseService
attr_reader :access_token attr_reader :access_token
def execute(access_token) def execute(access_token = nil)
@access_token = access_token @access_token = access_token
create_cluster.tap do |cluster| create_cluster.tap do |cluster|
......
...@@ -3,11 +3,7 @@ ...@@ -3,11 +3,7 @@
# Custom validator for ClusterName. # Custom validator for ClusterName.
class ClusterNameValidator < ActiveModel::EachValidator class ClusterNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
if record.user? if record.managed?
unless value.present?
record.errors.add(attribute, " has to be present")
end
elsif record.gcp?
if record.persisted? && record.name_changed? if record.persisted? && record.name_changed?
record.errors.add(attribute, " can not be changed because it's synchronized with provider") record.errors.add(attribute, " can not be changed because it's synchronized with provider")
end end
...@@ -19,6 +15,10 @@ class ClusterNameValidator < ActiveModel::EachValidator ...@@ -19,6 +15,10 @@ class ClusterNameValidator < ActiveModel::EachValidator
unless value =~ Gitlab::Regex.kubernetes_namespace_regex unless value =~ Gitlab::Regex.kubernetes_namespace_regex
record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message) record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message)
end end
else
unless value.present?
record.errors.add(attribute, " has to be present")
end
end end
end end
end end
= render 'devise/shared/tab_single', tab_title:'Change your password' = render 'devise/shared/tab_single', tab_title:'Change your password'
.login-box .login-box
.login-body .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-errors
= devise_error_messages! = devise_error_messages!
= f.hidden_field :reset_password_token = f.hidden_field :reset_password_token
...@@ -17,5 +17,5 @@ ...@@ -17,5 +17,5 @@
.clearfix.prepend-top-20 .clearfix.prepend-top-20
%p %p
%span.light Didn't receive a confirmation email? %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' = render 'devise/shared/sign_in_link'
...@@ -11,6 +11,6 @@ ...@@ -11,6 +11,6 @@
= f.check_box :remember_me, class: 'remember-me-checkbox' = f.check_box :remember_me, class: 'remember-me-checkbox'
%span Remember me %span Remember me
.pull-right.forgot-password .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 .submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save" = f.submit "Sign in", class: "btn btn-save"
<%- if controller_name != 'sessions' %> <%- 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 -%> <% end -%>
<%- if devise_mapping.registerable? && controller_name != 'registrations' && allow_signup? %> <%- 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 -%> <% end -%>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' %> <%- 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 -%> <% end -%>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> <%- 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 -%> <% end -%>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> <%- 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 -%> <% end -%>
%p %p
%span.light %span.light
Already have login and password? 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 @@ ...@@ -32,4 +32,4 @@
%p %p
%span.light Didn't receive a confirmation email? %span.light Didn't receive a confirmation email?
= succeed '.' do = 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 @@ ...@@ -9,7 +9,7 @@
%p Try logging in using your username or email. If you have forgotten your password, try recovering it %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 "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 %hr
%p.light If none of the options work, try contacting a GitLab administrator. %p.light If none of the options work, try contacting a GitLab administrator.
...@@ -160,7 +160,7 @@ ...@@ -160,7 +160,7 @@
= number_with_delimiter(@project.open_merge_requests_count) = number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines - 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 = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container .nav-icon-container
= sprite_icon('pipeline') = sprite_icon('pipeline')
...@@ -168,7 +168,7 @@ ...@@ -168,7 +168,7 @@
CI / CD CI / CD
%ul.sidebar-sub-level-items %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 = link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('CI / CD') } #{ _('CI / CD') }
...@@ -197,18 +197,18 @@ ...@@ -197,18 +197,18 @@
%span %span
Environments 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? - if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do = nav_link(path: 'pipelines#charts') do
= link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
%span %span
Charts 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 - if project_nav_tab? :wiki
= nav_link(controller: :wikis) do = nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
......
- if can?(current_user, :admin_cluster, @cluster) - if can?(current_user, :admin_cluster, @cluster)
.append-bottom-20 - if @cluster.managed?
%label.append-bottom-10 .append-bottom-20
= s_('ClusterIntegration|Google Container Engine') %label.append-bottom-10
%p = s_('ClusterIntegration|Google Container Engine')
- link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') %p
= s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke } - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
.well.form-group .well.form-group
%label.text-danger %label.text-danger
......
%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 %h4.prepend-top-20
= s_('ClusterIntegration|Create new cluster on Google Container Engine') = s_('ClusterIntegration|Enter the details for your cluster')
%p %p
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:') = s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
%ul %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 @@ ...@@ -3,8 +3,9 @@
.row.prepend-top-default .row.prepend-top-default
.col-sm-4 .col-sm-4
= render 'sidebar' = render 'projects/clusters/sidebar'
.col-sm-8 .col-sm-8
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine')
= render 'header' = render 'header'
.row .row
.col-sm-8.col-sm-offset-4.signin-with-google .col-sm-8.col-sm-offset-4.signin-with-google
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
.row.prepend-top-default .row.prepend-top-default
.col-sm-4 .col-sm-4
= render 'sidebar' = render 'projects/clusters/sidebar'
.col-sm-8 .col-sm-8
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine')
= render 'header' = render 'header'
= render 'form'
= render 'form'
...@@ -5,16 +5,9 @@ ...@@ -5,16 +5,9 @@
.col-sm-4 .col-sm-4
= render 'sidebar' = render 'sidebar'
.col-sm-8 .col-sm-8
- if @project.deployment_platform&.active? %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
%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') %p= s_('ClusterIntegration|Create a new cluster on Google Engine right from GitLab')
= link_to s_('ClusterIntegration|Manage Kubernetes integration'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20' = 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')
- else = link_to s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
%p= s_('ClusterIntegration|Create a new cluster on Google Container Engine right from GitLab')
= link_to s_('ClusterIntegration|Create on GKE'), providers_gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
%p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
= link_to s_('ClusterIntegration|Add an existing cluster'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20'
...@@ -13,52 +13,16 @@ ...@@ -13,52 +13,16 @@
cluster_status_reason: @cluster.status_reason, cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications') } } help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications') } }
.js-cluster-application-notice .js-cluster-application-notice
.flash-container .flash-container
%section.settings.no-animate.expanded %section.settings.no-animate.expanded
%h4= s_('ClusterIntegration|Enable cluster integration') = render 'banner'
.settings-content = render 'enabled'
.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'
.cluster-applications-table#js-cluster-applications .cluster-applications-table#js-cluster-applications
%section.settings#js-cluster-details %section.settings#js-cluster-details{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4= s_('ClusterIntegration|Cluster details') %h4= s_('ClusterIntegration|Cluster details')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
...@@ -66,20 +30,16 @@ ...@@ -66,20 +30,16 @@
%p= s_('ClusterIntegration|See and edit the details for your cluster') %p= s_('ClusterIntegration|See and edit the details for your cluster')
.settings-content .settings-content
- if @cluster.managed?
.form_group.append-bottom-20 = render 'projects/clusters/gcp/show'
%label.append-bottom-10{ for: 'cluster-name' } - else
= s_('ClusterIntegration|Cluster name') = render 'projects/clusters/user/show'
.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'))
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) } %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4= _('Advanced settings') %h4= _('Advanced settings')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = 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 .settings-content
= render 'advanced_settings' = 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) - type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false) - page_context_word = type.to_s.humanize(capitalize: false)
<<<<<<< HEAD
- issuables = @issues || @merge_requests || @epics - issuables = @issues || @merge_requests || @epics
=======
>>>>>>> upstream/master
%ul.nav-links.issues-state-filters %ul.nav-links.issues-state-filters
- if type != :epics - if type != :epics
...@@ -21,6 +24,4 @@ ...@@ -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 = 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)} #{issuables_state_counter_text(type, :closed)}
%li{ class: active_when(params[:state] == 'all') }> = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :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)}
- 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 ...@@ -218,10 +218,16 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
resources :clusters, except: [:edit] do resources :clusters, except: [:edit, :create] do
collection do collection do
get :login scope :providers do
get '/providers/gcp/new', action: :new_gcp 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 end
member do member do
......
...@@ -152,12 +152,23 @@ CE and EE. ...@@ -152,12 +152,23 @@ CE and EE.
## Previewing the changes live ## Previewing the changes live
If you want to preview the doc changes of your merge request live, you can use 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:** TIP: **Tip:**
If your branch contains only documentation changes, you can use If your branch contains only documentation changes, you can use
[special branch names](#testing) to avoid long running pipelines. [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) ![Manual trigger a docs build](img/manual_build_docs.png)
This job will: This job will:
......
...@@ -418,6 +418,20 @@ module Gitlab ...@@ -418,6 +418,20 @@ module Gitlab
parent_ids.size > 1 parent_ids.size > 1
end 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 private
def init_from_hash(hash) def init_from_hash(hash)
...@@ -463,6 +477,14 @@ module Gitlab ...@@ -463,6 +477,14 @@ module Gitlab
def serialize_keys def serialize_keys
SERIALIZE_KEYS SERIALIZE_KEYS
end 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 end
end end
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
module Git module Git
module Conflict module Conflict
class File 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) def initialize(repository, commit_oid, conflict, content)
@repository = repository @repository = repository
......
...@@ -75,7 +75,7 @@ module Gitlab ...@@ -75,7 +75,7 @@ module Gitlab
resolved_lines = file.resolve_lines(params[:sections]) resolved_lines = file.resolve_lines(params[:sections])
new_file = resolved_lines.map { |line| line[:full_line] }.join("\n") 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] elsif params[:content]
new_file = file.resolve_content(params[:content]) new_file = file.resolve_content(params[:content])
end end
......
This diff is collapsed.
...@@ -122,6 +122,36 @@ module Gitlab ...@@ -122,6 +122,36 @@ module Gitlab
).branch_update ).branch_update
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update) Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update)
end 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 end
end end
...@@ -44,7 +44,7 @@ module GoogleApi ...@@ -44,7 +44,7 @@ module GoogleApi
service = Google::Apis::ContainerV1::ContainerService.new service = Google::Apis::ContainerV1::ContainerService.new
service.authorization = access_token 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 end
def projects_zones_clusters_create(project_id, zone, cluster_name, cluster_size, machine_type:) def projects_zones_clusters_create(project_id, zone, cluster_name, cluster_size, machine_type:)
...@@ -62,14 +62,14 @@ module GoogleApi ...@@ -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 end
def projects_zones_operations(project_id, zone, operation_id) def projects_zones_operations(project_id, zone, operation_id)
service = Google::Apis::ContainerV1::ContainerService.new service = Google::Apis::ContainerV1::ContainerService.new
service.authorization = access_token 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 end
def parse_operation_id(self_link) def parse_operation_id(self_link)
...@@ -82,6 +82,12 @@ module GoogleApi ...@@ -82,6 +82,12 @@ module GoogleApi
def token_life_time(expires_at) def token_life_time(expires_at)
DateTime.strptime(expires_at, '%s').to_time.utc - Time.now.utc DateTime.strptime(expires_at, '%s').to_time.utc - Time.now.utc
end 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 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
...@@ -284,6 +284,27 @@ describe ProjectsController do ...@@ -284,6 +284,27 @@ describe ProjectsController do
expect(response).to redirect_to(namespace_project_path) expect(response).to redirect_to(namespace_project_path)
end end
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 end
describe "#update" do describe "#update" do
......
...@@ -13,27 +13,20 @@ FactoryGirl.define do ...@@ -13,27 +13,20 @@ FactoryGirl.define do
provider_type :user provider_type :user
platform_type :kubernetes platform_type :kubernetes
platform_kubernetes do platform_kubernetes factory: [:cluster_platform_kubernetes, :configured]
create(:cluster_platform_kubernetes, :configured)
end
end end
trait :provided_by_gcp do trait :provided_by_gcp do
provider_type :gcp provider_type :gcp
platform_type :kubernetes platform_type :kubernetes
before(:create) do |cluster, evaluator| provider_gcp factory: [:cluster_provider_gcp, :created]
cluster.platform_kubernetes = build(:cluster_platform_kubernetes, :configured) platform_kubernetes factory: [:cluster_platform_kubernetes, :configured]
cluster.provider_gcp = build(:cluster_provider_gcp, :created)
end
end end
trait :providing_by_gcp do trait :providing_by_gcp do
provider_type :gcp provider_type :gcp
provider_gcp factory: [:cluster_provider_gcp, :creating]
provider_gcp do
create(:cluster_provider_gcp, :creating)
end
end end
end 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,204 +3,23 @@ require 'spec_helper' ...@@ -3,204 +3,23 @@ require 'spec_helper'
feature 'Clusters', :js do feature 'Clusters', :js do
include GoogleApi::CloudPlatformHelpers include GoogleApi::CloudPlatformHelpers
let!(:project) { create(:project, :repository) } let(:project) { create(:project) }
let!(:user) { create(:user) } let(:user) { create(:user) }
before do before do
project.add_master(user) project.add_master(user)
gitlab_sign_in(user) gitlab_sign_in(user)
end end
context 'when user has signed in Google' do context 'when user does not have a cluster and visits cluster index page' do
before do
allow_any_instance_of(Projects::ClustersController)
.to receive(:token_in_session).and_return('token')
allow_any_instance_of(Projects::ClustersController)
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
end
context 'when user does not have a cluster and visits cluster index page' do
before do
visit project_clusters_path(project)
click_link 'Create on GKE'
end
it 'user sees a new page' do
expect(page).to have_button('Create cluster')
end
context 'when user filled form with valid parameters' do
before do
double.tap do |dbl|
allow(dbl).to receive(:status).and_return('RUNNING')
allow(dbl).to receive(:self_link)
.and_return('projects/gcp-project-12345/zones/us-central1-a/operations/ope-123')
allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create).and_return(dbl)
end
allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_name', with: 'dev-cluster'
click_button 'Create cluster'
end
it 'user sees a cluster details page and creation status' do
expect(page).to have_content('Cluster is being created on Google Container Engine...')
# Application Installation buttons
page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install')
end
Clusters::Cluster.last.provider.make_created!
expect(page).to have_content('Cluster was successfully created on Google Container Engine')
end
it 'user sees a error if something worng during creation' do
expect(page).to have_content('Cluster is being created on Google Container Engine...')
Clusters::Cluster.last.provider.make_errored!('Something wrong!')
expect(page).to have_content('Something wrong!')
end
end
context 'when user filled form with invalid parameters' do
before do
click_button 'Create cluster'
end
it 'user sees a validation error' do
expect(page).to have_css('#error_explanation')
end
end
end
context 'when user has a cluster and visits cluster index page' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
before do
visit project_clusters_path(project)
end
it 'user sees an cluster details page' do
expect(page).to have_button('Save')
expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
# Application Installation buttons
page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
end
end
context 'when user installs application: Helm Tiller' do
before do
allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
page.within('.js-cluster-application-row-helm') do
page.find(:css, '.js-cluster-application-install-button').click
end
end
it 'user sees status transition' do
page.within('.js-cluster-application-row-helm') do
# FE sends request and gets the response, then the buttons is "Install"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
Clusters::Cluster.last.application_helm.make_installing!
# FE starts polling and update the buttons to "Installing"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
Clusters::Cluster.last.application_helm.make_installed!
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
end
expect(page).to have_content('Helm Tiller was successfully installed on your cluster')
end
end
context 'when user installs application: Ingress' do
before do
allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
# Helm Tiller needs to be installed before you can install Ingress
create(:cluster_applications_helm, :installed, cluster: cluster)
visit project_clusters_path(project)
page.within('.js-cluster-application-row-ingress') do
page.find(:css, '.js-cluster-application-install-button').click
end
end
it 'user sees status transition' do
page.within('.js-cluster-application-row-ingress') do
# FE sends request and gets the response, then the buttons is "Install"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
Clusters::Cluster.last.application_ingress.make_installing!
# FE starts polling and update the buttons to "Installing"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
Clusters::Cluster.last.application_ingress.make_installed!
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
end
expect(page).to have_content('Ingress was successfully installed on your cluster')
end
end
context 'when user disables the cluster' do
before do
page.find(:css, '.js-toggle-cluster').click
click_button 'Save'
end
it 'user sees the succeccful message' do
expect(page).to have_content('Cluster was successfully updated.')
end
end
context 'when user destory the cluster' do
before do
page.accept_confirm do
click_link 'Remove integration'
end
end
it 'user sees creation form with the succeccful message' do
expect(page).to have_content('Cluster integration was successfully removed.')
expect(page).to have_link('Create on GKE')
end
end
end
end
context 'when user has not signed in Google' do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Create on GKE' click_link 'Create on GKE'
end end
it 'user sees a login page' do it 'user sees a new page' do
expect(page).to have_css('.signin-with-google') expect(page).to have_button('Create cluster')
end end
end end
end end
...@@ -185,6 +185,36 @@ describe 'Pipeline', :js do ...@@ -185,6 +185,36 @@ describe 'Pipeline', :js do
end end
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 describe 'GET /:project/pipelines/:id/builds' do
include_context 'pipeline builds' include_context 'pipeline builds'
......
...@@ -36,6 +36,20 @@ describe('Clusters', () => { ...@@ -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', () => { describe('checkForNewInstalls', () => {
const INITIAL_APP_MAP = { const INITIAL_APP_MAP = {
helm: { status: null, title: 'Helm Tiller' }, helm: { status: null, title: 'Helm Tiller' },
...@@ -113,7 +127,7 @@ describe('Clusters', () => { ...@@ -113,7 +127,7 @@ describe('Clusters', () => {
}); });
describe('when cluster is created', () => { 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'); cluster.updateContainer(null, 'created');
expect( expect(
......
import Vue from 'vue'; import Vue from 'vue';
import jobComponent from '~/pipelines/components/graph/job_component.vue'; import jobComponent from '~/pipelines/components/graph/job_component.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('pipeline graph job component', () => { describe('pipeline graph job component', () => {
let JobComponent; let JobComponent;
let component;
const mockJob = { const mockJob = {
id: 4256, id: 4256,
...@@ -13,6 +15,7 @@ describe('pipeline graph job component', () => { ...@@ -13,6 +15,7 @@ describe('pipeline graph job component', () => {
label: 'passed', label: 'passed',
group: 'success', group: 'success',
details_path: '/root/ci-mock/builds/4256', details_path: '/root/ci-mock/builds/4256',
has_details: true,
action: { action: {
icon: 'retry', icon: 'retry',
title: 'Retry', title: 'Retry',
...@@ -26,13 +29,13 @@ describe('pipeline graph job component', () => { ...@@ -26,13 +29,13 @@ describe('pipeline graph job component', () => {
JobComponent = Vue.extend(jobComponent); JobComponent = Vue.extend(jobComponent);
}); });
afterEach(() => {
component.$destroy();
});
describe('name with link', () => { describe('name with link', () => {
it('should render the job name and status with a link', (done) => { it('should render the job name and status with a link', (done) => {
const component = new JobComponent({ component = mountComponent(JobComponent, { job: mockJob });
propsData: {
job: mockJob,
},
}).$mount();
Vue.nextTick(() => { Vue.nextTick(() => {
const link = component.$el.querySelector('a'); const link = component.$el.querySelector('a');
...@@ -56,23 +59,23 @@ describe('pipeline graph job component', () => { ...@@ -56,23 +59,23 @@ describe('pipeline graph job component', () => {
describe('name without link', () => { describe('name without link', () => {
it('it should render status and name', () => { it('it should render status and name', () => {
const component = new JobComponent({ component = mountComponent(JobComponent, {
propsData: { job: {
job: { id: 4256,
id: 4256, name: 'test',
name: 'test', status: {
status: { icon: 'icon_status_success',
icon: 'icon_status_success', text: 'passed',
text: 'passed', label: 'passed',
label: 'passed', group: 'success',
group: 'success', details_path: '/root/ci-mock/builds/4256',
details_path: '/root/ci-mock/builds/4256', has_details: false,
},
}, },
}, },
}).$mount(); });
expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined(); expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined();
expect(component.$el.querySelector('a')).toBeNull();
expect( expect(
component.$el.querySelector('.ci-status-text').textContent.trim(), component.$el.querySelector('.ci-status-text').textContent.trim(),
...@@ -82,11 +85,7 @@ describe('pipeline graph job component', () => { ...@@ -82,11 +85,7 @@ describe('pipeline graph job component', () => {
describe('action icon', () => { describe('action icon', () => {
it('it should render the action icon', () => { it('it should render the action icon', () => {
const component = new JobComponent({ component = mountComponent(JobComponent, { job: mockJob });
propsData: {
job: mockJob,
},
}).$mount();
expect(component.$el.querySelector('a.ci-action-icon-container')).toBeDefined(); expect(component.$el.querySelector('a.ci-action-icon-container')).toBeDefined();
expect(component.$el.querySelector('i.ci-action-icon-wrapper')).toBeDefined(); expect(component.$el.querySelector('i.ci-action-icon-wrapper')).toBeDefined();
...@@ -95,24 +94,20 @@ describe('pipeline graph job component', () => { ...@@ -95,24 +94,20 @@ describe('pipeline graph job component', () => {
describe('dropdown', () => { describe('dropdown', () => {
it('should render the dropdown action icon', () => { it('should render the dropdown action icon', () => {
const component = new JobComponent({ component = mountComponent(JobComponent, {
propsData: { job: mockJob,
job: mockJob, isDropdown: true,
isDropdown: true, });
},
}).$mount();
expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined(); expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined();
}); });
}); });
it('should render provided class name', () => { it('should render provided class name', () => {
const component = new JobComponent({ component = mountComponent(JobComponent, {
propsData: { job: mockJob,
job: mockJob, cssClassJobName: 'css-class-job-name',
cssClassJobName: 'css-class-job-name', });
},
}).$mount();
expect( expect(
component.$el.querySelector('a').classList.contains('css-class-job-name'), component.$el.querySelector('a').classList.contains('css-class-job-name'),
......
...@@ -3,6 +3,7 @@ require 'spec_helper' ...@@ -3,6 +3,7 @@ require 'spec_helper'
describe GoogleApi::CloudPlatform::Client do describe GoogleApi::CloudPlatform::Client do
let(:token) { 'token' } let(:token) { 'token' }
let(:client) { described_class.new(token, nil) } let(:client) { described_class.new(token, nil) }
let(:user_agent_options) { client.instance_eval { user_agent_header } }
describe '.session_key_for_redirect_uri' do describe '.session_key_for_redirect_uri' do
let(:state) { 'random_string' } let(:state) { 'random_string' }
...@@ -55,7 +56,8 @@ describe GoogleApi::CloudPlatform::Client do ...@@ -55,7 +56,8 @@ describe GoogleApi::CloudPlatform::Client do
before do before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService) 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 end
it { is_expected.to eq(gke_cluster) } it { is_expected.to eq(gke_cluster) }
...@@ -74,7 +76,8 @@ describe GoogleApi::CloudPlatform::Client do ...@@ -74,7 +76,8 @@ describe GoogleApi::CloudPlatform::Client do
before do before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService) 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 end
it { is_expected.to eq(operation) } it { is_expected.to eq(operation) }
...@@ -102,7 +105,8 @@ describe GoogleApi::CloudPlatform::Client do ...@@ -102,7 +105,8 @@ describe GoogleApi::CloudPlatform::Client do
before do before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService) 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 end
it { is_expected.to eq(operation) } it { is_expected.to eq(operation) }
...@@ -125,4 +129,18 @@ describe GoogleApi::CloudPlatform::Client do ...@@ -125,4 +129,18 @@ describe GoogleApi::CloudPlatform::Client do
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
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 end
...@@ -543,6 +543,7 @@ describe Namespace do ...@@ -543,6 +543,7 @@ describe Namespace do
end end
end end
<<<<<<< HEAD
describe '#share_with_group_lock with subgroups', :nested_groups do describe '#share_with_group_lock with subgroups', :nested_groups do
context 'when creating a subgroup' do context 'when creating a subgroup' do
let(:subgroup) { create(:group, parent: root_group )} let(:subgroup) { create(:group, parent: root_group )}
...@@ -656,6 +657,9 @@ describe Namespace do ...@@ -656,6 +657,9 @@ describe Namespace do
end end
describe '#has_forks_of?' do describe '#has_forks_of?' do
=======
describe '#find_fork_of?' do
>>>>>>> upstream/master
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) } let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) }
...@@ -674,6 +678,14 @@ describe Namespace do ...@@ -674,6 +678,14 @@ describe Namespace do
expect(other_namespace.find_fork_of(project)).to eq(other_fork) expect(other_namespace.find_fork_of(project)).to eq(other_fork)
end end
context 'with request store enabled', :request_store do
it 'only queries once' do
expect(project.fork_network).to receive(:find_forks_in).once.and_call_original
2.times { namespace.find_fork_of(project) }
end
end
end end
describe '#root_ancestor' do describe '#root_ancestor' do
......
...@@ -1408,42 +1408,52 @@ describe Repository do ...@@ -1408,42 +1408,52 @@ describe Repository do
end end
describe '#cherry_pick' do describe '#cherry_pick' do
let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') } shared_examples 'cherry-picking a commit' do
let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') } let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
let(:message) { 'cherry-pick message' } let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
let(:message) { 'cherry-pick message' }
context 'when there is a conflict' do
it 'raises an error' do context 'when there is a conflict' do
expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) it 'raises an error' do
expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
end end
end
context 'when commit was already cherry-picked' do context 'when commit was already cherry-picked' do
it 'raises an error' do it 'raises an error' do
repository.cherry_pick(user, pickable_commit, 'master', message) repository.cherry_pick(user, pickable_commit, 'master', message)
expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
end end
end
context 'when commit can be cherry-picked' do context 'when commit can be cherry-picked' do
it 'cherry-picks the changes' do it 'cherry-picks the changes' do
expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy
end
end end
end
context 'cherry-picking a merge commit' do context 'cherry-picking a merge commit' do
it 'cherry-picks the changes' do it 'cherry-picks the changes' do
expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil
cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message) cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message)
cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message
expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
expect(cherry_pick_commit_message).to eq(message) expect(cherry_pick_commit_message).to eq(message)
end
end end
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 end
describe '#before_delete' do describe '#before_delete' do
......
...@@ -41,7 +41,8 @@ RSpec::Matchers.define :exceed_query_limit do |expected| ...@@ -41,7 +41,8 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
supports_block_expectations supports_block_expectations
match do |block| match do |block|
query_count(&block) > expected_count + threshold @subject_block = block
actual_count > expected_count + threshold
end end
failure_message_when_negated do |actual| failure_message_when_negated do |actual|
...@@ -55,6 +56,11 @@ RSpec::Matchers.define :exceed_query_limit do |expected| ...@@ -55,6 +56,11 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
self self
end end
def for_query(query)
@query = query
self
end
def threshold def threshold
@threshold.to_i @threshold.to_i
end end
...@@ -68,12 +74,15 @@ RSpec::Matchers.define :exceed_query_limit do |expected| ...@@ -68,12 +74,15 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
end end
def actual_count def actual_count
@recorder.count @actual_count ||= if @query
recorder.log.select { |recorded| recorded =~ @query }.size
else
recorder.count
end
end end
def query_count(&block) def recorder
@recorder = ActiveRecord::QueryRecorder.new(&block) @recorder ||= ActiveRecord::QueryRecorder.new(&@subject_block)
@recorder.count
end end
def count_queries(queries) 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