summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js13
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js24
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue13
-rw-r--r--app/assets/javascripts/clusters/constants.js7
-rw-r--r--app/assets/javascripts/pages/groups/clusters/destroy/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/show/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/update/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/clusters/index/index.js4
-rw-r--r--app/assets/stylesheets/pages/clusters.scss35
-rw-r--r--app/controllers/clusters/clusters_controller.rb4
-rw-r--r--app/controllers/groups/clusters/applications_controller.rb18
-rw-r--r--app/controllers/groups/clusters_controller.rb25
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/models/clusters/applications/knative.rb2
-rw-r--r--app/models/clusters/cluster.rb6
-rw-r--r--app/models/clusters/platforms/kubernetes.rb12
-rw-r--r--app/models/wiki_page.rb6
-rw-r--r--app/policies/clusters/cluster_policy.rb6
-rw-r--r--app/policies/group_policy.rb4
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/presenters/clusterable_presenter.rb12
-rw-r--r--app/presenters/clusters/cluster_presenter.rb2
-rw-r--r--app/presenters/group_clusterable_presenter.rb36
-rw-r--r--app/presenters/project_clusterable_presenter.rb16
-rw-r--r--app/services/clusters/applications/create_service.rb11
-rw-r--r--app/services/clusters/create_service.rb4
-rw-r--r--app/services/commits/change_service.rb8
-rw-r--r--app/views/clusters/clusters/_buttons.html.haml4
-rw-r--r--app/views/clusters/clusters/_cluster.html.haml40
-rw-r--r--app/views/clusters/clusters/_empty_state.html.haml6
-rw-r--r--app/views/clusters/clusters/_sidebar.html.haml9
-rw-r--r--app/views/clusters/clusters/gcp/_show.html.haml7
-rw-r--r--app/views/clusters/clusters/index.html.haml7
-rw-r--r--app/views/clusters/clusters/show.html.haml1
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml7
-rw-r--r--app/views/clusters/clusters/user/_show.html.haml7
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml13
-rw-r--r--changelogs/unreleased/34758-group-cluster-controller.yml5
-rw-r--r--changelogs/unreleased/rs-revert-api.yml5
-rw-r--r--config/routes/group.rb2
-rw-r--r--doc/api/commits.md42
-rw-r--r--doc/development/testing_guide/review_apps.md92
-rw-r--r--lib/api/commits.rb34
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/init_command.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/install_command.rb6
-rw-r--r--lib/gitlab/kubernetes/helm/upgrade_command.rb4
-rw-r--r--locale/gitlab.pot30
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb49
-rw-r--r--spec/controllers/groups/clusters/applications_controller_spec.rb87
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb574
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb152
-rw-r--r--spec/factories/clusters/platforms/kubernetes.rb2
-rw-r--r--spec/features/projects/clusters_spec.rb31
-rw-r--r--spec/lib/gitlab/kubernetes/helm/init_command_spec.rb4
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb41
-rw-r--r--spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb16
-rw-r--r--spec/models/clusters/cluster_spec.rb22
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb12
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb42
-rw-r--r--spec/policies/group_policy_spec.rb6
-rw-r--r--spec/policies/project_policy_spec.rb4
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb7
-rw-r--r--spec/presenters/group_clusterable_presenter_spec.rb77
-rw-r--r--spec/requests/api/commits_spec.rb112
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb42
-rw-r--r--spec/services/clusters/update_service_spec.rb27
-rw-r--r--spec/support/shared_examples/helm_generated_script.rb4
70 files changed, 1615 insertions, 331 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 02dfe1c7d6f..71fc2ac7d80 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -9,7 +9,7 @@ import eventHub from './event_hub';
import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
-import applications from './components/applications.vue';
+import Applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
/**
@@ -31,6 +31,7 @@ export default class Clusters {
installKnativePath,
installPrometheusPath,
managePrometheusPath,
+ clusterType,
clusterStatus,
clusterStatusReason,
helpPath,
@@ -67,7 +68,7 @@ export default class Clusters {
initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
- this.initApplications();
+ this.initApplications(clusterType);
if (this.store.state.status !== 'created') {
this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
@@ -79,23 +80,21 @@ export default class Clusters {
}
}
- initApplications() {
+ initApplications(type) {
const { store } = this;
const el = document.querySelector('#js-cluster-applications');
this.applications = new Vue({
el,
- components: {
- applications,
- },
data() {
return {
state: store.state,
};
},
render(createElement) {
- return createElement('applications', {
+ return createElement(Applications, {
props: {
+ type,
applications: this.state.applications,
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
deleted file mode 100644
index 789c8360124..00000000000
--- a/app/assets/javascripts/clusters/clusters_index.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-import setupToggleButtons from '~/toggle_buttons';
-import initDismissableCallout from '~/dismissable_callout';
-
-import ClustersService from './services/clusters_service';
-
-export default () => {
- const clusterList = document.querySelector('.js-clusters-list');
-
- initDismissableCallout('.gcp-signup-offer');
-
- // The empty state won't have a clusterList
- if (clusterList) {
- setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
- ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
- err => {
- createFlash(__('Something went wrong on our end.'));
- throw err;
- },
- ),
- );
- }
-};
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index c7ffb470d4d..c1026d1273a 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -13,7 +13,7 @@ import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import { APPLICATION_STATUS, INGRESS } from '../constants';
+import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
export default {
components: {
@@ -21,6 +21,11 @@ export default {
clipboardButton,
},
props: {
+ type: {
+ type: String,
+ required: false,
+ default: CLUSTER_TYPE.PROJECT,
+ },
applications: {
type: Object,
required: false,
@@ -59,6 +64,9 @@ export default {
prometheusLogo,
}),
computed: {
+ isProjectCluster() {
+ return this.type === CLUSTER_TYPE.PROJECT;
+ },
helmInstalled() {
return (
this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
@@ -281,6 +289,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="prometheus"
:logo-url="prometheusLogo"
:title="applications.prometheus.title"
@@ -299,6 +308,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="runner"
:logo-url="gitlabLogo"
:title="applications.runner.title"
@@ -317,6 +327,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="jupyter"
:logo-url="jupyterhubLogo"
:title="applications.jupyter.title"
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index d707420c845..15cf4a56138 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -1,3 +1,10 @@
+// These need to match the enum found in app/models/clusters/cluster.rb
+export const CLUSTER_TYPE = {
+ INSTANCE: 'instance_type',
+ GROUP: 'group_type',
+ PROJECT: 'project_type',
+};
+
// These need to match what is returned from the server
export const APPLICATION_STATUS = {
NOT_INSTALLABLE: 'not_installable',
diff --git a/app/assets/javascripts/pages/groups/clusters/destroy/index.js b/app/assets/javascripts/pages/groups/clusters/destroy/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/destroy/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/index/index.js b/app/assets/javascripts/pages/groups/clusters/index/index.js
new file mode 100644
index 00000000000..845a5f7042c
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/index/index.js
@@ -0,0 +1,5 @@
+import initDismissableCallout from '~/dismissable_callout';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initDismissableCallout('.gcp-signup-offer');
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/show/index.js b/app/assets/javascripts/pages/groups/clusters/show/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/show/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/update/index.js b/app/assets/javascripts/pages/groups/clusters/update/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/update/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/index.js b/app/assets/javascripts/pages/groups/index.js
new file mode 100644
index 00000000000..bf80d8b8193
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/index.js
@@ -0,0 +1,16 @@
+import initDismissableCallout from '~/dismissable_callout';
+import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const { page } = document.body.dataset;
+ const newClusterViews = [
+ 'groups:clusters:new',
+ 'groups:clusters:create_gcp',
+ 'groups:clusters:create_user',
+ ];
+
+ if (newClusterViews.indexOf(page) > -1) {
+ initDismissableCallout('.gcp-signup-offer');
+ initGkeDropdowns();
+ }
+});
diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js
index e4b8baede58..845a5f7042c 100644
--- a/app/assets/javascripts/pages/projects/clusters/index/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/index/index.js
@@ -1,5 +1,5 @@
-import ClustersIndex from '~/clusters/clusters_index';
+import initDismissableCallout from '~/dismissable_callout';
document.addEventListener('DOMContentLoaded', () => {
- new ClustersIndex(); // eslint-disable-line no-new
+ initDismissableCallout('.gcp-signup-offer');
});
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 71a3fd544f2..ad12cd101b6 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -25,6 +25,12 @@
.cluster-application-row {
border-bottom: 1px solid $border-color;
padding: $gl-padding;
+
+ &:last-child {
+ border-bottom: 0;
+ border-bottom-left-radius: calc(#{$border-radius-default} - 1px);
+ border-bottom-right-radius: calc(#{$border-radius-default} - 1px);
+ }
}
}
@@ -73,6 +79,10 @@
padding: $gl-padding-top $gl-padding;
}
+ .card {
+ margin-bottom: $gl-vert-padding;
+ }
+
.empty-state .svg-content img {
width: 145px;
}
@@ -80,6 +90,31 @@
.top-area .nav-controls > .btn.btn-add-cluster {
margin-right: 0;
}
+
+ .clusters-table {
+ background-color: $gray-light;
+ padding: $gl-padding-8;
+ }
+
+ .badge-light {
+ background-color: $white-normal;
+ }
+
+ .gl-responsive-table-row {
+ padding: $gl-padding;
+ border: 0;
+
+ &.table-row-header {
+ background-color: none;
+ border: 0;
+ font-weight: bold;
+ color: $gl-gray-500;
+ }
+ }
+}
+
+.cluster-warning {
+ @include alert-variant(theme-color-level('warning', $alert-bg-level), theme-color-level('warning', $alert-border-level), theme-color-level('warning', $alert-color-level));
}
.gcp-signup-offer {
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index f6f2060ebb5..2e9c77ae55c 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -183,13 +183,13 @@ class Clusters::ClustersController < Clusters::BaseController
def gcp_cluster
@gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
- end
+ end.present(current_user: current_user)
end
def user_cluster
@user_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_platform_kubernetes
- end
+ end.present(current_user: current_user)
end
def validate_gcp_token
diff --git a/app/controllers/groups/clusters/applications_controller.rb b/app/controllers/groups/clusters/applications_controller.rb
new file mode 100644
index 00000000000..8dd8a01cf40
--- /dev/null
+++ b/app/controllers/groups/clusters/applications_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Groups::Clusters::ApplicationsController < Clusters::ApplicationsController
+ include ControllerWithCrossProjectAccessCheck
+
+ prepend_before_action :group
+ requires_cross_project_access
+
+ private
+
+ def clusterable
+ @clusterable ||= ClusterablePresenter.fabricate(group, current_user: current_user)
+ end
+
+ def group
+ @group ||= find_routable!(Group, params[:group_id] || params[:id])
+ end
+end
diff --git a/app/controllers/groups/clusters_controller.rb b/app/controllers/groups/clusters_controller.rb
new file mode 100644
index 00000000000..50c44b7a58b
--- /dev/null
+++ b/app/controllers/groups/clusters_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Groups::ClustersController < Clusters::ClustersController
+ include ControllerWithCrossProjectAccessCheck
+
+ prepend_before_action :check_group_clusters_feature_flag!
+ prepend_before_action :group
+ requires_cross_project_access
+
+ layout 'group'
+
+ private
+
+ def clusterable
+ @clusterable ||= ClusterablePresenter.fabricate(group, current_user: current_user)
+ end
+
+ def group
+ @group ||= find_routable!(Group, params[:group_id] || params[:id])
+ end
+
+ def check_group_clusters_feature_flag!
+ render_404 unless Feature.enabled?(:group_clusters)
+ end
+end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 0c313e9e6d3..e9b9b9b7721 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -140,6 +140,10 @@ module GroupsHelper
can?(current_user, "read_group_#{resource}".to_sym, @group)
end
+ if can?(current_user, :read_cluster, @group) && Feature.enabled?(:group_clusters)
+ links << :kubernetes
+ end
+
if can?(current_user, :admin_group, @group)
links << :settings
end
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 8adb99fcb04..a79a97576d1 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -44,7 +44,7 @@ module Clusters
private
def install_script
- ["/usr/bin/kubectl apply -f #{ISTIO_CRDS} >/dev/null"]
+ ["/usr/bin/kubectl apply -f #{ISTIO_CRDS}"]
end
end
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 48d6c0daa0f..0ba056e57d4 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -29,7 +29,7 @@ module Clusters
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
- has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true
+ has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true
has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
@@ -144,6 +144,10 @@ module Clusters
)
end
+ def allow_user_defined_namespace?
+ project_type?
+ end
+
private
def restrict_modification
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index d69038be532..ea02ae6c9d8 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -38,6 +38,8 @@ module Clusters
validates :namespace, exclusion: { in: RESERVED_NAMESPACES }
+ validate :no_namespace, unless: :allow_user_defined_namespace?
+
# We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
validates :api_url, url: true, presence: true
validates :token, presence: true
@@ -52,6 +54,7 @@ module Clusters
delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, to: :cluster, allow_nil: true
delegate :managed?, to: :cluster, allow_nil: true
+ delegate :allow_user_defined_namespace?, to: :cluster, allow_nil: true
delegate :kubernetes_namespace, to: :cluster
alias_method :active?, :enabled?
@@ -150,7 +153,8 @@ module Clusters
end
def build_kube_client!
- raise "Incomplete settings" unless api_url && actual_namespace
+ raise "Incomplete settings" unless api_url
+ raise "No namespace" if cluster.project_type? && actual_namespace.empty? # can probably remove this line once we remove #actual_namespace
unless (username && password) || token
raise "Either username/password or token is required to access API"
@@ -207,6 +211,12 @@ module Clusters
self.token = self.token&.strip
end
+ def no_namespace
+ if namespace
+ errors.add(:namespace, 'only allowed for project cluster')
+ end
+ end
+
def prevent_modification
return unless managed?
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index ae7fb5f962a..7769c3d71c0 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -151,12 +151,6 @@ class WikiPage
last_version&.sha
end
- # Returns the Date that this latest version was
- # created on.
- def created_at
- @page.version.date
- end
-
# Returns boolean True or False if this instance
# is an old version of the page.
def historical?
diff --git a/app/policies/clusters/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb
index 147943a3d6c..d6d590687e2 100644
--- a/app/policies/clusters/cluster_policy.rb
+++ b/app/policies/clusters/cluster_policy.rb
@@ -4,11 +4,7 @@ module Clusters
class ClusterPolicy < BasePolicy
alias_method :cluster, :subject
+ delegate { cluster.group }
delegate { cluster.project }
-
- rule { can?(:maintainer_access) }.policy do
- enable :update_cluster
- enable :admin_cluster
- end
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 73c93b22c95..6b4e56ef5e4 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -65,6 +65,10 @@ class GroupPolicy < BasePolicy
enable :create_projects
enable :admin_pipeline
enable :admin_build
+ enable :read_cluster
+ enable :create_cluster
+ enable :update_cluster
+ enable :admin_cluster
end
rule { owner }.policy do
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a76a083bceb..1c082945299 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -258,6 +258,8 @@ class ProjectPolicy < BasePolicy
enable :update_pages
enable :read_cluster
enable :create_cluster
+ enable :update_cluster
+ enable :admin_cluster
enable :create_environment_terminal
end
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index cff0e74d6ea..9cc137aa3bd 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -43,4 +43,16 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
def cluster_path(cluster, params = {})
raise NotImplementedError
end
+
+ def empty_state_help_text
+ nil
+ end
+
+ def sidebar_text
+ raise NotImplementedError
+ end
+
+ def learn_more_link
+ raise NotImplementedError
+ end
end
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index 78d632eb77c..7e6eccb648c 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -15,6 +15,8 @@ module Clusters
def show_path
if cluster.project_type?
project_cluster_path(project, cluster)
+ elsif cluster.group_type?
+ group_cluster_path(group, cluster)
else
raise NotImplementedError
end
diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb
new file mode 100644
index 00000000000..d963c188559
--- /dev/null
+++ b/app/presenters/group_clusterable_presenter.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class GroupClusterablePresenter < ClusterablePresenter
+ extend ::Gitlab::Utils::Override
+ include ActionView::Helpers::UrlHelper
+
+ override :cluster_status_cluster_path
+ def cluster_status_cluster_path(cluster, params = {})
+ cluster_status_group_cluster_path(clusterable, cluster, params)
+ end
+
+ override :install_applications_cluster_path
+ def install_applications_cluster_path(cluster, application)
+ install_applications_group_cluster_path(clusterable, cluster, application)
+ end
+
+ override :cluster_path
+ def cluster_path(cluster, params = {})
+ group_cluster_path(clusterable, cluster, params)
+ end
+
+ override :empty_state_help_text
+ def empty_state_help_text
+ s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.')
+ end
+
+ override :sidebar_text
+ def sidebar_text
+ s_('ClusterIntegration|Adding a Kubernetes cluster to your group will automatically share the cluster across all your projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster.')
+ end
+
+ override :learn_more_link
+ def learn_more_link
+ link_to(s_('ClusterIntegration|Learn more about group Kubernetes clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ end
+end
diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb
index 12077b2e735..63e69b91b11 100644
--- a/app/presenters/project_clusterable_presenter.rb
+++ b/app/presenters/project_clusterable_presenter.rb
@@ -1,15 +1,31 @@
# frozen_string_literal: true
class ProjectClusterablePresenter < ClusterablePresenter
+ extend ::Gitlab::Utils::Override
+ include ActionView::Helpers::UrlHelper
+
+ override :cluster_status_cluster_path
def cluster_status_cluster_path(cluster, params = {})
cluster_status_project_cluster_path(clusterable, cluster, params)
end
+ override :install_applications_cluster_path
def install_applications_cluster_path(cluster, application)
install_applications_project_cluster_path(clusterable, cluster, application)
end
+ override :cluster_path
def cluster_path(cluster, params = {})
project_cluster_path(clusterable, cluster, params)
end
+
+ override :sidebar_text
+ def sidebar_text
+ s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
+ end
+
+ override :learn_more_link
+ def learn_more_link
+ link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ end
end
diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb
index c348cad4803..844807c2581 100644
--- a/app/services/clusters/applications/create_service.rb
+++ b/app/services/clusters/applications/create_service.rb
@@ -42,7 +42,16 @@ module Clusters
def builders
{
"helm" => -> (cluster) { cluster.application_helm || cluster.build_application_helm },
- "ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress },
+ "ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress }
+ }.tap do |hash|
+ hash.merge!(project_builders) if cluster.project_type?
+ end
+ end
+
+ # These applications will need extra configuration to enable them to work
+ # with groups of projects
+ def project_builders
+ {
"prometheus" => -> (cluster) { cluster.application_prometheus || cluster.build_application_prometheus },
"runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner },
"jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter },
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
index 270db4a52fd..5a9da053780 100644
--- a/app/services/clusters/create_service.rb
+++ b/app/services/clusters/create_service.rb
@@ -36,6 +36,10 @@ module Clusters
case clusterable
when ::Project
{ cluster_type: :project_type, projects: [clusterable] }
+ when ::Group
+ { cluster_type: :group_type, groups: [clusterable] }
+ else
+ raise NotImplementedError
end
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 2fbd442fc2e..fbf71f02837 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -24,8 +24,12 @@ module Commits
start_project: @start_project,
start_branch_name: @start_branch)
rescue Gitlab::Git::Repository::CreateTreeError
- error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
- This #{@commit.change_type_title(current_user)} may already have been #{action.to_s.dasherize}ed, or a more recent commit may have updated some of its content."
+ act = action.to_s.dasherize
+ type = @commit.change_type_title(current_user)
+
+ error_msg = "Sorry, we cannot #{act} this #{type} automatically. " \
+ "This #{type} may already have been #{act}ed, or a more recent " \
+ "commit may have updated some of its content."
raise ChangeError, error_msg
end
end
diff --git a/app/views/clusters/clusters/_buttons.html.haml b/app/views/clusters/clusters/_buttons.html.haml
new file mode 100644
index 00000000000..db2e247e341
--- /dev/null
+++ b/app/views/clusters/clusters/_buttons.html.haml
@@ -0,0 +1,4 @@
+-# This partial is overridden in EE
+.nav-controls
+ %span.btn.btn-add-cluster.disabled.js-add-cluster
+ = s_("ClusterIntegration|Add Kubernetes cluster")
diff --git a/app/views/clusters/clusters/_cluster.html.haml b/app/views/clusters/clusters/_cluster.html.haml
index facbcb7fc59..adeca013749 100644
--- a/app/views/clusters/clusters/_cluster.html.haml
+++ b/app/views/clusters/clusters/_cluster.html.haml
@@ -1,24 +1,16 @@
-.gl-responsive-table-row
- .table-section.section-30
- .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
- .table-mobile-content
- = link_to cluster.name, cluster.show_path
- .table-section.section-30
- .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
- .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.js-project-feature-toggle.project-feature-toggle{ type: "button",
- class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
- "aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"),
- disabled: !cluster.can_toggle_cluster?,
- data: { endpoint: clusterable.cluster_path(cluster, format: :json) } }
- %input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? }
- = icon("spinner spin", class: "loading-icon")
- %span.toggle-icon
- = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
- = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
+.card
+ .card-body.gl-responsive-table-row
+ .table-section.section-60
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
+ .table-mobile-content
+ = link_to cluster.name, cluster.show_path
+ - unless cluster.enabled?
+ %span.badge.badge-danger Connection disabled
+ .table-section.section-25
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
+ .table-mobile-content= cluster.environment_scope
+ .table-section.section-15.text-right
+ .table-mobile-header{ role: "rowheader" }
+ .table-mobile-content
+ %span.badge.badge-light
+ = cluster.project_type? ? s_("ClusterIntegration|Project cluster") : s_("ClusterIntegration|Group cluster")
diff --git a/app/views/clusters/clusters/_empty_state.html.haml b/app/views/clusters/clusters/_empty_state.html.haml
index 800e76d92ef..c926ec258f0 100644
--- a/app/views/clusters/clusters/_empty_state.html.haml
+++ b/app/views/clusters/clusters/_empty_state.html.haml
@@ -4,8 +4,10 @@
.col-12
.text-content
%h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation')
- - link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- %p= s_('ClusterIntegration|Kubernetes 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
+ = s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
+ = clusterable.empty_state_help_text
+ = clusterable.learn_more_link
- if clusterable.can_create_cluster?
.text-center
diff --git a/app/views/clusters/clusters/_sidebar.html.haml b/app/views/clusters/clusters/_sidebar.html.haml
index 3d10348212f..6e4415c21a9 100644
--- a/app/views/clusters/clusters/_sidebar.html.haml
+++ b/app/views/clusters/clusters/_sidebar.html.haml
@@ -1,9 +1,6 @@
-- clusters_help_url = help_page_path('user/project/clusters/index.md')
-- help_link_start = "<a href=\"%{url}\" target=\"_blank\" rel=\"noopener noreferrer\">".html_safe
-- help_link_end = '</a>'.html_safe
%h4.prepend-top-0
- = s_('ClusterIntegration|Kubernetes cluster integration')
+ = s_('ClusterIntegration|Add a Kubernetes cluster integration')
%p
- = s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
+ = clusterable.sidebar_text
%p
- = s_('ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: clusters_help_url }, help_link_end: help_link_end }
+ = clusterable.learn_more_link
diff --git a/app/views/clusters/clusters/gcp/_show.html.haml b/app/views/clusters/clusters/gcp/_show.html.haml
index ca55ccb8fdf..e9f05eaf453 100644
--- a/app/views/clusters/clusters/gcp/_show.html.haml
+++ b/app/views/clusters/clusters/gcp/_show.html.haml
@@ -33,9 +33,10 @@
= s_('ClusterIntegration|Show')
= clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ - if @cluster.allow_user_defined_namespace?
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
.form-check
diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index a55de84b5cd..ad6d1d856d6 100644
--- a/app/views/clusters/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -10,14 +10,13 @@
.top-area.adjust
.nav-text
= s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project")
- .ci-table.js-clusters-list
+ = render 'clusters/clusters/buttons'
+ .clusters-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
- .table-section.section-30{ role: "rowheader" }
+ .table-section.section-60{ role: "rowheader" }
= s_("ClusterIntegration|Kubernetes cluster")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Environment scope")
- .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)
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 7ea85fe43d6..8a7f7a5c978 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -15,6 +15,7 @@
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
toggle_status: @cluster.enabled? ? 'true': 'false',
+ cluster_type: @cluster.cluster_type,
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index e4758938059..9793c77fc2b 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -21,9 +21,10 @@
= platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold'
= platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ - if @user_cluster.allow_user_defined_namespace?
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
.form-check
diff --git a/app/views/clusters/clusters/user/_show.html.haml b/app/views/clusters/clusters/user/_show.html.haml
index ad8c35e32e3..cac8e72edd3 100644
--- a/app/views/clusters/clusters/user/_show.html.haml
+++ b/app/views/clusters/clusters/user/_show.html.haml
@@ -22,9 +22,10 @@
%button.js-show-cluster-token.btn-blank{ type: 'button' }
= s_('ClusterIntegration|Show')
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ - if @cluster.allow_user_defined_namespace?
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
.form-check
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index b390c396a09..3cd5168c1f7 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -116,6 +116,19 @@
%strong.fly-out-top-item-name
= _('Members')
+ - if group_sidebar_link?(:kubernetes)
+ = nav_link(controller: [:clusters]) do
+ = link_to group_clusters_path(@group) do
+ .nav-icon-container
+ = sprite_icon('cloud-gear')
+ %span.nav-item-name
+ = _('Kubernetes')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do
+ = link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
+ %strong.fly-out-top-item-name
+ = _('Kubernetes')
+
- if group_sidebar_link?(:settings)
= nav_link(path: group_nav_link_paths) do
= link_to edit_group_path(@group) do
diff --git a/changelogs/unreleased/34758-group-cluster-controller.yml b/changelogs/unreleased/34758-group-cluster-controller.yml
new file mode 100644
index 00000000000..88c4c872714
--- /dev/null
+++ b/changelogs/unreleased/34758-group-cluster-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to create group level clusters and install gitlab managed applications
+merge_request: 22450
+author:
+type: added
diff --git a/changelogs/unreleased/rs-revert-api.yml b/changelogs/unreleased/rs-revert-api.yml
new file mode 100644
index 00000000000..c07b2fe624c
--- /dev/null
+++ b/changelogs/unreleased/rs-revert-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add revert to commits API
+merge_request: 22919
+author:
+type: added
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 2328b50b760..a0aeebe4b91 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -53,6 +53,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resource :avatar, only: [:destroy]
+ concerns :clusterable
+
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 9b7ca4b6e70..994eefa423f 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -288,6 +288,47 @@ Example response:
}
```
+## Revert a commit
+
+> [Introduced][ce-22919] in GitLab 11.6.
+
+Reverts a commit in a given branch.
+
+```
+POST /projects/:id/repository/commits/:sha/revert
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `sha` | string | yes | Commit SHA to revert |
+| `branch` | string | yes | Target branch name |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v4/projects/5/repository/commits/a738f717824ff53aebad8b090c1b79a14f2bd9e8/revert"
+```
+
+Example response:
+
+```json
+{
+ "id":"8b090c1b79a14f2bd9e8a738f717824ff53aebad",
+ "short_id": "8b090c1b",
+ "title":"Revert \"Feature added\"",
+ "created_at":"2018-11-08T15:55:26.000Z",
+ "parent_ids":["a738f717824ff53aebad8b090c1b79a14f2bd9e8"],
+ "message":"Revert \"Feature added\"\n\nThis reverts commit a738f717824ff53aebad8b090c1b79a14f2bd9e8",
+ "author_name":"Administrator",
+ "author_email":"admin@example.com",
+ "authored_date":"2018-11-08T15:55:26.000Z",
+ "committer_name":"Administrator",
+ "committer_email":"admin@example.com",
+ "committed_date":"2018-11-08T15:55:26.000Z"
+}
+```
+
## Get the diff of a commit
Get the diff of a commit in a project.
@@ -619,3 +660,4 @@ Example response:
[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
[ce-15026]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15026
[ce-18004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18004
+[ce-22919]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22919
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 4f4ff85fe1d..36d150c8a5b 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -1,75 +1,82 @@
-# Review apps
+# Review Apps
-Review Apps are automatically deployed by each pipeline, both in
+Review Apps are automatically deployed by each pipeline, both in
[CE](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22010) and
[EE](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6665).
## How does it work?
-1. On every [pipeline][gitlab-pipeline] during the `test` stage, the
+1. On every [pipeline][gitlab-pipeline] during the `test` stage, the
[`review` job][review-job] is automatically started.
1. The `review` job [triggers a pipeline][cng-pipeline] in the
- [`CNG-mirror`][cng-mirror] [^1] project
-1. The `CNG-mirror` pipeline creates the Docker images of each component (e.g. `gitlab-rails-ee`,
- `gitlab-shell`, `gitaly` etc.) based on the commit from the
+ [`CNG-mirror`][cng-mirror] project.
+ - We use the `CNG-mirror` project so that the `CNG`, (**C**loud **N**ative
+ **G**itLab), project's registry is not overloaded with a lot of transient
+ Docker images.
+1. The `CNG-mirror` pipeline creates the Docker images of each component (e.g.
+ `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.) based on the commit from the
[GitLab pipeline][gitlab-pipeline] and store them in its
- [registry][cng-mirror-registry]
-1. Once all images are built, the review app is deployed using
- [the official GitLab Helm chart][helm-chart] [^2] to the
+ [registry][cng-mirror-registry].
+1. Once all images are built, the Review App is deployed using
+ [the official GitLab Helm chart][helm-chart] to the
[`review-apps-ee` Kubernetes cluster on GCP][review-apps-ee]
- - The actual scripts used to deploy the review app can be found at
- [`scripts/review_apps/review-apps.sh`][review-apps.sh]
- - These scripts are basically
- [our official Auto DevOps scripts][Auto-DevOps.gitlab-ci.yml] where the
- default CNG images are overriden with the images built and stored in the
- [`CNG-mirror` project's registry][cng-mirror-registry]
-1. Once the `review` job succeeds, you should be able to use your review app
+ - The actual scripts used to deploy the Review App can be found at
+ [`scripts/review_apps/review-apps.sh`][review-apps.sh]
+ - These scripts are basically
+ [our official Auto DevOps scripts][Auto-DevOps.gitlab-ci.yml] where the
+ default CNG images are overriden with the images built and stored in the
+ [`CNG-mirror` project's registry][cng-mirror-registry].
+ - Since we're using [the official GitLab Helm chart][helm-chart], this means
+ you get a dedicated environment for your branch that's very close to what it
+ would look in production.
+1. Once the `review` job succeeds, you should be able to use your Review App
thanks to the direct link to it from the MR widget. The default username is
`root` and its password can be found in the 1Password secure note named
- **gitlab-{ce,ee} review app's root password**.
+ **gitlab-{ce,ee} Review App's root password** (note that there's currently
+ [a bug where the default password seems to be overriden][password-bug]).
**Additional notes:**
-- The Kubernetes cluster is connected to the `gitlab-ee` project using [GitLab's
- Kubernetes integration][gitlab-k8s-integration]. This basically allows to have
- a link to the review app directly from the merge request widget.
-- The manual `stop_review` in the `post-cleanup` stage can be used to stop a
- review app manually, and is also started by GitLab once a branch is deleted
-- [TBD] Review apps are cleaned up regularly using a pipeline schedule that runs
- the [`scripts/review_apps/automated_cleanup.rb`][automated_cleanup.rb] script
-- If you're unable to log in using the `root` username and password, the
- deployment may have failed. Stop the Review App via the `stop_review`
+- The Kubernetes cluster is connected to the `gitlab-{ce,ee}` projects using
+ [GitLab's Kubernetes integration][gitlab-k8s-integration]. This basically
+ allows to have a link to the Review App directly from the merge request widget.
+- The manual `stop_review` in the `test` stage can be used to stop a Review App
+ manually, and is also started by GitLab once a branch is deleted.
+- Review Apps are cleaned up regularly using a pipeline schedule that runs
+ the [`scripts/review_apps/automated_cleanup.rb`][automated_cleanup.rb] script.
+- If the Review App deployment fails, you can simply retry it (there's no need
+ to run the `stop_review` job first).
+- If you're unable to log in using the `root` username and password, you may
+ encounter [this bug][password-bug]. Stop the Review App via the `stop_review`
manual job and then retry the `review` job to redeploy the Review App.
-[^1]: We use the `CNG-mirror` project so that the `CNG`, (**C**loud **N**ative **G**itLab), project's registry is
- not overloaded with a lot of transient Docker images.
-[^2]: Since we're using [the official GitLab Helm chart][helm-chart], this means
- you get the a dedicated environment for your branch that's very close to what it
- would look in production
-
## Frequently Asked Questions
-**Will it be too much to trigger CNG image builds on every test run? This could create thousands of unused docker images.**
+**Isn't it too much to trigger CNG image builds on every test run? This creates
+thousands of unused Docker images.**
- > We have to start somewhere and improve later. If we see this getting out of hand, we will revisit.
+ > We have to start somewhere and improve later. Also, we're using the
+ CNG-mirror project to store these Docker images so that we can just wipe out
+ the registry at some point, and use a new fresh, empty one.
-**How big is the Kubernetes cluster?**
+**How big are the Kubernetes clusters (`review-apps-ce` and `review-apps-ee`)?**
- > The cluster is currently setup with a single pool of preemptible
- nodes, with a minimum of 1 node and a maximum of 30 nodes.
+ > The clusters are currently set up with a single pool of preemptible nodes,
+ with a minimum of 1 node and a maximum of 100 nodes.
**What are the machine running on the cluster?**
> We're currently using `n1-standard-4` (4 vCPUs, 15 GB memory) machines.
-**How do we secure this from abuse? Apps are open to the world so we need to find a way to limit it to only us.**
+**How do we secure this from abuse? Apps are open to the world so we need to
+find a way to limit it to only us.**
- > This won't work for forks. We will add a root password to 1password shared vault.
+ > This isn't enabled for forks.
-[gitlab-pipeline]: https://gitlab.com/gitlab-org/gitlab-ee/pipelines/29302122
-[review-job]: https://gitlab.com/gitlab-org/gitlab-ee/-/jobs/94294136
+[gitlab-pipeline]: https://gitlab.com/gitlab-org/gitlab-ce/pipelines/35850709
+[review-job]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/118076368
[cng-mirror]: https://gitlab.com/gitlab-org/build/CNG-mirror
-[cng-pipeline]: https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/29307727
+[cng-pipeline]: https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/35883435
[cng-mirror-registry]: https://gitlab.com/gitlab-org/build/CNG-mirror/container_registry
[helm-chart]: https://gitlab.com/charts/gitlab/
[review-apps-ee]: https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps-ee?project=gitlab-review-apps
@@ -77,6 +84,7 @@ Review Apps are automatically deployed by each pipeline, both in
[automated_cleanup.rb]: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/scripts/review_apps/automated_cleanup.rb
[Auto-DevOps.gitlab-ci.yml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
[gitlab-k8s-integration]: https://docs.gitlab.com/ee/user/project/clusters/index.html
+[password-bug]: https://gitlab.com/gitlab-org/gitlab-ce/issues/53621
---
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index e59abd3e3d0..1b228069005 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -204,6 +204,40 @@ module API
end
end
+ desc 'Revert a commit in a branch' do
+ detail 'This feature was introduced in GitLab 11.6'
+ success Entities::Commit
+ end
+ params do
+ requires :sha, type: String, desc: 'Commit SHA to revert'
+ requires :branch, type: String, desc: 'Target branch name', allow_blank: false
+ end
+ post ':id/repository/commits/:sha/revert', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
+ authorize_push_to_branch!(params[:branch])
+
+ commit = user_project.commit(params[:sha])
+ not_found!('Commit') unless commit
+
+ find_branch!(params[:branch])
+
+ commit_params = {
+ commit: commit,
+ start_branch: params[:branch],
+ branch_name: params[:branch]
+ }
+
+ result = ::Commits::RevertService
+ .new(user_project, current_user, commit_params)
+ .execute
+
+ if result[:status] == :success
+ present user_project.repository.commit(result[:result]),
+ with: Entities::Commit
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
desc 'Get all references a commit is pushed to' do
detail 'This feature was introduced in GitLab 10.6'
success Entities::BasicRef
diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb
index 008cba9d33c..f2ce24fefa1 100644
--- a/lib/gitlab/kubernetes/helm/base_command.rb
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -10,7 +10,7 @@ module Gitlab
def generate_script
<<~HEREDOC
- set -eo pipefail
+ set -xeo pipefail
HEREDOC
end
diff --git a/lib/gitlab/kubernetes/helm/init_command.rb b/lib/gitlab/kubernetes/helm/init_command.rb
index c7046a9ea75..6691080deca 100644
--- a/lib/gitlab/kubernetes/helm/init_command.rb
+++ b/lib/gitlab/kubernetes/helm/init_command.rb
@@ -45,7 +45,7 @@ module Gitlab
def init_helm_command
command = %w[helm init] + init_command_flags
- command.shelljoin + " >/dev/null\n"
+ command.shelljoin
end
def init_command_flags
diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb
index 55add06bdb4..ff1c1657b98 100644
--- a/lib/gitlab/kubernetes/helm/install_command.rb
+++ b/lib/gitlab/kubernetes/helm/install_command.rb
@@ -35,7 +35,7 @@ module Gitlab
private
def init_command
- 'helm init --client-only >/dev/null'
+ 'helm init --client-only'
end
def repository_command
@@ -43,13 +43,13 @@ module Gitlab
end
def repository_update_command
- 'helm repo update >/dev/null' if repository
+ 'helm repo update' if repository
end
def install_command
command = ['helm', 'install', chart] + install_command_flags
- command.shelljoin + " >/dev/null\n"
+ command.shelljoin
end
def preinstall_command
diff --git a/lib/gitlab/kubernetes/helm/upgrade_command.rb b/lib/gitlab/kubernetes/helm/upgrade_command.rb
index 74188046739..b36315f7a82 100644
--- a/lib/gitlab/kubernetes/helm/upgrade_command.rb
+++ b/lib/gitlab/kubernetes/helm/upgrade_command.rb
@@ -36,7 +36,7 @@ module Gitlab
private
def init_command
- 'helm init --client-only >/dev/null'
+ 'helm init --client-only'
end
def repository_command
@@ -50,7 +50,7 @@ module Gitlab
" --namespace #{::Gitlab::Kubernetes::Helm::NAMESPACE}" \
" -f /data/helm/#{name}/config/values.yaml"
- "helm upgrade #{name} #{chart}#{upgrade_flags} >/dev/null\n"
+ "helm upgrade #{name} #{chart}#{upgrade_flags}"
end
def optional_version_flag
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3d66b092143..d3e1a51370e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1364,6 +1364,15 @@ msgstr ""
msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|Add a Kubernetes cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Adding a Kubernetes cluster to your group will automatically share the cluster across all your projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster."
+msgstr ""
+
+msgid "ClusterIntegration|Adding an integration to your group will share the cluster across all your projects."
+msgstr ""
+
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
@@ -1466,6 +1475,9 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Group cluster"
+msgstr ""
+
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
@@ -1520,9 +1532,6 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster integration"
-msgstr ""
-
msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
msgstr ""
@@ -1532,7 +1541,7 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details"
msgstr ""
-msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
+msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
@@ -1541,10 +1550,13 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
msgstr ""
-msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
-msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
+msgid "ClusterIntegration|Learn more about Kubernetes"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about group Kubernetes clusters"
msgstr ""
msgid "ClusterIntegration|Machine type"
@@ -1589,6 +1601,9 @@ msgstr ""
msgid "ClusterIntegration|Point a wildcard DNS to this generated IP address in order to access your application after it has been deployed."
msgstr ""
+msgid "ClusterIntegration|Project cluster"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1679,9 +1694,6 @@ msgstr ""
msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters."
msgstr ""
-msgid "ClusterIntegration|Toggle Kubernetes Cluster"
-msgstr ""
-
msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
new file mode 100644
index 00000000000..43894372cf5
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Push over HTTP using Git protocol version 2', :requires_git_protocol_v2 do
+ it 'user pushes to the repository' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ # Create a project to push to
+ project = Resource::Project.fabricate! do |project|
+ project.name = 'git-protocol-project'
+ end
+
+ file_name = 'README.md'
+ file_content = 'Test Git protocol v2'
+ git_protocol = '2'
+ git_protocol_reported = nil
+
+ # Use Git to clone the project, push a file to it, and then check the
+ # supported Git protocol
+ Git::Repository.perform do |repository|
+ username = 'GitLab QA'
+ email = 'root@gitlab.com'
+
+ repository.uri = project.repository_http_location.uri
+ repository.use_default_credentials
+ repository.clone
+ repository.configure_identity(username, email)
+
+ git_protocol_reported = repository.push_with_git_protocol(
+ git_protocol,
+ file_name,
+ file_content)
+ end
+
+ project.visit!
+ Page::Project::Show.perform(&:wait_for_push)
+
+ # Check that the push worked
+ expect(page).to have_content(file_name)
+ expect(page).to have_content(file_content)
+
+ # And check that the correct Git protocol was used
+ expect(git_protocol_reported).to eq(git_protocol)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/groups/clusters/applications_controller_spec.rb b/spec/controllers/groups/clusters/applications_controller_spec.rb
new file mode 100644
index 00000000000..68a798542b6
--- /dev/null
+++ b/spec/controllers/groups/clusters/applications_controller_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::Clusters::ApplicationsController do
+ include AccessMatchersForController
+
+ def current_application
+ Clusters::Cluster::APPLICATIONS[application]
+ end
+
+ describe 'POST create' do
+ let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:group) { cluster.group }
+ let(:application) { 'helm' }
+ let(:params) { { application: application, id: cluster.id } }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'schedule an application installation' do
+ expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
+
+ expect { go }.to change { current_application.count }
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.application_helm).to be_scheduled
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it 'return 404' do
+ expect { go }.not_to change { current_application.count }
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is unknown' do
+ let(:application) { 'unkwnown-app' }
+
+ it 'return 404' do
+ go
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is already installing' do
+ before do
+ create(:clusters_applications_helm, :installing, cluster: cluster)
+ end
+
+ it 'returns 400' do
+ go
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ end
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(group_id: group)
+ end
+ end
+end
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
new file mode 100644
index 00000000000..6e130f830a2
--- /dev/null
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -0,0 +1,574 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::ClustersController do
+ include AccessMatchersForController
+ include GoogleApi::CloudPlatformHelpers
+
+ set(:group) { create(:group) }
+
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ describe 'GET index' do
+ def go(params = {})
+ get :index, params.reverse_merge(group_id: group)
+ end
+
+ context 'when feature flag is not enabled' do
+ before do
+ stub_feature_flags(group_clusters: false)
+ end
+
+ it 'renders 404' do
+ go
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(group_clusters: true)
+ end
+
+ describe 'functionality' do
+ context 'when group has one or more clusters' do
+ let(:group) { create(:group) }
+
+ let!(:enabled_cluster) do
+ create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group])
+ end
+
+ let!(:disabled_cluster) do
+ create(:cluster, :disabled, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
+ end
+
+ 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
+
+ context 'when page is specified' do
+ let(:last_page) { group.clusters.page.total_pages }
+
+ before do
+ allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
+ create_list(:cluster, 2, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
+ end
+
+ it 'redirects to the page' do
+ go(page: last_page)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:clusters).current_page).to eq(last_page)
+ end
+ end
+ end
+
+ context 'when group does not have a cluster' do
+ let(:group) { create(:group) }
+
+ 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
+ end
+ end
+ end
+
+ describe 'security' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET new' do
+ def go
+ get :new, group_id: group
+ end
+
+ describe 'functionality for new cluster' do
+ context 'when omniauth has been configured' do
+ let(:key) { 'secret-key' }
+ let(:session_key_for_redirect_uri) do
+ GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
+ end
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(key)
+ end
+
+ it 'has authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to include(key)
+ expect(session[session_key_for_redirect_uri]).to eq(new_group_cluster_path(group))
+ end
+ end
+
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'does not have authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to be_nil
+ end
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'has new object' do
+ go
+
+ expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+ end
+
+ describe 'functionality for existing cluster' do
+ it 'has new object' do
+ go
+
+ expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST create for new cluster' do
+ let(:legacy_abac_param) { 'true' }
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project-12345',
+ legacy_abac: legacy_abac_param
+ }
+ }
+ }
+ end
+
+ def go
+ post :create_gcp, params.merge(group_id: group)
+ end
+
+ describe 'functionality' do
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+
+ cluster = group.clusters.first
+
+ expect(response).to redirect_to(group_cluster_path(group, cluster))
+ expect(cluster).to be_gcp
+ expect(cluster).to be_kubernetes
+ expect(cluster.provider_gcp).to be_legacy_abac
+ end
+
+ context 'when legacy_abac param is false' do
+ let(:legacy_abac_param) { 'false' }
+
+ it 'creates a new cluster with legacy_abac_disabled' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+ expect(group.clusters.first.provider_gcp).not_to be_legacy_abac
+ end
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow_any_instance_of(described_class)
+ .to receive(:token_in_session).and_return('token')
+ allow_any_instance_of(described_class)
+ .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create) do
+ OpenStruct.new(
+ self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
+ status: 'RUNNING'
+ )
+ end
+
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+ end
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST create for existing cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test'
+ }
+ }
+ }
+ end
+
+ def go
+ post :create_user, params.merge(group_id: group)
+ end
+
+ describe 'functionality' do
+ context 'when creates a cluster' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+
+ cluster = group.clusters.first
+
+ expect(response).to redirect_to(group_cluster_path(group, cluster))
+ expect(cluster).to be_user
+ expect(cluster).to be_kubernetes
+ end
+ end
+
+ context 'when creates a RBAC-enabled cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test',
+ authorization_type: 'rbac'
+ }
+ }
+ }
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+
+ cluster = group.clusters.first
+
+ expect(response).to redirect_to(group_cluster_path(group, cluster))
+ expect(cluster).to be_user
+ expect(cluster).to be_kubernetes
+ expect(cluster).to be_platform_kubernetes_rbac
+ end
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET cluster_status' do
+ let(:cluster) { create(:cluster, :providing_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ def go
+ get :cluster_status,
+ group_id: group.to_param,
+ id: cluster,
+ format: :json
+ end
+
+ describe 'functionality' do
+ it 'responds with matching schema' do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('cluster_status')
+ end
+
+ it 'invokes schedule_status_update on each application' do
+ expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update)
+
+ go
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET show' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ def go
+ get :show,
+ group_id: group,
+ id: cluster
+ end
+
+ describe 'functionality' do
+ it 'renders view' do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:cluster)).to eq(cluster)
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'PUT update' do
+ def go(format: :html)
+ put :update, params.merge(
+ group_id: group.to_param,
+ id: cluster,
+ format: format
+ )
+ end
+
+ let(:cluster) { create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group]) }
+
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name'
+ }
+ }
+ end
+
+ it 'updates and redirects back to show page' do
+ go
+
+ cluster.reload
+ expect(response).to redirect_to(group_cluster_path(group, cluster))
+ expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ end
+
+ context 'when format is json' do
+ context 'when changing parameters' do
+ context 'when valid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name'
+ }
+ }
+ end
+
+ it 'updates and redirects back to show page' do
+ go(format: :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')
+ end
+ end
+
+ context 'when invalid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: ''
+ }
+ }
+ end
+
+ it 'rejects changes' do
+ go(format: :json)
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+ end
+
+ describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
+
+ def go
+ delete :destroy,
+ group_id: group,
+ id: cluster
+ end
+
+ describe 'functionality' do
+ context 'when cluster is provided by GCP' do
+ context 'when cluster is created' do
+ it 'destroys and redirects back to clusters list' do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(group_clusters_path(group))
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+
+ context 'when cluster is being created' do
+ let!(:cluster) { create(:cluster, :providing_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
+
+ it 'destroys and redirects back to clusters list' do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(group_clusters_path(group))
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+ end
+
+ context 'when cluster is provided by user' do
+ let!(:cluster) { create(:cluster, :provided_by_user, :production_environment, cluster_type: :group_type, groups: [group]) }
+
+ it 'destroys and redirects back to clusters list' do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(0)
+
+ expect(response).to redirect_to(group_clusters_path(group))
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+ end
+
+ describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ context 'no group_id param' do
+ it 'does not respond to any action without group_id param' do
+ expect { get :index }.to raise_error(ActionController::UrlGenerationError)
+ end
+ end
+end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 04aece26590..483222363bb 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -122,7 +122,7 @@ describe Projects::ClustersController do
it 'has new object' do
go
- expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::Cluster)
+ expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
end
end
@@ -143,7 +143,7 @@ describe Projects::ClustersController do
it 'has new object' do
go
- expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::Cluster)
+ expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
end
end
@@ -396,20 +396,6 @@ describe Projects::ClustersController do
end
describe 'PUT update' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- let(:params) do
- {
- cluster: {
- enabled: false,
- name: 'my-new-cluster-name',
- platform_kubernetes_attributes: {
- namespace: 'my-namespace'
- }
- }
- }
- end
-
def go(format: :html)
put :update, params.merge(namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -423,105 +409,73 @@ describe Projects::ClustersController do
stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
end
- context 'when cluster is provided by GCP' do
- it "updates and redirects back to show page" do
- go
-
- cluster.reload
- expect(response).to redirect_to(project_cluster_path(project, cluster))
- expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
- expect(cluster.enabled).to be_falsey
- end
-
- it "does not change cluster name" do
- go
-
- cluster.reload
- expect(cluster.name).to eq('test-cluster')
- end
-
- context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
+ let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
- it "rejects changes" do
- go
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
- expect(cluster.enabled).to be_truthy
- end
- end
- end
-
- context 'when cluster is provided by user' do
- let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
-
- let(:params) do
- {
- cluster: {
- enabled: false,
- name: 'my-new-cluster-name',
- platform_kubernetes_attributes: {
- namespace: 'my-namespace'
- }
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
}
}
- end
+ }
+ end
- it "updates and redirects back to show page" do
- go
+ it "updates and redirects back to show page" do
+ go
- cluster.reload
- expect(response).to redirect_to(project_cluster_path(project, cluster))
- expect(flash[:notice]).to eq('Kubernetes 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
+ cluster.reload
+ expect(response).to redirect_to(project_cluster_path(project, cluster))
+ expect(flash[:notice]).to eq('Kubernetes 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
- context 'when format is json' do
- context 'when changing parameters' do
- context 'when valid parameters are used' do
- let(:params) do
- {
- cluster: {
- enabled: false,
- name: 'my-new-cluster-name',
- platform_kubernetes_attributes: {
- namespace: 'my-namespace'
- }
+ context 'when format is json' do
+ context 'when changing parameters' do
+ context 'when valid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
}
}
- end
+ }
+ end
- it "updates and redirects back to show page" do
- go(format: :json)
+ it "updates and redirects back to show page" do
+ go(format: :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
+ 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 #@'
- }
+ context 'when invalid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ platform_kubernetes_attributes: {
+ namespace: 'my invalid namespace #@'
}
}
- end
+ }
+ end
- it "rejects changes" do
- go(format: :json)
+ it "rejects changes" do
+ go(format: :json)
- expect(response).to have_http_status(:bad_request)
- end
+ expect(response).to have_http_status(:bad_request)
end
end
end
diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb
index 4a0d1b181ea..8169c457ab7 100644
--- a/spec/factories/clusters/platforms/kubernetes.rb
+++ b/spec/factories/clusters/platforms/kubernetes.rb
@@ -10,7 +10,7 @@ FactoryBot.define do
username 'xxxxxx'
password 'xxxxxx'
- after(:create) do |platform_kubernetes, evaluator|
+ before(:create) do |platform_kubernetes, evaluator|
pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
platform_kubernetes.ca_cert = File.read(pem_file)
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index f13c35c00d3..a85e7333ba8 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -35,37 +35,6 @@ describe 'Clusters', :js do
expect(page).to have_selector('.gl-responsive-table-row', count: 2)
end
- context 'inline update of cluster' do
- it 'user can update cluster' do
- expect(page).to have_selector('.js-project-feature-toggle')
- end
-
- context 'with successful request' do
- it 'user sees updated cluster' do
- expect do
- page.find('.js-project-feature-toggle').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-project-feature-toggle').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
diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
index 72dc1817936..4a3b9d4bf6a 100644
--- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::Kubernetes::Helm::InitCommand do
let(:commands) do
<<~EOS
- helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem >/dev/null
+ helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem
EOS
end
@@ -22,7 +22,7 @@ describe Gitlab::Kubernetes::Helm::InitCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem --service-account tiller >/dev/null
+ helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem --service-account tiller
EOS
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
index ed879350004..2b7e3ea6def 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -26,9 +26,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
- helm repo update >/dev/null
+ helm repo update
#{helm_install_comand}
EOS
end
@@ -43,7 +43,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--tls-key /data/helm/app-name/config/key.pem
--version 1.2.3
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
@@ -54,9 +54,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
- helm repo update >/dev/null
+ helm repo update
#{helm_install_command}
EOS
end
@@ -72,7 +72,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--version 1.2.3
--set rbac.create\\=true,rbac.enabled\\=true
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
@@ -84,7 +84,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
#{helm_install_command}
EOS
end
@@ -99,7 +99,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--tls-key /data/helm/app-name/config/key.pem
--version 1.2.3
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
@@ -111,9 +111,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
- helm repo update >/dev/null
+ helm repo update
#{helm_install_command}
EOS
end
@@ -122,7 +122,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
<<~EOS.strip
/bin/date
/bin/true
- helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null
+ helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml
EOS
end
end
@@ -134,17 +134,16 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
- helm repo update >/dev/null
+ helm repo update
#{helm_install_command}
EOS
end
let(:helm_install_command) do
<<~EOS.strip
- helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null
-
+ helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml
/bin/date
/bin/false
EOS
@@ -158,9 +157,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
- helm repo update >/dev/null
+ helm repo update
#{helm_install_command}
EOS
end
@@ -171,7 +170,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--name app-name
--version 1.2.3
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
@@ -183,9 +182,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
- helm repo update >/dev/null
+ helm repo update
#{helm_install_command}
EOS
end
@@ -199,7 +198,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb
index 3dabf04413e..9c9fc91ef3c 100644
--- a/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb
@@ -21,8 +21,8 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
- helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm init --client-only
+ helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
@@ -33,8 +33,8 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
- helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm init --client-only
+ helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
@@ -56,9 +56,9 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add #{application.name} #{application.repository}
- helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
@@ -70,8 +70,8 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
- helm upgrade #{application.name} #{application.chart} --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm init --client-only
+ helm upgrade #{application.name} #{application.chart} --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 10b9ca1a778..98d7e799d67 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -343,4 +343,26 @@ describe Clusters::Cluster do
it { is_expected.to eq(false) }
end
end
+
+ describe '#allow_user_defined_namespace?' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { cluster.allow_user_defined_namespace? }
+
+ context 'project type cluster' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'group type cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'instance type cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index 2bcccc8184a..f5d261c4e9d 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -58,6 +58,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_truthy }
end
+
+ context 'for group cluster' do
+ let(:namespace) { 'namespace-123' }
+ let(:cluster) { build(:cluster, :group, :provided_by_user) }
+ let(:kubernetes) { cluster.platform_kubernetes }
+
+ before do
+ kubernetes.namespace = namespace
+ end
+
+ it { is_expected.to be_falsey }
+ end
end
context 'when validates api_url' do
diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
index ced969830d8..b2f0ca1bc30 100644
--- a/spec/policies/clusters/cluster_policy_spec.rb
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -24,5 +24,47 @@ describe Clusters::ClusterPolicy, :models do
it { expect(policy).to be_allowed :update_cluster }
it { expect(policy).to be_allowed :admin_cluster }
end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, :group) }
+ let(:group) { cluster.group }
+ let(:project) { create(:project, namespace: group) }
+
+ context 'when group developer' do
+ before do
+ group.add_developer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when group maintainer' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+
+ context 'when project maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when project developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+ end
end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 5e583be457e..9d0093e8159 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -21,7 +21,11 @@ describe GroupPolicy do
let(:maintainer_permissions) do
[
- :create_projects
+ :create_projects,
+ :read_cluster,
+ :create_cluster,
+ :update_cluster,
+ :admin_cluster
]
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index b7ec35d6ec5..d6bc67a9d70 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -163,7 +163,7 @@ describe ProjectPolicy do
:create_build, :read_build, :update_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
- :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
]
@@ -182,7 +182,7 @@ describe ProjectPolicy do
:create_build, :read_build, :update_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
- :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
]
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 7af181f37d5..72c5eac3ede 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -82,5 +82,12 @@ describe Clusters::ClusterPresenter do
it { is_expected.to eq(project_cluster_path(project, cluster)) }
end
+
+ context 'group_type cluster' do
+ let(:group) { cluster.group }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+ it { is_expected.to eq(group_cluster_path(group, cluster)) }
+ end
end
end
diff --git a/spec/presenters/group_clusterable_presenter_spec.rb b/spec/presenters/group_clusterable_presenter_spec.rb
new file mode 100644
index 00000000000..205160742bf
--- /dev/null
+++ b/spec/presenters/group_clusterable_presenter_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GroupClusterablePresenter do
+ include Gitlab::Routing.url_helpers
+
+ let(:presenter) { described_class.new(group) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+ let(:group) { cluster.group }
+
+ describe '#can_create_cluster?' do
+ let(:user) { create(:user) }
+
+ subject { presenter.can_create_cluster? }
+
+ before do
+ allow(presenter).to receive(:current_user).and_return(user)
+ end
+
+ context 'when user can create' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when user cannot create' do
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#index_path' do
+ subject { presenter.index_path }
+
+ it { is_expected.to eq(group_clusters_path(group)) }
+ end
+
+ describe '#new_path' do
+ subject { presenter.new_path }
+
+ it { is_expected.to eq(new_group_cluster_path(group)) }
+ end
+
+ describe '#create_user_clusters_path' do
+ subject { presenter.create_user_clusters_path }
+
+ it { is_expected.to eq(create_user_group_clusters_path(group)) }
+ end
+
+ describe '#create_gcp_clusters_path' do
+ subject { presenter.create_gcp_clusters_path }
+
+ it { is_expected.to eq(create_gcp_group_clusters_path(group)) }
+ end
+
+ describe '#cluster_status_cluster_path' do
+ subject { presenter.cluster_status_cluster_path(cluster) }
+
+ it { is_expected.to eq(cluster_status_group_cluster_path(group, cluster)) }
+ end
+
+ describe '#install_applications_cluster_path' do
+ let(:application) { :helm }
+
+ subject { presenter.install_applications_cluster_path(cluster, application) }
+
+ it { is_expected.to eq(install_applications_group_cluster_path(group, cluster, application)) }
+ end
+
+ describe '#cluster_path' do
+ subject { presenter.cluster_path(cluster) }
+
+ it { is_expected.to eq(group_cluster_path(group, cluster)) }
+ end
+end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 2963dea634a..329d069ef3d 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1208,6 +1208,118 @@ describe API::Commits do
end
end
+ describe 'POST :id/repository/commits/:sha/revert' do
+ let(:commit_id) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+ let(:commit) { project.commit(commit_id) }
+ let(:branch) { 'master' }
+ let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/revert" }
+
+ shared_examples_for 'ref revert' do
+ context 'when ref exists' do
+ it 'reverts the ref commit' do
+ post api(route, current_user), branch: branch
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/commit/basic')
+
+ expect(json_response['message']).to eq(commit.revert_message(user))
+ expect(json_response['author_name']).to eq(user.name)
+ expect(json_response['committer_name']).to eq(user.name)
+ expect(json_response['parent_ids']).to contain_exactly(commit_id)
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { post api(route, current_user), branch: branch }
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like '403 response' do
+ let(:request) { post api(route), branch: branch }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { post api(route), branch: branch }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as an owner' do
+ let(:current_user) { user }
+
+ it_behaves_like 'ref revert'
+
+ context 'when ref does not exist' do
+ let(:commit_id) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), branch: branch }
+ let(:message) { '404 Commit Not Found' }
+ end
+ end
+
+ context 'when branch is missing' do
+ it_behaves_like '400 response' do
+ let(:request) { post api(route, current_user) }
+ end
+ end
+
+ context 'when branch is empty' do
+ ['', ' '].each do |branch|
+ it_behaves_like '400 response' do
+ let(:request) { post api(route, current_user), branch: branch }
+ end
+ end
+ end
+
+ context 'when branch does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), branch: 'foo' }
+ let(:message) { '404 Branch Not Found' }
+ end
+ end
+
+ context 'when ref contains a dot' do
+ let(:commit_id) { branch_with_dot.name }
+ let(:commit) { project.repository.commit(commit_id) }
+
+ it_behaves_like '400 response' do
+ let(:request) { post api(route, current_user) }
+ end
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ let(:current_user) { user }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when branch is protected' do
+ before do
+ create(:protected_branch, project: project, name: 'feature')
+ end
+
+ it 'returns 400 if you are not allowed to push to the target branch' do
+ post api(route, current_user), branch: 'feature'
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['message']).to match(/You are not allowed to push into this branch/)
+ end
+ end
+ end
+ end
+
describe 'POST /projects/:id/repository/commits/:sha/comments' do
let(:commit) { project.repository.commit }
let(:commit_id) { commit.id }
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index a9985133b93..0bd7719345e 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -60,14 +60,6 @@ describe Clusters::Applications::CreateService do
end
end
- context 'invalid application' do
- let(:params) { { application: 'non-existent' } }
-
- it 'raises an error' do
- expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
- end
- end
-
context 'knative application' do
let(:params) do
{
@@ -100,5 +92,39 @@ describe Clusters::Applications::CreateService do
expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
end
end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ end
+
+ where(:application, :association, :allowed) do
+ 'helm' | :application_helm | true
+ 'ingress' | :application_ingress | true
+ 'runner' | :application_runner | false
+ 'jupyter' | :application_jupyter | false
+ 'prometheus' | :application_prometheus | false
+ end
+
+ with_them do
+ let(:params) { { application: application } }
+
+ it 'executes for each application' do
+ if allowed
+ expect do
+ subject
+
+ cluster.reload
+ end.to change(cluster, association)
+ else
+ expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb
index a1b20c61116..73f9be242a3 100644
--- a/spec/services/clusters/update_service_spec.rb
+++ b/spec/services/clusters/update_service_spec.rb
@@ -62,5 +62,32 @@ describe Clusters::UpdateService do
expect(cluster.errors[:"platform_kubernetes.namespace"]).to be_present
end
end
+
+ context 'when cluster is provided by GCP' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+
+ let(:params) do
+ {
+ name: 'my-new-name'
+ }
+ end
+
+ it 'does not change cluster name' do
+ is_expected.to eq(false)
+
+ cluster.reload
+ expect(cluster.name).to eq('test-cluster')
+ end
+
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :providing_by_gcp) }
+
+ it 'rejects changes' do
+ is_expected.to eq(false)
+
+ expect(cluster.errors.full_messages).to include('cannot modify during creation')
+ end
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb
index 361d4220c6e..ba9b7d3bdcf 100644
--- a/spec/support/shared_examples/helm_generated_script.rb
+++ b/spec/support/shared_examples/helm_generated_script.rb
@@ -2,12 +2,12 @@ shared_examples 'helm commands' do
describe '#generate_script' do
let(:helm_setup) do
<<~EOS
- set -eo pipefail
+ set -xeo pipefail
EOS
end
it 'should return appropriate command' do
- expect(subject.generate_script).to eq(helm_setup + commands)
+ expect(subject.generate_script.strip).to eq((helm_setup + commands).strip)
end
end
end