Commit ececcc32 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into '3730-allow-admins-to-disable-mirroring'

# Conflicts:
#   db/schema.rb
parents fe89a7c1 f98c32a7
...@@ -130,7 +130,8 @@ freeze date (the 7th) should have a corresponding Enterprise Edition merge ...@@ -130,7 +130,8 @@ freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the request, even if there are no conflicts. This is to reduce the size of the
subsequent EE merge, as we often merge a lot to CE on the release date. For more subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts]. [Automatic CE->EE merge][automatic_ce_ee_merge] and
[Guidelines for implementing Enterprise Edition features][ee_features].
### After the 7th ### After the 7th
...@@ -281,4 +282,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http ...@@ -281,4 +282,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements ["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review [Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
[done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done [done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done
[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html [automatic_ce_ee_merge]: https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
[ee_features]: https://docs.gitlab.com/ce/development/ee_features.html
...@@ -150,8 +150,8 @@ export default class Clusters { ...@@ -150,8 +150,8 @@ export default class Clusters {
} }
toggle() { toggle() {
this.toggleButton.classList.toggle('checked'); this.toggleButton.classList.toggle('is-checked');
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString()); this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString());
} }
showToken() { showToken() {
......
import Flash from '../flash';
import { s__ } from '../locale';
import ClustersService from './services/clusters_service';
/**
* Toggles loading and disabled classes.
* @param {HTMLElement} button
*/
const toggleLoadingButton = (button) => {
if (button.getAttribute('disabled')) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', true);
}
button.classList.toggle('is-loading');
};
/**
* Toggles checked class for the given button
* @param {HTMLElement} button
*/
const toggleValue = (button) => {
button.classList.toggle('is-checked');
};
/**
* Handles toggle buttons in the cluster's table.
*
* When the user clicks the toggle button for each cluster, it:
* - toggles the button
* - shows a loading and disables button
* - Makes a put request to the given endpoint
* Once we receive the response, either:
* 1) Show updated status in case of successfull response
* 2) Show initial status in case of failed response
*/
export default function setClusterTableToggles() {
document.querySelectorAll('.js-toggle-cluster-list')
.forEach(button => button.addEventListener('click', (e) => {
const toggleButton = e.currentTarget;
const endpoint = toggleButton.getAttribute('data-endpoint');
toggleValue(toggleButton);
toggleLoadingButton(toggleButton);
const value = toggleButton.classList.contains('is-checked');
ClustersService.updateCluster(endpoint, { cluster: { enabled: value } })
.then(() => {
toggleLoadingButton(toggleButton);
})
.catch(() => {
toggleLoadingButton(toggleButton);
toggleValue(toggleButton);
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
});
}));
}
...@@ -17,4 +17,8 @@ export default class ClusterService { ...@@ -17,4 +17,8 @@ export default class ClusterService {
installApplication(appId) { installApplication(appId) {
return axios.post(this.appInstallEndpointMap[appId]); return axios.post(this.appInstallEndpointMap[appId]);
} }
static updateCluster(endpoint, data) {
return axios.put(endpoint, data);
}
} }
...@@ -621,7 +621,15 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -621,7 +621,15 @@ import initGroupAnalytics from './init_group_analytics';
import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle') import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap .then(cluster => new cluster.default()) // eslint-disable-line new-cap
.catch((err) => { .catch((err) => {
Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript')); Flash(s__('ClusterIntegration|Problem setting up the cluster'));
throw err;
});
break;
case 'projects:clusters:index':
import(/* webpackChunkName: "clusters_index" */ './clusters/clusters_index')
.then(clusterIndex => clusterIndex.default())
.catch((err) => {
Flash(s__('ClusterIntegration|Problem setting up the clusters list'));
throw err; throw err;
}); });
break; break;
......
<script> <script>
import projectFeatureToggle from './project_feature_toggle.vue'; import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
export default { export default {
props: { props: {
......
<script>
export default {
props: {
name: {
type: String,
required: false,
default: '',
},
value: {
type: Boolean,
required: true,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
},
model: {
prop: 'value',
event: 'change',
},
methods: {
toggleFeature() {
if (!this.disabledInput) this.$emit('change', !this.value);
},
},
};
</script>
<template>
<label class="toggle-wrapper">
<input
v-if="name"
type="hidden"
:name="name"
:value="value"
/>
<button
type="button"
aria-label="Toggle"
class="project-feature-toggle"
data-enabled-text="Enabled"
data-disabled-text="Disabled"
:class="{ checked: value, disabled: disabledInput }"
@click="toggleFeature"
/>
</label>
</template>
<script> <script>
import projectFeatureSetting from './project_feature_setting.vue'; import projectFeatureSetting from './project_feature_setting.vue';
import projectFeatureToggle from './project_feature_toggle.vue'; import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue'; import projectSettingRow from './project_setting_row.vue';
import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import { toggleHiddenClassBySelector } from '../external'; import { toggleHiddenClassBySelector } from '../external';
......
<script>
import loadingIcon from './loading_icon.vue';
export default {
props: {
name: {
type: String,
required: false,
default: '',
},
value: {
type: Boolean,
required: true,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
enabledText: {
type: String,
required: false,
default: 'Enabled',
},
disabledText: {
type: String,
required: false,
default: 'Disabled',
},
},
components: {
loadingIcon,
},
model: {
prop: 'value',
event: 'change',
},
methods: {
toggleFeature() {
if (!this.disabledInput) this.$emit('change', !this.value);
},
},
};
</script>
<template>
<label class="toggle-wrapper">
<input
type="hidden"
:name="name"
:value="value"
/>
<button
type="button"
aria-label="Toggle"
class="project-feature-toggle"
:data-enabled-text="enabledText"
:data-disabled-text="disabledText"
:class="{
'is-checked': value,
'is-disabled': disabledInput,
'is-loading': isLoading
}"
@click="toggleFeature"
>
<loadingIcon class="loading-icon" />
</button>
</label>
</template>
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
@import "framework/tabs"; @import "framework/tabs";
@import "framework/timeline"; @import "framework/timeline";
@import "framework/tooltips"; @import "framework/tooltips";
@import "framework/toggle";
@import "framework/typography"; @import "framework/typography";
@import "framework/zen"; @import "framework/zen";
@import "framework/blank"; @import "framework/blank";
......
/**
* Toggle button
*
* @usage
* ### Active and Inactive text should be provided as data attributes:
* <button type="button" class="project-feature-toggle" data-enabled-text="Enabled" data-disabled-text="Disabled">
* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
* </button>
* ### Checked should have `is-checked` class
* <button type="button" class="project-feature-toggle is-checked" data-enabled-text="Enabled" data-disabled-text="Disabled">
* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
* </button>
* ### Disabled should have `is-disabled` class
* <button type="button" class="project-feature-toggle is-disabled" data-enabled-text="Enabled" data-disabled-text="Disabled" disabled="true">
* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
* </button>
* ### Loading should have `is-loading` and an icon with `loading-icon` class
* <button type="button" class="project-feature-toggle is-loading" data-enabled-text="Enabled" data-disabled-text="Disabled">
* <i class="fa fa-spinner fa-spin loading-icon"></i>
* </button>
*/
.project-feature-toggle {
position: relative;
border: 0;
outline: 0;
display: block;
width: 100px;
height: 24px;
cursor: pointer;
user-select: none;
background: $feature-toggle-color-disabled;
border-radius: 12px;
padding: 3px;
transition: all .4s ease;
&::selection,
&::before::selection,
&::after::selection {
background: none;
}
&::before {
color: $feature-toggle-text-color;
font-size: 12px;
line-height: 24px;
position: absolute;
top: 0;
left: 25px;
right: 5px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
animation: animate-disabled .2s ease-in;
content: attr(data-disabled-text);
}
&::after {
position: relative;
display: block;
content: "";
width: 22px;
height: 18px;
left: 0;
border-radius: 9px;
background: $feature-toggle-color;
transition: all .2s ease;
}
.loading-icon {
display: none;
font-size: 12px;
color: $white-light;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&.is-loading {
&::before {
display: none;
}
.loading-icon {
display: block;
&::before {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
&.is-checked {
background: $feature-toggle-color-enabled;
&::before {
left: 5px;
right: 25px;
animation: animate-enabled .2s ease-in;
content: attr(data-enabled-text);
}
&::after {
left: calc(100% - 22px);
}
}
&.is-disabled {
opacity: 0.4;
cursor: not-allowed;
}
@media (max-width: $screen-xs-min) {
width: 50px;
&::before,
&.is-checked::before {
display: none;
}
}
@keyframes animate-enabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes animate-disabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
}
...@@ -14,3 +14,17 @@ ...@@ -14,3 +14,17 @@
} }
@include new-style-dropdown('.clusters-dropdown '); @include new-style-dropdown('.clusters-dropdown ');
.clusters-container {
.nav-bar-right {
padding: $gl-padding-top $gl-padding;
}
.empty-state .svg-content img {
width: 145px;
}
.top-area .nav-controls > .btn.btn-add-cluster {
margin-right: 0;
}
}
...@@ -134,93 +134,6 @@ ...@@ -134,93 +134,6 @@
} }
} }
.project-feature-toggle {
position: relative;
border: 0;
outline: 0;
display: block;
width: 100px;
height: 24px;
cursor: pointer;
user-select: none;
background: $feature-toggle-color-disabled;
border-radius: 12px;
padding: 3px;
transition: all .4s ease;
&::selection,
&::before::selection,
&::after::selection {
background: none;
}
&::before {
color: $feature-toggle-text-color;
font-size: 12px;
line-height: 24px;
position: absolute;
top: 0;
left: 25px;
right: 5px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
animation: animate-disabled .2s ease-in;
content: attr(data-disabled-text);
}
&::after {
position: relative;
display: block;
content: "";
width: 22px;
height: 18px;
left: 0;
border-radius: 9px;
background: $feature-toggle-color;
transition: all .2s ease;
}
&.checked {
background: $feature-toggle-color-enabled;
&::before {
left: 5px;
right: 25px;
animation: animate-enabled .2s ease-in;
content: attr(data-enabled-text);
}
&::after {
left: calc(100% - 22px);
}
}
&.disabled {
opacity: 0.4;
cursor: not-allowed;
}
@media (max-width: $screen-xs-min) {
width: 50px;
&::before,
&.checked::before {
display: none;
}
}
@keyframes animate-enabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes animate-disabled {
0%, 35% { opacity: 0; }
100% { opacity: 1; }
}
}
.project-home-panel, .project-home-panel,
.group-home-panel { .group-home-panel {
padding-top: 24px; padding-top: 24px;
......
...@@ -8,11 +8,11 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -8,11 +8,11 @@ class Projects::ClustersController < Projects::ApplicationController
STATUS_POLLING_INTERVAL = 10_000 STATUS_POLLING_INTERVAL = 10_000
def index def index
if project.cluster @scope = params[:scope] || 'all'
redirect_to project_cluster_path(project, project.cluster) @clusters = ClustersFinder.new(project, current_user, @scope).execute.page(params[:page])
else @active_count = ClustersFinder.new(project, current_user, :active).execute.count
redirect_to new_project_cluster_path(project) @inactive_count = ClustersFinder.new(project, current_user, :inactive).execute.count
end @all_count = @active_count + @inactive_count
end end
def new def new
...@@ -39,10 +39,20 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -39,10 +39,20 @@ class Projects::ClustersController < Projects::ApplicationController
.execute(cluster) .execute(cluster)
if cluster.valid? if cluster.valid?
flash[:notice] = "Cluster was successfully updated." respond_to do |format|
redirect_to project_cluster_path(project, project.cluster) format.json do
head :no_content
end
format.html do
flash[:notice] = "Cluster was successfully updated."
redirect_to project_cluster_path(project, cluster)
end
end
else else
render :show respond_to do |format|
format.json { head :bad_request }
format.html { render :show }
end
end end
end end
...@@ -63,6 +73,19 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -63,6 +73,19 @@ class Projects::ClustersController < Projects::ApplicationController
.present(current_user: current_user) .present(current_user: current_user)
end end
def create_params
params.require(:cluster).permit(
:enabled,
:name,
:provider_type,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type
])
end
def update_params def update_params
if cluster.managed? if cluster.managed?
params.require(:cluster).permit( params.require(:cluster).permit(
......
class ClustersFinder
def initialize(project, user, scope)
@project = project
@user = user
@scope = scope || :active
end
def execute
clusters = project.clusters
filter_by_scope(clusters)
end
private
attr_reader :project, :user, :scope
def filter_by_scope(clusters)
case scope.to_sym
when :all
clusters
when :inactive
clusters.disabled
when :active
clusters.enabled
else
raise "Invalid scope #{scope}"
end
end
end
...@@ -55,6 +55,10 @@ module Clusters ...@@ -55,6 +55,10 @@ module Clusters
end end
end end
def created?
status_name == :created
end
def applications def applications
[ [
application_helm || build_application_helm, application_helm || build_application_helm,
......
...@@ -194,7 +194,6 @@ class Project < ActiveRecord::Base ...@@ -194,7 +194,6 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics' has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project' has_one :cluster_project, class_name: 'Clusters::Project'
has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster' has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry, # Container repositories need to remove data from the container registry,
......
...@@ -5,5 +5,9 @@ module Clusters ...@@ -5,5 +5,9 @@ module Clusters
def gke_cluster_url def gke_cluster_url
"https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp? "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end end
def can_toggle_cluster?
can?(current_user, :update_cluster, cluster) && created?
end
end end
end end
...@@ -5,6 +5,8 @@ module Clusters ...@@ -5,6 +5,8 @@ module Clusters
def execute(access_token = nil) def execute(access_token = nil)
@access_token = access_token @access_token = access_token
raise ArgumentError.new('Instance does not support multiple clusters') unless can_create_cluster?
create_cluster.tap do |cluster| create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
end end
...@@ -25,5 +27,11 @@ module Clusters ...@@ -25,5 +27,11 @@ module Clusters
@cluster_params = params.merge(user: current_user, projects: [project]) @cluster_params = params.merge(user: current_user, projects: [project])
end end
def can_create_cluster?
return true if project.clusters.empty?
project.feature_available?(:multiple_clusters)
end
end end
end end
module Projects module Projects
class ForkService < BaseService class ForkService < BaseService
def execute def execute(fork_to_project = nil)
if fork_to_project
link_existing_project(fork_to_project)
else
fork_new_project
end
end
private
def link_existing_project(fork_to_project)
return if fork_to_project.forked?
link_fork_network(fork_to_project)
fork_to_project
end
def fork_new_project
new_params = { new_params = {
forked_from_project_id: @project.id, forked_from_project_id: @project.id,
visibility_level: allowed_visibility_level, visibility_level: allowed_visibility_level,
...@@ -21,15 +39,11 @@ module Projects ...@@ -21,15 +39,11 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level) new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
refresh_forks_count
link_fork_network(new_project) link_fork_network(new_project)
new_project new_project
end end
private
def fork_network def fork_network
if @project.fork_network if @project.fork_network
@project.fork_network @project.fork_network
...@@ -43,9 +57,17 @@ module Projects ...@@ -43,9 +57,17 @@ module Projects
end end
end end
def link_fork_network(new_project) def link_fork_network(fork_to_project)
fork_network.fork_network_members.create(project: new_project, fork_network.fork_network_members.create(project: fork_to_project,
forked_from_project: @project) forked_from_project: @project)
# TODO: remove this when ForkedProjectLink model is removed
unless fork_to_project.forked_project_link
fork_to_project.create_forked_project_link(forked_to_project: fork_to_project,
forked_from_project: @project)
end
refresh_forks_count
end end
def refresh_forks_count def refresh_forks_count
......
...@@ -201,7 +201,7 @@ ...@@ -201,7 +201,7 @@
= nav_link(controller: [:clusters, :user, :gcp]) do = nav_link(controller: [:clusters, :user, :gcp]) do
= link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
%span %span
Cluster Clusters
- 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
......
.gl-responsive-table-row
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Cluster")
.table-mobile-content
= link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment pattern")
.table-mobile-content= cluster.environment_scope
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
.table-mobile-content= cluster.platform_kubernetes&.actual_namespace
.table-section.section-10
.table-mobile-header{ role: "rowheader" }
.table-mobile-content
%button{ type: "button",
class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !cluster.can_toggle_cluster?,
data: { "enabled-text": s_("ClusterIntegration|Active"),
"disabled-text": s_("ClusterIntegration|Inactive"),
endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
= icon("spinner spin", class: "loading-icon")
.row.empty-state
.col-xs-12
.svg-content= image_tag 'illustrations/clusters_empty.svg'
.col-xs-12.text-center
.text-content
%h4= s_('ClusterIntegration|Integrate cluster automation')
- link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
%p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
%p
= link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
...@@ -5,12 +5,11 @@ ...@@ -5,12 +5,11 @@
= field.hidden_field :enabled, { class: 'js-toggle-input'} = field.hidden_field :enabled, { class: 'js-toggle-input'}
%button{ type: 'button', %button{ type: 'button',
class: "js-toggle-cluster project-feature-toggle #{'checked' unless !@cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, @cluster)}", class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
'aria-label': s_('ClusterIntegration|Toggle Cluster'), "aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !can?(current_user, :update_cluster, @cluster), disabled: !can?(current_user, :update_cluster, @cluster),
data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled' } } data: { "enabled-text": s_("ClusterIntegration|Active"), "disabled-text": s_("ClusterIntegration|Inactive"), } }
- if can?(current_user, :update_cluster, @cluster) - if can?(current_user, :update_cluster, @cluster)
.form-group .form-group
= field.submit _('Save'), class: 'btn btn-success' = field.submit _('Save'), class: 'btn btn-success'
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon("angle-left")
.fade-right= icon("angle-right")
%ul.nav-links.scrolling-tabs
%li{ class: ('active' if @scope == 'active') }>
= link_to project_clusters_path(@project, scope: :active), class: "js-active-tab" do
= s_("ClusterIntegration|Active")
%span.badge= @active_count
%li{ class: ('active' if @scope == 'inactive') }>
= link_to project_clusters_path(@project, scope: :inactive), class: "js-inactive-tab" do
= s_("ClusterIntegration|Inactive")
%span.badge= @inactive_count
%li{ class: ('active' if @scope.nil? || @scope == 'all') }>
= link_to project_clusters_path(@project), class: "js-all-tab" do
= s_("ClusterIntegration|All")
%span.badge= @all_count
.nav-controls
= link_to s_("ClusterIntegration|Add cluster"), new_project_cluster_path(@project), class: "btn btn-success btn-add-cluster disabled has-tooltip js-add-cluster", title: s_("ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate")
- breadcrumb_title "Clusters"
- page_title "Clusters"
.clusters-container
- if !@clusters.empty?
= render "tabs"
.ci-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Cluster")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Environment pattern")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Project namespace")
.table-section.section-10{ role: "rowheader" }
- @clusters.each do |cluster|
= render "cluster", cluster: cluster.present(current_user: current_user)
= paginate @clusters, theme: "gitlab"
- elsif @scope == 'all'
= render "empty_state"
- else
= render "tabs"
.prepend-top-20.text-center
= s_("ClusterIntegration|There are no clusters to show")
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- breadcrumb_title "Cluster" - add_to_breadcrumbs "Clusters", project_clusters_path(@project)
- breadcrumb_title @cluster.id
- page_title _("Cluster") - page_title _("Cluster")
- expanded = Rails.env.test? - expanded = Rails.env.test?
...@@ -28,7 +29,6 @@ ...@@ -28,7 +29,6 @@
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%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? - if @cluster.managed?
= render 'projects/clusters/gcp/show' = render 'projects/clusters/gcp/show'
......
---
title: Using appropiate services in the API for managing forks
merge_request: 15709
author:
type: fixed
class RescheduleForkNetworkCreationCaller < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'PopulateForkNetworksRange'.freeze
BATCH_SIZE = 100
DELAY_INTERVAL = 15.seconds
disable_ddl_transaction!
class ForkedProjectLink < ActiveRecord::Base
include EachBatch
self.table_name = 'forked_project_links'
end
def up
say 'Populating the `fork_networks` based on existing `forked_project_links`'
queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
end
def down
# nothing
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171201140229) do ActiveRecord::Schema.define(version: 20171205190711) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -16,8 +16,8 @@ comments: false ...@@ -16,8 +16,8 @@ comments: false
- [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) - [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md)
- [Generate a changelog entry with `bin/changelog`](changelog.md) - [Generate a changelog entry with `bin/changelog`](changelog.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed. - [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
- [Limit conflicts with EE when developing on CE](limit_ee_conflicts.md) - [Automatic CE->EE merge](automatic_ce_ee_merge.md)
- [Guidelines for implementing Enterprise Edition feature](ee_features.md) - [Guidelines for implementing Enterprise Edition features](ee_features.md)
## UX and frontend guides ## UX and frontend guides
......
# Automatic CE->EE merge
GitLab Community Edition is merged automatically every 3 hours into the
Enterprise Edition (look for the [`CE Upstream` merge requests]).
This merge is done automatically in a
[scheduled pipeline](https://gitlab.com/gitlab-org/release-tools/-/jobs/43201679).
If a merge is already in progress, the job [doesn't create a new one](https://gitlab.com/gitlab-org/release-tools/-/jobs/43157687).
**If you are pinged in a `CE Upstream` merge request to resolve a conflict,
please resolve the conflict as soon as possible or ask someone else to do it!**
>**Note:**
It's ok to resolve more conflicts than the one that you are asked to resolve. In
that case, it's a good habit to ask for a double-check on your resolution by
someone who is familiar with the code you touched.
[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream
### Always merge EE merge requests before their CE counterparts
**In order to avoid conflicts in the CE->EE merge, you should always merge the
EE version of your CE merge request first, if present.**
The rationale for this is that as CE->EE merges are done automatically every few
hours, it can happen that:
1. A CE merge request that needs EE-specific changes is merged
1. The automatic CE->EE merge happens
1. Conflicts due to the CE merge request occur since its EE merge request isn't
merged yet
1. The automatic merge bot will ping someone to resolve the conflict **that are
already resolved in the EE merge request that isn't merged yet**
That's a waste of time, and that's why you should merge EE merge request before
their CE counterpart.
## Avoiding CE->EE merge conflicts beforehand
To avoid the conflicts beforehand, check out the
[Guidelines for implementing Enterprise Edition features](ee_features.md).
In any case, the CI `ee_compat_check` job will tell you if you need to open an
EE version of your CE merge request.
### Conflicts detection in CE merge requests
For each commit (except on `master`), the `ee_compat_check` CI job tries to
detect if the current branch's changes will conflict during the CE->EE merge.
The job reports what files are conflicting and how to setup a merge request
against EE.
#### How the job works
1. Generates the diff between your branch and current CE `master`
1. Tries to apply it to current EE `master`
1. If it applies cleanly, the job succeeds, otherwise...
1. Detects a branch with the `ee-` prefix or `-ee` suffix in EE
1. If it exists, generate the diff between this branch and current EE `master`
1. Tries to apply it to current EE `master`
1. If it applies cleanly, the job succeeds
In the case where the job fails, it means you should create a `ee-<ce_branch>`
or `<ce_branch>-ee` branch, push it to EE and open a merge request against EE
`master`.
At this point if you retry the failing job in your CE merge request, it should
now pass.
Notes:
- This task is not a silver-bullet, its current goal is to bring awareness to
developers that their work needs to be ported to EE.
- Community contributors shouldn't be required to submit merge requests against
EE, but reviewers should take actions by either creating such EE merge request
or asking a GitLab developer to do it **before the merge request is merged**.
- If you branch is too far behind `master`, the job will fail. In that case you
should rebase your branch upon latest `master`.
- Code reviews for merge requests often consist of multiple iterations of
feedback and fixes. There is no need to update your EE MR after each
iteration. Instead, create an EE MR as soon as you see the
`ee_compat_check` job failing. After you receive the final approval
from a Maintainer (but **before the CE MR is merged**) update the EE MR.
This helps to identify significant conflicts sooner, but also reduces the
number of times you have to resolve conflicts.
- Please remember to
[always have your EE merge request merged before the CE version](#always-merge-ee-merge-requests-before-their-ce-counterparts).
- You can use [`git rerere`](https://git-scm.com/blog/2010/03/08/rerere.html)
to avoid resolving the same conflicts multiple times.
---
[Return to Development documentation](README.md)
# Guidelines for implementing Enterprise Edition feature # Guidelines for implementing Enterprise Edition features
- **Write the code and the tests.**: As with any code, EE features should have - **Write the code and the tests.**: As with any code, EE features should have
good test coverage to prevent regressions. good test coverage to prevent regressions.
...@@ -380,3 +380,9 @@ to avoid conflicts during CE to EE merge. ...@@ -380,3 +380,9 @@ to avoid conflicts during CE to EE merge.
} }
} }
``` ```
## gitlab-svgs
Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can
be resolved simply by regenerating those assets with
[`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs).
...@@ -142,7 +142,7 @@ tests. If it doesn't, the whole test suite will run (including docs). ...@@ -142,7 +142,7 @@ tests. If it doesn't, the whole test suite will run (including docs).
--- ---
When you submit a merge request to GitLab Community Edition (CE), there is an When you submit a merge request to GitLab Community Edition (CE), there is an
additional job called `rake ee_compat_check` that runs against Enterprise additional job called `ee_compat_check` that runs against Enterprise
Edition (EE) and checks if your changes can apply cleanly to the EE codebase. Edition (EE) and checks if your changes can apply cleanly to the EE codebase.
If that job fails, read the instructions in the job log for what to do next. If that job fails, read the instructions in the job log for what to do next.
Contributors do not need to submit their changes to EE, GitLab Inc. employees Contributors do not need to submit their changes to EE, GitLab Inc. employees
......
...@@ -5,62 +5,42 @@ This is the documentation for the Omnibus GitLab packages. For installations ...@@ -5,62 +5,42 @@ This is the documentation for the Omnibus GitLab packages. For installations
from source, follow the [**GitLab Geo nodes configuration for installations from source, follow the [**GitLab Geo nodes configuration for installations
from source**](configuration_source.md) guide. from source**](configuration_source.md) guide.
## Configuring a new secondary node
>**Note:** >**Note:**
Stages of the setup process must be completed in the documented order. This is the final step in setting up a secondary Geo node. Stages of the
Before attempting the steps in this stage, [complete all prior stages][toc]. setup process must be completed in the documented order.
Before attempting the steps in this stage, [complete all prior stages](README.md#using-omnibus-gitlab).
The basic steps of configuring a secondary node are:
This is the final step you need to follow in order to setup a Geo node. 1. replicate required configurations between the primary and the secondaries;
1. configure a second, tracking database on each secondary;
1. start GitLab on the secondary node's machine.
You are encouraged to first read through all the steps before executing them You are encouraged to first read through all the steps before executing them
in your testing/production environment. in your testing/production environment.
## Setting up GitLab
>**Notes:** >**Notes:**
- **Do not** setup any custom authentication in the secondary nodes, this will be - **Do not** setup any custom authentication in the secondary nodes, this will be
handled by the primary node. handled by the primary node.
- **Do not** add anything in the secondaries Geo nodes admin area - **Do not** add anything in the secondaries Geo nodes admin area
(**Admin Area ➔ Geo Nodes**). This is handled solely by the primary node. (**Admin Area ➔ Geo Nodes**). This is handled solely by the primary node.
After having installed GitLab Enterprise Edition in the instance that will serve
as a Geo node and set up the [database replication](database.md), the next steps
can be summed up to:
1. Replicate some required configurations between the primary and the secondaries
1. Configure a second, tracking database on each secondary
1. Start GitLab on the secondary node's machine
### Prerequisites
This is the last step of configuring a Geo secondary node. Make sure you have
followed the first two steps of the [Setup instructions](README.md#setup-instructions):
1. You have already installed on the secondary server the same version of
GitLab Enterprise Edition that is present on the primary server.
1. You have set up the database replication.
1. Your secondary node is allowed to communicate via HTTP/HTTPS with
your primary node (make sure your firewall is not blocking that).
1. Your nodes must have an NTP service running to synchronize the clocks.
You can use different timezones, but the hour relative to UTC can't be more
than 60 seconds off from each node.
### Step 1. Copying the database encryption key ### Step 1. Copying the database encryption key
GitLab stores a unique encryption key in disk that we use to safely store GitLab stores a unique encryption key on disk that is used to encrypt
sensitive data in the database. Any secondary node must have the sensitive data stored in the database. All secondary nodes must have the
**exact same value** for `db_key_base` as defined in the primary one. **exact same value** for `db_key_base` as defined on the primary node.
1. SSH into the **primary** node and login as root: 1. SSH into the **primary** node, and execute the command below
to display the current encryption key:
```bash
sudo gitlab-rake geo:db:show_encryption_key
``` ```
sudo -i
```
1. Execute the command below to display the current encryption key and copy it:
``` Copy the encryption key to bring it to the secondary node in the following steps.
gitlab-rake geo:db:show_encryption_key
```
1. SSH into the **secondary** node and login as root: 1. SSH into the **secondary** node and login as root:
...@@ -81,18 +61,22 @@ sensitive data in the database. Any secondary node must have the ...@@ -81,18 +61,22 @@ sensitive data in the database. Any secondary node must have the
gitlab-ctl reconfigure gitlab-ctl reconfigure
``` ```
Once reconfigured, the secondary will start automatically Once reconfigured, the secondary will automatically start
replicating missing data from the primary in a process known as backfill. replicating missing data from the primary in a process known as backfill.
Meanwhile, the primary node will start to notify changes to the secondary, which Meanwhile, the primary node will start to notify the secondary of any changes, so
will act on those notifications immediately. Make sure the secondary instance is that the secondary can act on those notifications immediately.
running and accessible.
### Step 2. Enabling hashed storage (optional, from GitLab 10.0) Make sure the secondary instance is
running and accessible. You can login to the secondary node
with the same credentials as used in the primary.
### Step 2. (Optional) Enabling hashed storage (from GitLab 10.0)
>**Warning** >**Warning**
Hashed storage is in **Alpha**. It is considered experimental and not Hashed storage is in **Alpha**. It is considered experimental and not
production-ready. See [Hashed production-ready. See [Hashed Storage](../administration/repository_storage_types.md)
Storage](../administration/repository_storage_types.md) for more detail. for more detail, and for the latest updates, check
[infrastructure issue #2821](https://gitlab.com/gitlab-com/infrastructure/issues/2821).
Using hashed storage significantly improves Geo replication - project and group Using hashed storage significantly improves Geo replication - project and group
renames no longer require synchronization between nodes. renames no longer require synchronization between nodes.
...@@ -120,7 +104,17 @@ method to be enabled. Navigate to **Admin Area ➔ Settings** ...@@ -120,7 +104,17 @@ method to be enabled. Navigate to **Admin Area ➔ Settings**
(`/admin/application_settings`) on the primary node, and set (`/admin/application_settings`) on the primary node, and set
`Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`. `Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`.
### Step 5. Managing the secondary GitLab node ### Verify proper functioning of the secondary node
Your nodes should now be ready to use. You can login to the secondary node
with the same credentials as used in the primary. Visit the secondary node's
**Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`) in your browser to check if it's
correctly identified as a secondary Geo node and if Geo is enabled.
If your installation isn't working properly, check the
[troubleshooting document](troubleshooting.md).
Point your users to the ["Using a Geo Server" guide](using_a_geo_server.md).
You can monitor the status of the syncing process on a secondary node You can monitor the status of the syncing process on a secondary node
by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`) by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`)
...@@ -139,36 +133,23 @@ The two most obvious issues that replication can have here are: ...@@ -139,36 +133,23 @@ The two most obvious issues that replication can have here are:
1. Instance to instance notification not working. In that case, it can be 1. Instance to instance notification not working. In that case, it can be
something of the following: something of the following:
- You are using a custom certificate or custom CA (see the - You are using a custom certificate or custom CA (see the
[Troubleshooting](configuration.md#troubleshooting) section) [troubleshooting document](troubleshooting.md))
- Instance is firewalled (check your firewall rules) - The instance is firewalled (check your firewall rules)
Currently, this is what is synced: Currently, this is what is synced:
* Git repositories * Git repositories
* Wikis * Wikis
* LFS objects * LFS objects
* Issue, merge request, snippet and comment attachments * Issues, merge requests, snippets, and comment attachments
* User, group, and project avatars * Users, groups, and project avatars
## Next steps
Your nodes should now be ready to use. You can login to the secondary node
with the same credentials as used in the primary. Visit the secondary node's
**Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`) in your browser to check if it's
correctly identified as a secondary Geo node and if Geo is enabled.
If your installation isn't working properly, check the
[troubleshooting](#troubleshooting) section.
Point your users to the ["Using a Geo Server" guide](using_a_geo_server.md).
## Selective replication ## Selective replication
With GitLab 9.5, GitLab Geo now supports the first iteration of selective GitLab Geo supports selective replication, which allows admins to choose which
replication, which allows admins to choose which groups should be groups should be replicated by secondary nodes.
replicated by secondary nodes.
It is important to notice that selective replication: It is important to note that selective replication:
1. Does not restrict permissions from secondary nodes. 1. Does not restrict permissions from secondary nodes.
1. Does not hide projects metadata from secondary nodes. Since Geo currently 1. Does not hide projects metadata from secondary nodes. Since Geo currently
...@@ -177,12 +158,10 @@ secondary nodes, but repositories that have not been selected will be empty. ...@@ -177,12 +158,10 @@ secondary nodes, but repositories that have not been selected will be empty.
1. Secondary nodes won't pull repositories that do not belong to the selected 1. Secondary nodes won't pull repositories that do not belong to the selected
groups to be replicated. groups to be replicated.
### Upgrading Geo ## Upgrading Geo
See the [updating the Geo nodes document](updating_the_geo_nodes.md). See the [updating the Geo nodes document](updating_the_geo_nodes.md).
## Troubleshooting ## Troubleshooting
See the [troubleshooting document](troubleshooting.md). See the [troubleshooting document](troubleshooting.md).
[toc]: README.md#using-omnibus-gitlab
...@@ -5,16 +5,22 @@ This is the documentation for installations from source. For installations ...@@ -5,16 +5,22 @@ This is the documentation for installations from source. For installations
using the Omnibus GitLab packages, follow the using the Omnibus GitLab packages, follow the
[**Omnibus GitLab Geo nodes configuration**](configuration.md) guide. [**Omnibus GitLab Geo nodes configuration**](configuration.md) guide.
## Configuring a new secondary node
>**Note:** >**Note:**
Stages of the setup process must be completed in the documented order. This is the final step in setting up a secondary Geo node. Stages of the setup
Before attempting the steps in this stage, [complete all prior stages][toc]. process must be completed in the documented order. Before attempting the steps
in this stage, [complete all prior stages](README.md#using-gitlab-installed-from-source).
The basic steps of configuring a secondary node are:
This is the final step you need to follow in order to setup a Geo node. 1. replicate required configurations between the primary and the secondaries;
1. configure a second, tracking database on each secondary;
1. start GitLab on the secondary node's machine.
You are encouraged to first read through all the steps before executing them You are encouraged to first read through all the steps before executing them
in your testing/production environment. in your testing/production environment.
## Setting up GitLab
>**Notes:** >**Notes:**
- **Do not** setup any custom authentication in the secondary nodes, this will be - **Do not** setup any custom authentication in the secondary nodes, this will be
...@@ -22,90 +28,48 @@ in your testing/production environment. ...@@ -22,90 +28,48 @@ in your testing/production environment.
- **Do not** add anything in the secondaries Geo nodes admin area - **Do not** add anything in the secondaries Geo nodes admin area
(**Admin Area ➔ Geo Nodes**). This is handled solely by the primary node. (**Admin Area ➔ Geo Nodes**). This is handled solely by the primary node.
After having installed GitLab Enterprise Edition in the instance that will serve
as a Geo node and set up the [database replication](database_source.md), the
next steps can be summed up to:
1. Replicate some required configurations between the primary and the secondaries
1. Configure a second, tracking database on each secondary
1. Start GitLab on the secondary node's machine
### Prerequisites
This is the last step of configuring a Geo secondary node. Make sure you have
followed the first two steps of the [Setup instructions](README.md#setup-instructions):
1. You have already installed on the secondary server the same version of
GitLab Enterprise Edition that is present on the primary server.
1. You have set up the database replication.
1. Your secondary node is allowed to communicate via HTTP/HTTPS with
your primary node (make sure your firewall is not blocking that).
1. Your nodes must have an NTP service running to synchronize the clocks.
You can use different timezones, but the hour relative to UTC can't be more
than 60 seconds off from each node.
1. You have set up another PostgreSQL database that can store writes for the secondary.
Note that this MUST be on another instance, since the primary replicated database
is read-only.
### Step 1. Copying the database encryption key ### Step 1. Copying the database encryption key
GitLab stores a unique encryption key in disk that we use to safely store GitLab stores a unique encryption key on disk that is used to encrypt
sensitive data in the database. Any secondary node must have the sensitive data stored in the database. All secondary nodes must have the
**exact same value** for `db_key_base` as defined in the primary one. **exact same value** for `db_key_base` as defined on the primary node.
1. SSH into the **primary** node and login as root:
``` 1. SSH into the **primary** node, and execute the command below to display the
sudo -i current encryption key:
```
1. Execute the command below to display the current encryption key and copy it: ```bash
```
sudo -u git -H bundle exec rake geo:db:show_encryption_key RAILS_ENV=production sudo -u git -H bundle exec rake geo:db:show_encryption_key RAILS_ENV=production
``` ```
1. SSH into the **secondary** node and login as root: Copy the encryption key to bring it to the secondary node in the following steps.
``` 1. SSH into the **secondary**, and execute the command below to open the
sudo -i `secrets.yml` file:
```
1. Open the `secrets.yml` file and change the value of `db_key_base` to the ```bash
output of the previous step:
```
sudo -u git -H editor config/secrets.yml sudo -u git -H editor config/secrets.yml
``` ```
1. Save and close the file. 1. Change the value of `db_key_base` to the output from the primary node.
Then save and close the file.
1. Restart GitLab for the changes to take effect: 1. Restart GitLab for the changes to take effect:
``` ```bash
service gitlab restart service gitlab restart
``` ```
The secondary will start automatically replicating missing data from the Once restarted, the secondary will automatically start replicating missing data
primary in a process known as backfill. Meanwhile, the primary node will start from the primary in a process known as backfill. Meanwhile, the primary node
to notify changes to the secondary, which will act on those notifications will start to notify the secondary of any changes, so that the secondary can
immediately. Make sure the secondary instance is running and accessible. act on those notifications immediately.
### Step 2. Enabling hashed storage (optional, GitLab 10.0)
>**Warning** Make sure the secondary instance is running and accessible. You can login to
Hashed storage is in **Alpha**. It is considered experimental and not the secondary node with the same credentials as used in the primary.
production-ready. See [Hashed
Storage](../administration/repository_storage_types.md) for more detail.
Using hashed storage significantly improves Geo replication - project and group ### Step 2. (Optional) Enabling hashed storage (from GitLab 10.0)
renames no longer require synchronization between nodes.
1. Visit the **primary** node's **Admin Area ➔ Settings** Read [Enabling Hashed Storage](configuration.md#step-2-optional-enabling-hashed-storage-from-gitlab-10-0)
(`/admin/application_settings`) in your browser
1. In the `Repository Storages` section, check `Create new projects using hashed storage paths`:
![](img/hashed-storage.png)
### Step 3. (Optional) Configuring the secondary to trust the primary ### Step 3. (Optional) Configuring the secondary to trust the primary
...@@ -130,52 +94,10 @@ method to be enabled. Navigate to **Admin Area ➔ Settings** ...@@ -130,52 +94,10 @@ method to be enabled. Navigate to **Admin Area ➔ Settings**
(`/admin/application_settings`) on the primary node, and set (`/admin/application_settings`) on the primary node, and set
`Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`. `Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`.
### Verify proper functioning of the secondary node
### Step 5. Managing the secondary GitLab node Read [Verify proper functioning of the secondary node](configuration.md#verify-proper-functioning-of-the-secondary-node).
You can monitor the status of the syncing process on a secondary node
by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`)
in your browser.
Please note that if `git_data_dirs` is customized on the primary for multiple
repository shards you must duplicate the same configuration on the secondary.
![GitLab Geo dashboard](img/geo-node-dashboard.png)
Disabling a secondary node stops the syncing process.
The two most obvious issues that replication can have here are:
1. Database replication not working well
1. Instance to instance notification not working. In that case, it can be
something of the following:
- You are using a custom certificate or custom CA (see the
[Troubleshooting](configuration.md#troubleshooting) section)
- Instance is firewalled (check your firewall rules)
Currently, this is what is synced:
* Git repositories
* Wikis
* LFS objects
* Issue, merge request, snippet and comment attachments
* User, group, and project avatars
You can monitor the status of the syncing process on a secondary node
by visiting the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`)
in your browser.
## Next steps
Your nodes should now be ready to use. You can login to the secondary node
with the same credentials as used in the primary. Visit the secondary node's
**Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`) in your browser to check if it's
correctly identified as a secondary Geo node and if Geo is enabled.
If your installation isn't working properly, check the
[troubleshooting](configuration.md#troubleshooting) section.
Point your users to the ["Using a Geo Server" guide](using_a_geo_server.md).
## Selective replication ## Selective replication
...@@ -184,5 +106,3 @@ Read [Selective replication](configuration.md#selective-replication). ...@@ -184,5 +106,3 @@ Read [Selective replication](configuration.md#selective-replication).
## Troubleshooting ## Troubleshooting
Read the [troubleshooting document](troubleshooting.md). Read the [troubleshooting document](troubleshooting.md).
[toc]: README.md#using-gitlab-installed-from-source
doc/gitlab-geo/img/geo-overview.png

75.1 KB | W: | H:

doc/gitlab-geo/img/geo-overview.png

76.8 KB | W: | H:

doc/gitlab-geo/img/geo-overview.png
doc/gitlab-geo/img/geo-overview.png
doc/gitlab-geo/img/geo-overview.png
doc/gitlab-geo/img/geo-overview.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -446,6 +446,7 @@ For example: ...@@ -446,6 +446,7 @@ For example:
# This header has Unicode in it: 한글 # This header has Unicode in it: 한글
## This header has spaces in it ## This header has spaces in it
### This header has spaces in it ### This header has spaces in it
## This header has 3.5 in it (and parentheses)
``` ```
Would generate the following link IDs: Would generate the following link IDs:
...@@ -455,6 +456,7 @@ Would generate the following link IDs: ...@@ -455,6 +456,7 @@ Would generate the following link IDs:
1. `this-header-has-unicode-in-it-한글` 1. `this-header-has-unicode-in-it-한글`
1. `this-header-has-spaces-in-it` 1. `this-header-has-spaces-in-it`
1. `this-header-has-spaces-in-it-1` 1. `this-header-has-spaces-in-it-1`
1. `this-header-has-3-5-in-it-and-parentheses`
Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID. Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
......
...@@ -44,6 +44,7 @@ class License < ActiveRecord::Base ...@@ -44,6 +44,7 @@ class License < ActiveRecord::Base
group_issue_boards group_issue_boards
jira_dev_panel_integration jira_dev_panel_integration
ldap_group_sync_filter ldap_group_sync_filter
multiple_clusters
object_storage object_storage
service_desk service_desk
variable_environment_scope variable_environment_scope
......
...@@ -379,15 +379,16 @@ module API ...@@ -379,15 +379,16 @@ module API
post ":id/fork/:forked_from_id" do post ":id/fork/:forked_from_id" do
authenticated_as_admin! authenticated_as_admin!
forked_from_project = find_project!(params[:forked_from_id]) fork_from_project = find_project!(params[:forked_from_id])
not_found!("Source Project") unless forked_from_project
if user_project.forked_from_project.nil? not_found!("Source Project") unless fork_from_project
user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
::Projects::ForksCountService.new(forked_from_project).refresh_cache result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project)
if result
present user_project.reload, with: Entities::Project
else else
render_api_error!("Project already forked", 409) render_api_error!("Project already forked", 409) if user_project.forked?
end end
end end
...@@ -395,11 +396,11 @@ module API ...@@ -395,11 +396,11 @@ module API
delete ":id/fork" do delete ":id/fork" do
authorize! :remove_fork_project, user_project authorize! :remove_fork_project, user_project
if user_project.forked? result = destroy_conditionally!(user_project) do
destroy_conditionally!(user_project.forked_project_link) ::Projects::UnlinkForkService.new(user_project, current_user).execute
else
not_modified!
end end
result ? status(204) : not_modified!
end end
desc 'Share the project with a group' do desc 'Share the project with a group' do
......
...@@ -280,7 +280,7 @@ module Gitlab ...@@ -280,7 +280,7 @@ module Gitlab
The `#{branch}` branch applies cleanly to EE/master! The `#{branch}` branch applies cleanly to EE/master!
Much ❤️! For more information, see Much ❤️! For more information, see
https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
#{THANKS_FOR_READING_BANNER} #{THANKS_FOR_READING_BANNER}
} }
end end
...@@ -357,7 +357,7 @@ module Gitlab ...@@ -357,7 +357,7 @@ module Gitlab
Once this is done, you can retry this failed build, and it should pass. Once this is done, you can retry this failed build, and it should pass.
Stay 💪 ! For more information, see Stay 💪 ! For more information, see
https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
#{THANKS_FOR_READING_BANNER} #{THANKS_FOR_READING_BANNER}
} }
end end
...@@ -378,7 +378,7 @@ module Gitlab ...@@ -378,7 +378,7 @@ module Gitlab
retry this build. retry this build.
Stay 💪 ! For more information, see Stay 💪 ! For more information, see
https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
#{THANKS_FOR_READING_BANNER} #{THANKS_FOR_READING_BANNER}
} }
end end
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-11-02 14:42+0100\n" "POT-Creation-Date: 2017-12-05 20:31+0100\n"
"PO-Revision-Date: 2017-11-02 14:42+0100\n" "PO-Revision-Date: 2017-12-05 20:31+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -58,6 +58,9 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts ...@@ -58,6 +58,9 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%{text} is available"
msgstr ""
msgid "(checkout the %{link} for information on how to install it)." msgid "(checkout the %{link} for information on how to install it)."
msgstr "" msgstr ""
...@@ -102,24 +105,15 @@ msgstr "" ...@@ -102,24 +105,15 @@ msgstr ""
msgid "Activity" msgid "Activity"
msgstr "" msgstr ""
msgid "Add"
msgstr ""
msgid "Add Changelog" msgid "Add Changelog"
msgstr "" msgstr ""
msgid "Add Contribution guide" msgid "Add Contribution guide"
msgstr "" msgstr ""
msgid "Add Group Webhooks and GitLab Enterprise Edition."
msgstr ""
msgid "Add License" msgid "Add License"
msgstr "" msgstr ""
msgid "Add an SSH key to your profile to pull or push via SSH."
msgstr ""
msgid "Add new directory" msgid "Add new directory"
msgstr "" msgstr ""
...@@ -132,6 +126,12 @@ msgstr "" ...@@ -132,6 +126,12 @@ msgstr ""
msgid "All" msgid "All"
msgstr "" msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
msgid "An error occurred. Please try again." msgid "An error occurred. Please try again."
msgstr "" msgstr ""
...@@ -141,6 +141,12 @@ msgstr "" ...@@ -141,6 +141,12 @@ msgstr ""
msgid "Applications" msgid "Applications"
msgstr "" msgstr ""
msgid "Apr"
msgstr ""
msgid "April"
msgstr ""
msgid "Archived project! Repository is read-only" msgid "Archived project! Repository is read-only"
msgstr "" msgstr ""
...@@ -168,6 +174,12 @@ msgstr "" ...@@ -168,6 +174,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}" msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "" msgstr ""
msgid "Aug"
msgstr ""
msgid "August"
msgstr ""
msgid "Authentication Log" msgid "Authentication Log"
msgstr "" msgstr ""
...@@ -201,58 +213,7 @@ msgstr "" ...@@ -201,58 +213,7 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project." msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr "" msgstr ""
msgid "Billing" msgid "Available"
msgstr ""
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
msgstr ""
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
msgstr ""
msgid "BillingPlans|Current plan"
msgstr ""
msgid "BillingPlans|Customer Support"
msgstr ""
msgid "BillingPlans|Downgrade"
msgstr ""
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
msgstr ""
msgid "BillingPlans|Manage plan"
msgstr ""
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
msgstr ""
msgid "BillingPlans|See all %{plan_name} features"
msgstr ""
msgid "BillingPlans|This group uses the plan associated with its parent group."
msgstr ""
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
msgstr ""
msgid "BillingPlans|Upgrade"
msgstr ""
msgid "BillingPlans|You are currently on the %{plan_link} plan."
msgstr ""
msgid "BillingPlans|frequently asked questions"
msgstr ""
msgid "BillingPlans|monthly"
msgstr ""
msgid "BillingPlans|paid annually at %{price_per_year}"
msgstr ""
msgid "BillingPlans|per user"
msgstr "" msgstr ""
msgid "Branch" msgid "Branch"
...@@ -266,6 +227,12 @@ msgstr "" ...@@ -266,6 +227,12 @@ msgstr ""
msgid "Branch has changed" msgid "Branch has changed"
msgstr "" msgstr ""
msgid "Branch is already taken"
msgstr ""
msgid "Branch name"
msgstr ""
msgid "BranchSwitcherPlaceholder|Search branches" msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "" msgstr ""
...@@ -326,9 +293,6 @@ msgstr "" ...@@ -326,9 +293,6 @@ msgstr ""
msgid "Branches|Sort by" msgid "Branches|Sort by"
msgstr "" msgstr ""
msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
msgstr ""
msgid "Branches|The default branch cannot be deleted" msgid "Branches|The default branch cannot be deleted"
msgstr "" msgstr ""
...@@ -341,15 +305,9 @@ msgstr "" ...@@ -341,15 +305,9 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:" msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr "" msgstr ""
msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
msgstr ""
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}." msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr "" msgstr ""
msgid "Branches|diverged from upstream"
msgstr ""
msgid "Branches|merged" msgid "Branches|merged"
msgstr "" msgstr ""
...@@ -389,9 +347,6 @@ msgstr "" ...@@ -389,9 +347,6 @@ msgstr ""
msgid "Cancel edit" msgid "Cancel edit"
msgstr "" msgstr ""
msgid "Change Weight"
msgstr ""
msgid "ChangeTypeActionLabel|Pick into branch" msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "" msgstr ""
...@@ -413,13 +368,16 @@ msgstr "" ...@@ -413,13 +368,16 @@ msgstr ""
msgid "Chat" msgid "Chat"
msgstr "" msgstr ""
msgid "Cherry-pick this commit" msgid "Checking %{text} availability…"
msgstr "" msgstr ""
msgid "Cherry-pick this merge request" msgid "Checking branch availability..."
msgstr "" msgstr ""
msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all." msgid "Cherry-pick this commit"
msgstr ""
msgid "Cherry-pick this merge request"
msgstr "" msgstr ""
msgid "CiStatusLabel|canceled" msgid "CiStatusLabel|canceled"
...@@ -482,13 +440,43 @@ msgstr "" ...@@ -482,13 +440,43 @@ msgstr ""
msgid "Clone repository" msgid "Clone repository"
msgstr "" msgstr ""
msgid "Close" msgid "Cluster"
msgstr "" msgstr ""
msgid "Cluster" msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below" msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
msgstr ""
msgid "ClusterIntegration|API URL"
msgstr ""
msgid "ClusterIntegration|Active"
msgstr ""
msgid "ClusterIntegration|Add an existing cluster"
msgstr ""
msgid "ClusterIntegration|Add cluster"
msgstr ""
msgid "ClusterIntegration|All"
msgstr ""
msgid "ClusterIntegration|Applications"
msgstr ""
msgid "ClusterIntegration|CA Certificate"
msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr ""
msgid "ClusterIntegration|Choose how to set up cluster integration"
msgstr ""
msgid "ClusterIntegration|Cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Cluster details" msgid "ClusterIntegration|Cluster details"
...@@ -512,21 +500,54 @@ msgstr "" ...@@ -512,21 +500,54 @@ msgstr ""
msgid "ClusterIntegration|Cluster name" msgid "ClusterIntegration|Cluster name"
msgstr "" msgstr ""
msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine. Refresh the page to see cluster's details"
msgstr ""
msgid "ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
msgstr ""
msgid "ClusterIntegration|Copy API URL"
msgstr ""
msgid "ClusterIntegration|Copy CA Certificate"
msgstr ""
msgid "ClusterIntegration|Copy Token"
msgstr "" msgstr ""
msgid "ClusterIntegration|Copy cluster name" msgid "ClusterIntegration|Copy cluster name"
msgstr "" msgstr ""
msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
msgstr ""
msgid "ClusterIntegration|Create cluster" msgid "ClusterIntegration|Create cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Create new cluster on Google Container Engine" msgid "ClusterIntegration|Create cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Create on GKE"
msgstr "" msgstr ""
msgid "ClusterIntegration|Enable cluster integration" msgid "ClusterIntegration|Enable cluster integration"
msgstr "" msgstr ""
msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Enter the details for your cluster"
msgstr ""
msgid "ClusterIntegration|Environment pattern"
msgstr ""
msgid "ClusterIntegration|GKE pricing"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project ID" msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "" msgstr ""
...@@ -536,27 +557,75 @@ msgstr "" ...@@ -536,27 +557,75 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project" msgid "ClusterIntegration|Google Container Engine project"
msgstr "" msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
msgid "ClusterIntegration|Inactive"
msgstr ""
msgid "ClusterIntegration|Ingress"
msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
msgstr ""
msgid "ClusterIntegration|Installed"
msgstr ""
msgid "ClusterIntegration|Installing"
msgstr ""
msgid "ClusterIntegration|Integrate cluster automation"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}" msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "" msgstr ""
msgid "ClusterIntegration|Learn more about Clusters"
msgstr ""
msgid "ClusterIntegration|Machine type" msgid "ClusterIntegration|Machine type"
msgstr "" msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters" msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr "" msgstr ""
msgid "ClusterIntegration|Manage Cluster integration on your GitLab project" msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr "" msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}" msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr "" msgstr ""
msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
msgstr ""
msgid "ClusterIntegration|Note:"
msgstr ""
msgid "ClusterIntegration|Number of nodes" msgid "ClusterIntegration|Number of nodes"
msgstr "" msgstr ""
msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:" msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr "" msgstr ""
msgid "ClusterIntegration|Problem setting up the cluster"
msgstr ""
msgid "ClusterIntegration|Problem setting up the clusters list"
msgstr ""
msgid "ClusterIntegration|Project ID"
msgstr ""
msgid "ClusterIntegration|Project namespace"
msgstr ""
msgid "ClusterIntegration|Project namespace (optional, unique)" msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr "" msgstr ""
...@@ -572,6 +641,12 @@ msgstr "" ...@@ -572,6 +641,12 @@ msgstr ""
msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine." msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine."
msgstr "" msgstr ""
msgid "ClusterIntegration|Request to begin installing failed"
msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster" msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr "" msgstr ""
...@@ -584,15 +659,33 @@ msgstr "" ...@@ -584,15 +659,33 @@ msgstr ""
msgid "ClusterIntegration|See zones" msgid "ClusterIntegration|See zones"
msgstr "" msgstr ""
msgid "ClusterIntegration|Service token"
msgstr ""
msgid "ClusterIntegration|Show"
msgstr ""
msgid "ClusterIntegration|Something went wrong on our end." msgid "ClusterIntegration|Something went wrong on our end."
msgstr "" msgstr ""
msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr "" msgstr ""
msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr ""
msgid "ClusterIntegration|There are no clusters to show"
msgstr ""
msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster" msgid "ClusterIntegration|Toggle Cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "" msgstr ""
...@@ -608,9 +701,15 @@ msgstr "" ...@@ -608,9 +701,15 @@ msgstr ""
msgid "ClusterIntegration|cluster" msgid "ClusterIntegration|cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|documentation"
msgstr ""
msgid "ClusterIntegration|help page" msgid "ClusterIntegration|help page"
msgstr "" msgstr ""
msgid "ClusterIntegration|installing applications"
msgstr ""
msgid "ClusterIntegration|meets the requirements" msgid "ClusterIntegration|meets the requirements"
msgstr "" msgstr ""
...@@ -625,11 +724,6 @@ msgid_plural "Commits" ...@@ -625,11 +724,6 @@ msgid_plural "Commits"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Commit %d file"
msgid_plural "Commit %d files"
msgstr[0] ""
msgstr[1] ""
msgid "Commit Message" msgid "Commit Message"
msgstr "" msgstr ""
...@@ -711,13 +805,13 @@ msgstr "" ...@@ -711,13 +805,13 @@ msgstr ""
msgid "Contributors" msgid "Contributors"
msgstr "" msgstr ""
msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node" msgid "ContributorsPage|Building repository graph."
msgstr "" msgstr ""
msgid "Control the maximum concurrency of repository backfill for this secondary node" msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
msgstr "" msgstr ""
msgid "Copy SSH public key to clipboard" msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr "" msgstr ""
msgid "Copy URL to clipboard" msgid "Copy URL to clipboard"
...@@ -810,6 +904,12 @@ msgstr "" ...@@ -810,6 +904,12 @@ msgstr ""
msgid "DashboardProjects|Personal" msgid "DashboardProjects|Personal"
msgstr "" msgstr ""
msgid "Dec"
msgstr ""
msgid "December"
msgstr ""
msgid "Define a custom pattern with cron syntax" msgid "Define a custom pattern with cron syntax"
msgstr "" msgstr ""
...@@ -827,9 +927,6 @@ msgstr "" ...@@ -827,9 +927,6 @@ msgstr ""
msgid "Description" msgid "Description"
msgstr "" msgstr ""
msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
msgstr ""
msgid "Details" msgid "Details"
msgstr "" msgstr ""
...@@ -842,9 +939,6 @@ msgstr "" ...@@ -842,9 +939,6 @@ msgstr ""
msgid "Dismiss Cycle Analytics introduction box" msgid "Dismiss Cycle Analytics introduction box"
msgstr "" msgstr ""
msgid "Dismiss Merge Request promotion"
msgstr ""
msgid "Don't show again" msgid "Don't show again"
msgstr "" msgstr ""
...@@ -884,6 +978,60 @@ msgstr "" ...@@ -884,6 +978,60 @@ msgstr ""
msgid "Emails" msgid "Emails"
msgstr "" msgstr ""
msgid "Environments|An error occurred while fetching the environments."
msgstr ""
msgid "Environments|An error occurred while making the request."
msgstr ""
msgid "Environments|Commit"
msgstr ""
msgid "Environments|Deployment"
msgstr ""
msgid "Environments|Environment"
msgstr ""
msgid "Environments|Environments"
msgstr ""
msgid "Environments|Environments are places where code gets deployed, such as staging or production."
msgstr ""
msgid "Environments|Job"
msgstr ""
msgid "Environments|New environment"
msgstr ""
msgid "Environments|No deployments yet"
msgstr ""
msgid "Environments|Open"
msgstr ""
msgid "Environments|Re-deploy"
msgstr ""
msgid "Environments|Read more about environments"
msgstr ""
msgid "Environments|Rollback"
msgstr ""
msgid "Environments|Show all"
msgstr ""
msgid "Environments|Updated"
msgstr ""
msgid "Environments|You don't have any environments right now."
msgstr ""
msgid "Error occurred when toggling the notification subscription"
msgstr ""
msgid "EventFilterBy|Filter by all" msgid "EventFilterBy|Filter by all"
msgstr "" msgstr ""
...@@ -923,6 +1071,12 @@ msgstr "" ...@@ -923,6 +1071,12 @@ msgstr ""
msgid "Failed to remove the pipeline schedule" msgid "Failed to remove the pipeline schedule"
msgstr "" msgstr ""
msgid "Feb"
msgstr ""
msgid "February"
msgstr ""
msgid "File name" msgid "File name"
msgstr "" msgstr ""
...@@ -967,21 +1121,6 @@ msgstr "" ...@@ -967,21 +1121,6 @@ msgstr ""
msgid "GPG Keys" msgid "GPG Keys"
msgstr "" msgstr ""
msgid "Geo Nodes"
msgstr ""
msgid "Geo|File sync capacity"
msgstr ""
msgid "Geo|Groups to replicate"
msgstr ""
msgid "Geo|Repository sync capacity"
msgstr ""
msgid "Geo|Select groups to replicate."
msgstr ""
msgid "Git storage health information has been reset" msgid "Git storage health information has been reset"
msgstr "" msgstr ""
...@@ -1093,51 +1232,46 @@ msgstr "" ...@@ -1093,51 +1232,46 @@ msgstr ""
msgid "Import repository" msgid "Import repository"
msgstr "" msgstr ""
msgid "Improve Issue boards with GitLab Enterprise Edition." msgid "Install a Runner compatible with GitLab CI"
msgstr "" msgstr ""
msgid "Improve issues management with Issue weight and GitLab Enterprise Edition." msgid "Internal - The group and any internal projects can be viewed by any logged in user."
msgstr "" msgstr ""
msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition." msgid "Internal - The project can be accessed by any logged in user."
msgstr "" msgstr ""
msgid "Install a Runner compatible with GitLab CI" msgid "Interval Pattern"
msgstr "" msgstr ""
msgid "Instance" msgid "Introducing Cycle Analytics"
msgid_plural "Instances"
msgstr[0] ""
msgstr[1] ""
msgid "Internal - The group and any internal projects can be viewed by any logged in user."
msgstr "" msgstr ""
msgid "Internal - The project can be accessed by any logged in user." msgid "Issue events"
msgstr "" msgstr ""
msgid "Interval Pattern" msgid "IssueBoards|Board"
msgstr "" msgstr ""
msgid "Introducing Cycle Analytics" msgid "Issues"
msgstr "" msgstr ""
msgid "Issue board focus mode" msgid "Jan"
msgstr "" msgstr ""
msgid "Issue boards with milestones" msgid "January"
msgstr "" msgstr ""
msgid "Issue events" msgid "Jul"
msgstr "" msgstr ""
msgid "IssueBoards|Board" msgid "July"
msgstr "" msgstr ""
msgid "IssueBoards|Boards" msgid "Jun"
msgstr "" msgstr ""
msgid "Issues" msgid "June"
msgstr "" msgstr ""
msgid "LFSStatus|Disabled" msgid "LFSStatus|Disabled"
...@@ -1193,9 +1327,6 @@ msgstr "" ...@@ -1193,9 +1327,6 @@ msgstr ""
msgid "Leave project" msgid "Leave project"
msgstr "" msgstr ""
msgid "License"
msgstr ""
msgid "Limited to showing %d event at most" msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most" msgid_plural "Limited to showing %d events at most"
msgstr[0] "" msgstr[0] ""
...@@ -1207,15 +1338,21 @@ msgstr "" ...@@ -1207,15 +1338,21 @@ msgstr ""
msgid "Locked" msgid "Locked"
msgstr "" msgstr ""
msgid "Locked Files" msgid "Login"
msgstr "" msgstr ""
msgid "Login" msgid "Mar"
msgstr ""
msgid "March"
msgstr "" msgstr ""
msgid "Maximum git storage failures" msgid "Maximum git storage failures"
msgstr "" msgstr ""
msgid "May"
msgstr ""
msgid "Median" msgid "Median"
msgstr "" msgstr ""
...@@ -1243,9 +1380,6 @@ msgstr "" ...@@ -1243,9 +1380,6 @@ msgstr ""
msgid "More information is available|here" msgid "More information is available|here"
msgstr "" msgstr ""
msgid "Multiple issue boards"
msgstr ""
msgid "New Cluster" msgid "New Cluster"
msgstr "" msgstr ""
...@@ -1260,6 +1394,9 @@ msgstr "" ...@@ -1260,6 +1394,9 @@ msgstr ""
msgid "New branch" msgid "New branch"
msgstr "" msgstr ""
msgid "New branch unavailable"
msgstr ""
msgid "New directory" msgid "New directory"
msgstr "" msgstr ""
...@@ -1299,6 +1436,9 @@ msgstr "" ...@@ -1299,6 +1436,9 @@ msgstr ""
msgid "No schedules" msgid "No schedules"
msgstr "" msgstr ""
msgid "No time spent"
msgstr ""
msgid "None" msgid "None"
msgstr "" msgstr ""
...@@ -1365,12 +1505,24 @@ msgstr "" ...@@ -1365,12 +1505,24 @@ msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "" msgstr ""
msgid "Nov"
msgstr ""
msgid "November"
msgstr ""
msgid "Number of access attempts" msgid "Number of access attempts"
msgstr "" msgstr ""
msgid "Number of failures before backing off" msgid "Number of failures before backing off"
msgstr "" msgstr ""
msgid "Oct"
msgstr ""
msgid "October"
msgstr ""
msgid "OfSearchInADropdown|Filter" msgid "OfSearchInADropdown|Filter"
msgstr "" msgstr ""
...@@ -1422,9 +1574,6 @@ msgstr "" ...@@ -1422,9 +1574,6 @@ msgstr ""
msgid "Pipeline Schedules" msgid "Pipeline Schedules"
msgstr "" msgstr ""
msgid "Pipeline quota"
msgstr ""
msgid "PipelineCharts|Failed:" msgid "PipelineCharts|Failed:"
msgstr "" msgstr ""
...@@ -1611,22 +1760,10 @@ msgstr "" ...@@ -1611,22 +1760,10 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph" msgid "ProjectNetworkGraph|Graph"
msgstr "" msgstr ""
msgid "ProjectSettings|Contact an admin to change this setting." msgid "ProjectSettings|Immediately run a pipeline on the default branch"
msgstr ""
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr ""
msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr "" msgstr ""
msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails." msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
msgstr "" msgstr ""
msgid "Projects" msgid "Projects"
...@@ -1653,19 +1790,46 @@ msgstr "" ...@@ -1653,19 +1790,46 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support" msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "" msgstr ""
msgid "Public - The group and any public projects can be viewed without any authentication." msgid "PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server."
msgstr "" msgstr ""
msgid "Public - The project can be accessed without any authentication." msgid "PrometheusService|Finding and configuring metrics..."
msgstr "" msgstr ""
msgid "Push Rules" msgid "PrometheusService|Metrics"
msgstr "" msgstr ""
msgid "Push events" msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
msgid "PrometheusService|Missing environment variable"
msgstr ""
msgid "PrometheusService|Monitored"
msgstr ""
msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
msgstr "" msgstr ""
msgid "PushRule|Committer restriction" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgstr ""
msgid "PrometheusService|View environments"
msgstr ""
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
msgid "Public - The project can be accessed without any authentication."
msgstr ""
msgid "Push events"
msgstr "" msgstr ""
msgid "Read more" msgid "Read more"
...@@ -1680,9 +1844,6 @@ msgstr "" ...@@ -1680,9 +1844,6 @@ msgstr ""
msgid "RefSwitcher|Tags" msgid "RefSwitcher|Tags"
msgstr "" msgstr ""
msgid "Registry"
msgstr ""
msgid "Related Commits" msgid "Related Commits"
msgstr "" msgstr ""
...@@ -1770,6 +1931,12 @@ msgstr "" ...@@ -1770,6 +1931,12 @@ msgstr ""
msgid "Select target branch" msgid "Select target branch"
msgstr "" msgstr ""
msgid "Sep"
msgstr ""
msgid "September"
msgstr ""
msgid "Service Templates" msgid "Service Templates"
msgstr "" msgstr ""
...@@ -1808,7 +1975,7 @@ msgstr "" ...@@ -1808,7 +1975,7 @@ msgstr ""
msgid "Something went wrong on our end." msgid "Something went wrong on our end."
msgstr "" msgstr ""
msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}" msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr "" msgstr ""
msgid "Something went wrong while fetching the projects." msgid "Something went wrong while fetching the projects."
...@@ -1859,9 +2026,6 @@ msgstr "" ...@@ -1859,9 +2026,6 @@ msgstr ""
msgid "SortOptions|Least popular" msgid "SortOptions|Least popular"
msgstr "" msgstr ""
msgid "SortOptions|Less weight"
msgstr ""
msgid "SortOptions|Milestone" msgid "SortOptions|Milestone"
msgstr "" msgstr ""
...@@ -1871,9 +2035,6 @@ msgstr "" ...@@ -1871,9 +2035,6 @@ msgstr ""
msgid "SortOptions|Milestone due soon" msgid "SortOptions|Milestone due soon"
msgstr "" msgstr ""
msgid "SortOptions|More weight"
msgstr ""
msgid "SortOptions|Most popular" msgid "SortOptions|Most popular"
msgstr "" msgstr ""
...@@ -1913,12 +2074,15 @@ msgstr "" ...@@ -1913,12 +2074,15 @@ msgstr ""
msgid "SortOptions|Start soon" msgid "SortOptions|Start soon"
msgstr "" msgstr ""
msgid "SortOptions|Weight" msgid "Source"
msgstr "" msgstr ""
msgid "Source code" msgid "Source code"
msgstr "" msgstr ""
msgid "Source is not available"
msgstr ""
msgid "Spam Logs" msgid "Spam Logs"
msgstr "" msgstr ""
...@@ -1937,6 +2101,9 @@ msgstr "" ...@@ -1937,6 +2101,9 @@ msgstr ""
msgid "Start the Runner!" msgid "Start the Runner!"
msgstr "" msgstr ""
msgid "Stopped"
msgstr ""
msgid "Subgroups" msgid "Subgroups"
msgstr "" msgstr ""
...@@ -1957,16 +2124,79 @@ msgstr[1] "" ...@@ -1957,16 +2124,79 @@ msgstr[1] ""
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
msgid "Target Branch" msgid "TagsPage|Browse commits"
msgstr "" msgstr ""
msgid "Team" msgid "TagsPage|Browse files"
msgstr ""
msgid "TagsPage|Can't find HEAD commit for this tag"
msgstr ""
msgid "TagsPage|Cancel"
msgstr ""
msgid "TagsPage|Create tag"
msgstr ""
msgid "TagsPage|Delete tag"
msgstr ""
msgid "TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?"
msgstr ""
msgid "TagsPage|Edit release notes"
msgstr ""
msgid "TagsPage|Existing branch name, tag, or commit SHA"
msgstr ""
msgid "TagsPage|Filter by tag name"
msgstr ""
msgid "TagsPage|New Tag"
msgstr ""
msgid "TagsPage|New tag"
msgstr "" msgstr ""
msgid "Thanks! Don't show me this again" msgid "TagsPage|Optionally, add a message to the tag."
msgstr "" msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project." msgid "TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page."
msgstr ""
msgid "TagsPage|Release notes"
msgstr ""
msgid "TagsPage|Repository has no tags yet."
msgstr ""
msgid "TagsPage|Sort by"
msgstr ""
msgid "TagsPage|Tags"
msgstr ""
msgid "TagsPage|Tags give the ability to mark specific points in history as being important"
msgstr ""
msgid "TagsPage|This tag has no release notes."
msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
msgid "TagsPage|Write your release notes or drag files here..."
msgstr ""
msgid "TagsPage|protected"
msgstr ""
msgid "Target Branch"
msgstr ""
msgid "Team"
msgstr "" msgstr ""
msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold" msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
...@@ -2210,10 +2440,10 @@ msgstr "" ...@@ -2210,10 +2440,10 @@ msgstr ""
msgid "Total Time" msgid "Total Time"
msgstr "" msgstr ""
msgid "Total test time for all commits/merges" msgid "Total issue time spent"
msgstr "" msgstr ""
msgid "Track activity with Contribution Analytics." msgid "Total test time for all commits/merges"
msgstr "" msgstr ""
msgid "Unlock" msgid "Unlock"
...@@ -2228,21 +2458,6 @@ msgstr "" ...@@ -2228,21 +2458,6 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "" msgstr ""
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
msgid "Upgrade your plan to activate Contribution Analytics."
msgstr ""
msgid "Upgrade your plan to activate Group Webhooks."
msgstr ""
msgid "Upgrade your plan to activate Issue weight."
msgstr ""
msgid "Upgrade your plan to improve Issue boards."
msgstr ""
msgid "Upload New File" msgid "Upload New File"
msgstr "" msgstr ""
...@@ -2285,12 +2500,6 @@ msgstr "" ...@@ -2285,12 +2500,6 @@ msgstr ""
msgid "We don't have enough data to show this stage." msgid "We don't have enough data to show this stage."
msgstr "" msgstr ""
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr ""
msgid "Weight"
msgstr ""
msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable" msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
msgstr "" msgstr ""
...@@ -2396,9 +2605,6 @@ msgstr "" ...@@ -2396,9 +2605,6 @@ msgstr ""
msgid "Wiki|Wiki Pages" msgid "Wiki|Wiki Pages"
msgstr "" msgstr ""
msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
msgstr ""
msgid "Withdraw Access Request" msgid "Withdraw Access Request"
msgstr "" msgstr ""
...@@ -2417,15 +2623,9 @@ msgstr "" ...@@ -2417,15 +2623,9 @@ msgstr ""
msgid "You are on a read-only GitLab instance." msgid "You are on a read-only GitLab instance."
msgstr "" msgstr ""
msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
msgstr ""
msgid "You can only add files when you are on a branch" msgid "You can only add files when you are on a branch"
msgstr "" msgstr ""
msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
msgstr ""
msgid "You cannot write to this read-only GitLab instance." msgid "You cannot write to this read-only GitLab instance."
msgstr "" msgstr ""
...@@ -2459,6 +2659,9 @@ msgstr "" ...@@ -2459,6 +2659,9 @@ msgstr ""
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile" msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "" msgstr ""
msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
msgstr ""
msgid "Your comment will not be visible to the public." msgid "Your comment will not be visible to the public."
msgstr "" msgstr ""
...@@ -2471,7 +2674,7 @@ msgstr "" ...@@ -2471,7 +2674,7 @@ msgstr ""
msgid "Your projects" msgid "Your projects"
msgstr "" msgstr ""
msgid "commit" msgid "branch name"
msgstr "" msgstr ""
msgid "day" msgid "day"
...@@ -2496,7 +2699,7 @@ msgstr "" ...@@ -2496,7 +2699,7 @@ msgstr ""
msgid "personal access token" msgid "personal access token"
msgstr "" msgstr ""
msgid "to help your contributors communicate effectively!" msgid "source"
msgstr "" msgstr ""
msgid "username" msgid "username"
......
...@@ -143,9 +143,9 @@ describe Projects::Clusters::GcpController do ...@@ -143,9 +143,9 @@ describe Projects::Clusters::GcpController do
expect(ClusterProvisionWorker).to receive(:perform_async) expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count } expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Providers::Gcp.count } .and change { Clusters::Providers::Gcp.count }
expect(response).to redirect_to(project_cluster_path(project, project.cluster)) expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
expect(project.cluster).to be_gcp expect(project.clusters.first).to be_gcp
expect(project.cluster).to be_kubernetes expect(project.clusters.first).to be_kubernetes
end end
end end
end end
......
...@@ -64,7 +64,9 @@ describe Projects::Clusters::UserController do ...@@ -64,7 +64,9 @@ describe Projects::Clusters::UserController do
expect(ClusterProvisionWorker).to receive(:perform_async) expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count } expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Platforms::Kubernetes.count } .and change { Clusters::Platforms::Kubernetes.count }
expect(response).to redirect_to(project_cluster_path(project, project.cluster)) expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
expect(project.clusters.first).to be_user
expect(project.clusters.first).to be_kubernetes
end end
end end
end end
......
...@@ -15,14 +15,72 @@ describe Projects::ClustersController do ...@@ -15,14 +15,72 @@ describe Projects::ClustersController do
sign_in(user) sign_in(user)
end end
context 'when project has a cluster' do context 'when project has one or more clusters' do
let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } let(:project) { create(:project) }
let!(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let!(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
it 'lists available clusters' do
go
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
end
it 'assigns counters to correct values' do
go
expect(assigns(:active_count)).to eq(1)
expect(assigns(:inactive_count)).to eq(1)
end
context 'when page is specified' do
let(:last_page) { project.clusters.page.total_pages }
it { expect(go).to redirect_to(project_cluster_path(project, project.cluster)) } before do
allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
create_list(:cluster, 2, :provided_by_gcp, projects: [project])
get :index, namespace_id: project.namespace, project_id: project, page: last_page
end
it 'redirects to the page' do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:clusters).current_page).to eq(last_page)
end
end
context 'when only enabled clusters are requested' do
it 'returns only enabled clusters' do
get :index, namespace_id: project.namespace, project_id: project, scope: 'active'
expect(assigns(:clusters)).to all(have_attributes(enabled: true))
end
end
context 'when only disabled clusters are requested' do
it 'returns only disabled clusters' do
get :index, namespace_id: project.namespace, project_id: project, scope: 'inactive'
expect(assigns(:clusters)).to all(have_attributes(enabled: false))
end
end
end end
context 'when project does not have a cluster' do context 'when project does not have a cluster' do
it { expect(go).to redirect_to(new_project_cluster_path(project)) } let(:project) { create(:project) }
it 'returns an empty state page' do
go
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index, partial: :empty_state)
expect(assigns(:clusters)).to eq([])
end
it 'assigns counters to zero' do
go
expect(assigns(:active_count)).to eq(0)
expect(assigns(:inactive_count)).to eq(0)
end
end end
end end
...@@ -146,7 +204,7 @@ describe Projects::ClustersController do ...@@ -146,7 +204,7 @@ describe Projects::ClustersController do
go go
cluster.reload cluster.reload
expect(response).to redirect_to(project_cluster_path(project, project.cluster)) expect(response).to redirect_to(project_cluster_path(project, cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.') expect(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey expect(cluster.enabled).to be_falsey
end end
...@@ -180,28 +238,77 @@ describe Projects::ClustersController do ...@@ -180,28 +238,77 @@ describe Projects::ClustersController do
sign_in(user) sign_in(user)
end end
context 'when changing parameters' do context 'when format is json' do
let(:params) do context 'when changing parameters' do
{ context 'when valid parameters are used' do
cluster: { let(:params) do
enabled: false, {
name: 'my-new-cluster-name', cluster: {
platform_kubernetes_attributes: { enabled: false,
namespace: 'my-namespace' name: 'my-new-cluster-name',
platform_kubernetes_attributes: {
namespace: 'my-namespace'
}
}
} }
} end
}
it "updates and redirects back to show page" do
go_json
cluster.reload
expect(response).to have_http_status(:no_content)
expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
end
end
context 'when invalid parameters are used' do
let(:params) do
{
cluster: {
enabled: false,
platform_kubernetes_attributes: {
namespace: 'my invalid namespace #@'
}
}
}
end
it "rejects changes" do
go_json
expect(response).to have_http_status(:bad_request)
end
end
end end
end
it "updates and redirects back to show page" do context 'when format is html' do
go context 'when update enabled' do
let(:params) do
{
cluster: {
enabled: false,
name: 'my-new-cluster-name',
platform_kubernetes_attributes: {
namespace: 'my-namespace'
}
}
}
end
cluster.reload it "updates and redirects back to show page" do
expect(response).to redirect_to(project_cluster_path(project, project.cluster)) go
expect(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey cluster.reload
expect(cluster.name).to eq('my-new-cluster-name') expect(response).to redirect_to(project_cluster_path(project, cluster))
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace') expect(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
end
end end
end end
end end
...@@ -228,6 +335,13 @@ describe Projects::ClustersController do ...@@ -228,6 +335,13 @@ describe Projects::ClustersController do
project_id: project, project_id: project,
id: cluster) id: cluster)
end end
def go_json
put :update, params.merge(namespace_id: project.namespace,
project_id: project,
id: cluster,
format: :json)
end
end end
describe 'DELETE destroy' do describe 'DELETE destroy' do
......
...@@ -14,6 +14,7 @@ describe Projects::HashedStorage::MigrateAttachmentsService do ...@@ -14,6 +14,7 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
context 'on success' do context 'on success' do
before do before do
TestEnv.clean_test_path
FileUtils.mkdir_p(FileUploader.dynamic_path_builder(old_attachments_path)) FileUtils.mkdir_p(FileUploader.dynamic_path_builder(old_attachments_path))
end end
......
...@@ -28,5 +28,9 @@ FactoryGirl.define do ...@@ -28,5 +28,9 @@ FactoryGirl.define do
provider_type :gcp provider_type :gcp
provider_gcp factory: [:cluster_provider_gcp, :creating] provider_gcp factory: [:cluster_provider_gcp, :creating]
end end
trait :disabled do
enabled false
end
end end
end end
...@@ -24,6 +24,7 @@ feature 'Gcp Cluster', :js do ...@@ -24,6 +24,7 @@ feature 'Gcp Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Create on GKE' click_link 'Create on GKE'
end end
...@@ -116,7 +117,7 @@ feature 'Gcp Cluster', :js do ...@@ -116,7 +117,7 @@ feature 'Gcp Cluster', :js do
it 'user sees creation form with the successful message' do it 'user sees creation form with the successful message' do
expect(page).to have_content('Cluster integration was successfully removed.') expect(page).to have_content('Cluster integration was successfully removed.')
expect(page).to have_link('Create on GKE') expect(page).to have_link('Add cluster')
end end
end end
end end
...@@ -126,6 +127,7 @@ feature 'Gcp Cluster', :js do ...@@ -126,6 +127,7 @@ feature 'Gcp Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Create on GKE' click_link 'Create on GKE'
end end
......
...@@ -16,6 +16,7 @@ feature 'User Cluster', :js do ...@@ -16,6 +16,7 @@ feature 'User Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Add an existing cluster' click_link 'Add an existing cluster'
end end
...@@ -94,7 +95,7 @@ feature 'User Cluster', :js do ...@@ -94,7 +95,7 @@ feature 'User Cluster', :js do
it 'user sees creation form with the successful message' do it 'user sees creation form with the successful message' do
expect(page).to have_content('Cluster integration was successfully removed.') expect(page).to have_content('Cluster integration was successfully removed.')
expect(page).to have_link('Add an existing cluster') expect(page).to have_link('Add cluster')
end end
end end
end end
......
...@@ -14,12 +14,82 @@ feature 'Clusters', :js do ...@@ -14,12 +14,82 @@ feature 'Clusters', :js do
context 'when user does not have a cluster and visits cluster index page' do context 'when user does not have a cluster and visits cluster index page' do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
end
it 'sees empty state' do
expect(page).to have_link('Add cluster')
expect(page).to have_selector('.empty-state')
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
click_link 'Create on GKE' it 'user sees a table with one cluster' do
# One is the header row, the other the cluster row
expect(page).to have_selector('.gl-responsive-table-row', count: 2)
end end
it 'user sees a new page' do it 'user sees a disabled add cluster button ' do
expect(page).to have_button('Create cluster') expect(page).to have_selector('.js-add-cluster.disabled')
end
it 'user sees navigation tabs' do
expect(page.find('.js-active-tab').text).to include('Active')
expect(page.find('.js-active-tab .badge').text).to include('1')
expect(page.find('.js-inactive-tab').text).to include('Inactive')
expect(page.find('.js-inactive-tab .badge').text).to include('0')
expect(page.find('.js-all-tab').text).to include('All')
expect(page.find('.js-all-tab .badge').text).to include('1')
end
context 'inline update of cluster' do
it 'user can update cluster' do
expect(page).to have_selector('.js-toggle-cluster-list')
end
context 'with sucessfull request' do
it 'user sees updated cluster' do
expect do
page.find('.js-toggle-cluster-list').click
wait_for_requests
end.to change { cluster.reload.enabled }
expect(page).not_to have_selector('.is-checked')
expect(cluster.reload).not_to be_enabled
end
end
context 'with failed request' do
it 'user sees not update cluster and error message' do
expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false }
page.find('.js-toggle-cluster-list').click
expect(page).to have_content('Something went wrong on our end.')
expect(page).to have_selector('.is-checked')
expect(cluster.reload).to be_enabled
end
end
end
context 'when user clicks on a cluster' do
before do
click_link cluster.name
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
end end
end end
end end
...@@ -177,7 +177,7 @@ describe 'Edit Project Settings' do ...@@ -177,7 +177,7 @@ describe 'Edit Project Settings' do
click_button "Save changes" click_button "Save changes"
end end
expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.disabled", count: 2) expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.is-disabled", count: 2)
end end
it "shows empty features project homepage" do it "shows empty features project homepage" do
...@@ -272,10 +272,10 @@ describe 'Edit Project Settings' do ...@@ -272,10 +272,10 @@ describe 'Edit Project Settings' do
end end
def toggle_feature_off(feature_name) def toggle_feature_off(feature_name)
find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.checked").click find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.is-checked").click
end end
def toggle_feature_on(feature_name) def toggle_feature_on(feature_name)
find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.checked)").click find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.is-checked)").click
end end
end end
require 'spec_helper'
describe ClustersFinder do
let(:project) { create(:project) }
set(:user) { create(:user) }
describe '#execute' do
let(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
subject { described_class.new(project, user, scope).execute }
context 'when scope is all' do
let(:scope) { :all }
it { is_expected.to match_array([enabled_cluster, disabled_cluster]) }
end
context 'when scope is active' do
let(:scope) { :active }
it { is_expected.to match_array([enabled_cluster]) }
end
context 'when scope is inactive' do
let(:scope) { :inactive }
it { is_expected.to match_array([disabled_cluster]) }
end
end
end
...@@ -28,7 +28,7 @@ describe('Clusters', () => { ...@@ -28,7 +28,7 @@ describe('Clusters', () => {
expect( expect(
cluster.toggleButton.classList, cluster.toggleButton.classList,
).not.toContain('checked'); ).not.toContain('is-checked');
expect( expect(
cluster.toggleInput.getAttribute('value'), cluster.toggleInput.getAttribute('value'),
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import setClusterTableToggles from '~/clusters/clusters_index';
import { setTimeout } from 'core-js/library/web/timers';
describe('Clusters table', () => {
preloadFixtures('clusters/index_cluster.html.raw');
let mock;
beforeEach(() => {
loadFixtures('clusters/index_cluster.html.raw');
mock = new MockAdapter(axios);
setClusterTableToggles();
});
describe('update cluster', () => {
it('renders loading state while request is made', () => {
const button = document.querySelector('.js-toggle-cluster-list');
button.click();
expect(button.classList).toContain('is-loading');
expect(button.getAttribute('disabled')).toEqual('true');
});
afterEach(() => {
mock.restore();
});
it('shows updated state after sucessfull request', (done) => {
mock.onPut().reply(200, {}, {});
const button = document.querySelector('.js-toggle-cluster-list');
button.click();
expect(button.classList).toContain('is-loading');
setTimeout(() => {
expect(button.classList).not.toContain('is-loading');
expect(button.classList).not.toContain('is-checked');
done();
}, 0);
});
it('shows inital state after failed request', (done) => {
mock.onPut().reply(500, {}, {});
const button = document.querySelector('.js-toggle-cluster-list');
button.click();
expect(button.classList).toContain('is-loading');
setTimeout(() => {
expect(button.classList).not.toContain('is-loading');
expect(button.classList).toContain('is-checked');
done();
}, 0);
});
});
});
...@@ -31,4 +31,19 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle ...@@ -31,4 +31,19 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
expect(response).to be_success expect(response).to be_success
store_frontend_fixture(response, example.description) store_frontend_fixture(response, example.description)
end end
context 'rendering non-empty state' do
before do
cluster
end
it 'clusters/index_cluster.html.raw' do |example|
get :index,
namespace_id: namespace,
project_id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
end end
import Vue from 'vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Toggle Button', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(toggleButton);
});
afterEach(() => {
vm.$destroy();
});
describe('render output', () => {
beforeEach(() => {
vm = mountComponent(Component, {
value: true,
name: 'foo',
});
});
it('renders input with provided name', () => {
expect(vm.$el.querySelector('input').getAttribute('name')).toEqual('foo');
});
it('renders input with provided value', () => {
expect(vm.$el.querySelector('input').getAttribute('value')).toEqual('true');
});
it('renders Enabled and Disabled text data attributes', () => {
expect(vm.$el.querySelector('button').getAttribute('data-enabled-text')).toEqual('Enabled');
expect(vm.$el.querySelector('button').getAttribute('data-disabled-text')).toEqual('Disabled');
});
});
describe('is-checked', () => {
beforeEach(() => {
vm = mountComponent(Component, {
value: true,
});
spyOn(vm, '$emit');
});
it('renders is checked class', () => {
expect(vm.$el.querySelector('button').classList.contains('is-checked')).toEqual(true);
});
it('emits change event when clicked', () => {
vm.$el.querySelector('button').click();
expect(vm.$emit).toHaveBeenCalledWith('change', false);
});
});
describe('is-disabled', () => {
beforeEach(() => {
vm = mountComponent(Component, {
value: true,
disabledInput: true,
});
spyOn(vm, '$emit');
});
it('renders disabled button', () => {
expect(vm.$el.querySelector('button').classList.contains('is-disabled')).toEqual(true);
});
it('does not emit change event when clicked', () => {
vm.$el.querySelector('button').click();
expect(vm.$emit).not.toHaveBeenCalled();
});
});
describe('is-loading', () => {
beforeEach(() => {
vm = mountComponent(Component, {
value: true,
isLoading: true,
});
});
it('renders loading class', () => {
expect(vm.$el.querySelector('button').classList.contains('is-loading')).toEqual(true);
});
});
});
...@@ -198,4 +198,26 @@ describe Clusters::Cluster do ...@@ -198,4 +198,26 @@ describe Clusters::Cluster do
end end
end end
end end
describe '#created?' do
let(:cluster) { create(:cluster, :provided_by_gcp) }
subject { cluster.created? }
context 'when status_name is :created' do
before do
allow(cluster).to receive_message_chain(:provider, :status_name).and_return(:created)
end
it { is_expected.to eq(true) }
end
context 'when status_name is not :created' do
before do
allow(cluster).to receive_message_chain(:provider, :status_name).and_return(:creating)
end
it { is_expected.to eq(false) }
end
end
end end
...@@ -79,7 +79,7 @@ describe Project do ...@@ -79,7 +79,7 @@ describe Project do
it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:pipeline_schedules) } it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_one(:cluster) } it { is_expected.to have_many(:clusters) }
it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') } it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
context 'after initialized' do context 'after initialized' do
......
...@@ -31,4 +31,44 @@ describe Clusters::ClusterPresenter do ...@@ -31,4 +31,44 @@ describe Clusters::ClusterPresenter do
it { is_expected.to include(cluster.provider.zone) } it { is_expected.to include(cluster.provider.zone) }
it { is_expected.to include(cluster.name) } it { is_expected.to include(cluster.name) }
end end
describe '#can_toggle_cluster' do
let(:user) { create(:user) }
before do
allow(cluster).to receive(:current_user).and_return(user)
end
subject { described_class.new(cluster).can_toggle_cluster? }
context 'when user can update' do
before do
allow_any_instance_of(described_class).to receive(:can?).with(user, :update_cluster, cluster).and_return(true)
end
context 'when cluster is created' do
before do
allow(cluster).to receive(:created?).and_return(true)
end
it { is_expected.to eq(true) }
end
context 'when cluster is not created' do
before do
allow(cluster).to receive(:created?).and_return(false)
end
it { is_expected.to eq(false) }
end
end
context 'when user can not update' do
before do
allow_any_instance_of(described_class).to receive(:can?).with(user, :update_cluster, cluster).and_return(false)
end
it { is_expected.to eq(false) }
end
end
end end
...@@ -4,10 +4,11 @@ describe Clusters::CreateService do ...@@ -4,10 +4,11 @@ describe Clusters::CreateService do
let(:access_token) { 'xxx' } let(:access_token) { 'xxx' }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:result) { described_class.new(project, user, params).execute(access_token) }
subject { described_class.new(project, user, params).execute(access_token) }
context 'when provider is gcp' do context 'when provider is gcp' do
context 'when correct params' do shared_context 'valid params' do
let(:params) do let(:params) do
{ {
name: 'test-cluster', name: 'test-cluster',
...@@ -20,27 +21,9 @@ describe Clusters::CreateService do ...@@ -20,27 +21,9 @@ describe Clusters::CreateService do
} }
} }
end end
it 'creates a cluster object and performs a worker' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { result }
.to change { Clusters::Cluster.count }.by(1)
.and change { Clusters::Providers::Gcp.count }.by(1)
expect(result.name).to eq('test-cluster')
expect(result.user).to eq(user)
expect(result.project).to eq(project)
expect(result.provider.gcp_project_id).to eq('gcp-project')
expect(result.provider.zone).to eq('us-central1-a')
expect(result.provider.num_nodes).to eq(1)
expect(result.provider.machine_type).to eq('machine_type-a')
expect(result.provider.access_token).to eq(access_token)
expect(result.platform).to be_nil
end
end end
context 'when invalid params' do shared_context 'invalid params' do
let(:params) do let(:params) do
{ {
name: 'test-cluster', name: 'test-cluster',
...@@ -53,11 +36,86 @@ describe Clusters::CreateService do ...@@ -53,11 +36,86 @@ describe Clusters::CreateService do
} }
} }
end end
end
shared_examples 'create cluster' do
it 'creates a cluster object and performs a worker' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { subject }
.to change { Clusters::Cluster.count }.by(1)
.and change { Clusters::Providers::Gcp.count }.by(1)
expect(subject.name).to eq('test-cluster')
expect(subject.user).to eq(user)
expect(subject.project).to eq(project)
expect(subject.provider.gcp_project_id).to eq('gcp-project')
expect(subject.provider.zone).to eq('us-central1-a')
expect(subject.provider.num_nodes).to eq(1)
expect(subject.provider.machine_type).to eq('machine_type-a')
expect(subject.provider.access_token).to eq(access_token)
expect(subject.platform).to be_nil
end
end
shared_examples 'error' do
it 'returns an error' do it 'returns an error' do
expect(ClusterProvisionWorker).not_to receive(:perform_async) expect(ClusterProvisionWorker).not_to receive(:perform_async)
expect { result }.to change { Clusters::Cluster.count }.by(0) expect { subject }.to change { Clusters::Cluster.count }.by(0)
expect(result.errors[:"provider_gcp.gcp_project_id"]).to be_present expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present
end
end
context 'when project has no clusters' do
context 'when correct params' do
include_context 'valid params'
include_examples 'create cluster'
end
context 'when invalid params' do
include_context 'invalid params'
include_examples 'error'
end
end
context 'when project has a cluster' do
let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
before do
allow(project).to receive(:feature_available?).and_call_original
end
context 'when license has multiple clusters feature' do
before do
allow(project).to receive(:feature_available?).with(:multiple_clusters).and_return(true)
end
context 'when correct params' do
include_context 'valid params'
include_examples 'create cluster'
end
context 'when invalid params' do
include_context 'invalid params'
include_examples 'error'
end
end
context 'when license does not have multiple clusters feature' do
include_context 'valid params'
before do
allow(project).to receive(:feature_available?).with(:multiple_clusters).and_return(false)
end
it 'does not create a cluster' do
expect(ClusterProvisionWorker).not_to receive(:perform_async)
expect { subject }.to raise_error(ArgumentError).and change { Clusters::Cluster.count }.by(0)
end
end end
end end
end end
......
...@@ -3,210 +3,253 @@ require 'spec_helper' ...@@ -3,210 +3,253 @@ require 'spec_helper'
describe Projects::ForkService do describe Projects::ForkService do
include ProjectForksHelper include ProjectForksHelper
let(:gitlab_shell) { Gitlab::Shell.new } let(:gitlab_shell) { Gitlab::Shell.new }
context 'when forking a new project' do
describe 'fork by user' do
before do
@from_user = create(:user)
@from_namespace = @from_user.namespace
avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
@from_project = create(:project,
:repository,
creator_id: @from_user.id,
namespace: @from_namespace,
star_count: 107,
avatar: avatar,
description: 'wow such project')
@to_user = create(:user)
@to_namespace = @to_user.namespace
@from_project.add_user(@to_user, :developer)
end
describe 'fork by user' do context 'fork project' do
before do context 'when forker is a guest' do
@from_user = create(:user) before do
@from_namespace = @from_user.namespace @guest = create(:user)
avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") @from_project.add_user(@guest, :guest)
@from_project = create(:project, end
:repository, subject { fork_project(@from_project, @guest) }
creator_id: @from_user.id,
namespace: @from_namespace,
star_count: 107,
avatar: avatar,
description: 'wow such project')
@to_user = create(:user)
@to_namespace = @to_user.namespace
@from_project.add_user(@to_user, :developer)
end
context 'fork project' do it { is_expected.not_to be_persisted }
context 'when forker is a guest' do it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
before do
@guest = create(:user)
@from_project.add_user(@guest, :guest)
end end
subject { fork_project(@from_project, @guest) }
it { is_expected.not_to be_persisted } describe "successfully creates project in the user namespace" do
it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) } let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) }
end
describe "successfully creates project in the user namespace" do it { expect(to_project).to be_persisted }
let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) } it { expect(to_project.errors).to be_empty }
it { expect(to_project.owner).to eq(@to_user) }
it { expect(to_project).to be_persisted } it { expect(to_project.namespace).to eq(@to_user.namespace) }
it { expect(to_project.errors).to be_empty } it { expect(to_project.star_count).to be_zero }
it { expect(to_project.owner).to eq(@to_user) } it { expect(to_project.description).to eq(@from_project.description) }
it { expect(to_project.namespace).to eq(@to_user.namespace) } it { expect(to_project.avatar.file).to be_exists }
it { expect(to_project.star_count).to be_zero }
it { expect(to_project.description).to eq(@from_project.description) }
it { expect(to_project.avatar.file).to be_exists }
# This test is here because we had a bug where the from-project lost its
# avatar after being forked.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
it "after forking the from-project still has its avatar" do
# If we do not fork the project first we cannot detect the bug.
expect(to_project).to be_persisted
expect(@from_project.avatar.file).to be_exists
end
it 'flushes the forks count cache of the source project' do # This test is here because we had a bug where the from-project lost its
expect(@from_project.forks_count).to be_zero # avatar after being forked.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
it "after forking the from-project still has its avatar" do
# If we do not fork the project first we cannot detect the bug.
expect(to_project).to be_persisted
fork_project(@from_project, @to_user) expect(@from_project.avatar.file).to be_exists
end
expect(@from_project.forks_count).to eq(1) it 'flushes the forks count cache of the source project' do
end expect(@from_project.forks_count).to be_zero
it 'creates a fork network with the new project and the root project set' do fork_project(@from_project, @to_user)
to_project
fork_network = @from_project.reload.fork_network
expect(fork_network).not_to be_nil expect(@from_project.forks_count).to eq(1)
expect(fork_network.root_project).to eq(@from_project) end
expect(fork_network.projects).to contain_exactly(@from_project, to_project)
end
end
context 'creating a fork of a fork' do it 'creates a fork network with the new project and the root project set' do
let(:from_forked_project) { fork_project(@from_project, @to_user) } to_project
let(:other_namespace) do fork_network = @from_project.reload.fork_network
group = create(:group)
group.add_owner(@to_user)
group
end
let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
it 'sets the root of the network to the root project' do expect(fork_network).not_to be_nil
expect(to_project.fork_network.root_project).to eq(@from_project) expect(fork_network.root_project).to eq(@from_project)
expect(fork_network.projects).to contain_exactly(@from_project, to_project)
end
end end
it 'sets the forked_from_project on the membership' do context 'creating a fork of a fork' do
expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project) let(:from_forked_project) { fork_project(@from_project, @to_user) }
let(:other_namespace) do
group = create(:group)
group.add_owner(@to_user)
group
end
let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
it 'sets the root of the network to the root project' do
expect(to_project.fork_network.root_project).to eq(@from_project)
end
it 'sets the forked_from_project on the membership' do
expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project)
end
end end
end end
end
context 'project already exists' do context 'project already exists' do
it "fails due to validation, not transaction failure" do it "fails due to validation, not transaction failure" do
@existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user, namespace: @to_namespace) @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace)
expect(@existing_project).to be_persisted expect(@existing_project).to be_persisted
expect(@to_project).not_to be_persisted expect(@to_project).not_to be_persisted
expect(@to_project.errors[:name]).to eq(['has already been taken']) expect(@to_project.errors[:name]).to eq(['has already been taken'])
expect(@to_project.errors[:path]).to eq(['has already been taken']) expect(@to_project.errors[:path]).to eq(['has already been taken'])
end
end end
end
context 'repository already exists' do context 'repository already exists' do
let(:repository_storage) { 'default' } let(:repository_storage) { 'default' }
let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
before do before do
gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}") gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
end end
after do after do
gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}") gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}")
end end
it 'does not allow creation' do it 'does not allow creation' do
to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace) to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace)
expect(to_project).not_to be_persisted expect(to_project).not_to be_persisted
expect(to_project.errors.messages).to have_key(:base) expect(to_project.errors.messages).to have_key(:base)
expect(to_project.errors.messages[:base].first).to match('There is already a repository with that name on disk') expect(to_project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
end
end end
end
context 'GitLab CI is enabled' do context 'GitLab CI is enabled' do
it "forks and enables CI for fork" do it "forks and enables CI for fork" do
@from_project.enable_ci @from_project.enable_ci
@to_project = fork_project(@from_project, @to_user) @to_project = fork_project(@from_project, @to_user)
expect(@to_project.builds_enabled?).to be_truthy expect(@to_project.builds_enabled?).to be_truthy
end
end end
end
context "when project has restricted visibility level" do context "when project has restricted visibility level" do
context "and only one visibility level is restricted" do context "and only one visibility level is restricted" do
before do before do
@from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL) @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
end
it "creates fork with highest allowed level" do
forked_project = fork_project(@from_project, @to_user)
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
end end
it "creates fork with highest allowed level" do context "and all visibility levels are restricted" do
forked_project = fork_project(@from_project, @to_user) before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE])
end
it "creates fork with private visibility levels" do
forked_project = fork_project(@from_project, @to_user)
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end end
end end
end
context "and all visibility levels are restricted" do describe 'fork to namespace' do
before do before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE]) @group_owner = create(:user)
@developer = create(:user)
@project = create(:project, :repository,
creator_id: @group_owner.id,
star_count: 777,
description: 'Wow, such a cool project!')
@group = create(:group)
@group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER)
@project.add_user(@developer, :developer)
@project.add_user(@group_owner, :developer)
@opts = { namespace: @group }
end
context 'fork project for group' do
it 'group owner successfully forks project into the group' do
to_project = fork_project(@project, @group_owner, @opts)
expect(to_project).to be_persisted
expect(to_project.errors).to be_empty
expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
expect(to_project.path).to eq(@project.path)
expect(to_project.description).to eq(@project.description)
expect(to_project.star_count).to be_zero
end end
end
it "creates fork with private visibility levels" do context 'fork project for group when user not owner' do
forked_project = fork_project(@from_project, @to_user) it 'group developer fails to fork project into the group' do
to_project = fork_project(@project, @developer, @opts)
expect(to_project.errors[:namespace]).to eq(['is not valid'])
end
end
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) context 'project already exists in group' do
it 'fails due to validation, not transaction failure' do
existing_project = create(:project, :repository,
name: @project.name,
namespace: @group)
to_project = fork_project(@project, @group_owner, @opts)
expect(existing_project.persisted?).to be_truthy
expect(to_project.errors[:name]).to eq(['has already been taken'])
expect(to_project.errors[:path]).to eq(['has already been taken'])
end end
end end
end end
end end
describe 'fork to namespace' do context 'when linking fork to an existing project' do
before do let(:fork_from_project) { create(:project, :public) }
@group_owner = create(:user) let(:fork_to_project) { create(:project, :public) }
@developer = create(:user) let(:user) { create(:user) }
@project = create(:project, :repository,
creator_id: @group_owner.id, subject { described_class.new(fork_from_project, user) }
star_count: 777,
description: 'Wow, such a cool project!') def forked_from_project(project)
@group = create(:group) project.fork_network_member&.forked_from_project
@group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER)
@project.add_user(@developer, :developer)
@project.add_user(@group_owner, :developer)
@opts = { namespace: @group }
end end
context 'fork project for group' do context 'if project is already forked' do
it 'group owner successfully forks project into the group' do it 'does not create fork relation' do
to_project = fork_project(@project, @group_owner, @opts) allow(fork_to_project).to receive(:forked?).and_return(true)
expect(forked_from_project(fork_to_project)).to be_nil
expect(to_project).to be_persisted expect(subject.execute(fork_to_project)).to be_nil
expect(to_project.errors).to be_empty expect(forked_from_project(fork_to_project)).to be_nil
expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
expect(to_project.path).to eq(@project.path)
expect(to_project.description).to eq(@project.description)
expect(to_project.star_count).to be_zero
end end
end end
context 'fork project for group when user not owner' do context 'if project is not forked' do
it 'group developer fails to fork project into the group' do it 'creates fork relation' do
to_project = fork_project(@project, @developer, @opts) expect(fork_to_project.forked?).to be false
expect(to_project.errors[:namespace]).to eq(['is not valid']) expect(forked_from_project(fork_to_project)).to be_nil
subject.execute(fork_to_project)
expect(fork_to_project.forked?).to be true
expect(forked_from_project(fork_to_project)).to eq fork_from_project
expect(fork_to_project.forked_from_project).to eq fork_from_project
end end
end
context 'project already exists in group' do it 'flushes the forks count cache of the source project' do
it 'fails due to validation, not transaction failure' do expect(fork_from_project.forks_count).to be_zero
existing_project = create(:project, :repository,
name: @project.name, subject.execute(fork_to_project)
namespace: @group)
to_project = fork_project(@project, @group_owner, @opts) expect(fork_from_project.forks_count).to eq(1)
expect(existing_project.persisted?).to be_truthy
expect(to_project.errors[:name]).to eq(['has already been taken'])
expect(to_project.errors[:path]).to eq(['has already been taken'])
end end
end end
end end
......
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