Commit 46311254 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '212229-move-features-to-core-multiple-kubernetes-clusters' into 'master'

[RUN AS-IF-FOSS] Move feature to core: "Multiple Kubernetes clusters"

Closes #212229

See merge request gitlab-org/gitlab!35094
parents 999e21a7 9ac622c0
# frozen_string_literal: true
module ClustersHelper
# EE overrides this
def has_multiple_clusters?
false
true
end
def create_new_cluster_label(provider: nil)
......@@ -95,5 +94,3 @@ module ClustersHelper
can?(user, :admin_cluster, cluster)
end
end
ClustersHelper.prepend_if_ee('EE::ClustersHelper')
......@@ -2,6 +2,7 @@
module Clusters
class Cluster < ApplicationRecord
prepend HasEnvironmentScope
include Presentable
include Gitlab::Utils::StrongMemoize
include FromUnion
......@@ -81,6 +82,7 @@ module Clusters
validate :no_groups, unless: :group_type?
validate :no_projects, unless: :project_type?
validate :unique_management_project_environment_scope
validate :unique_environment_scope
after_save :clear_reactive_cache!
......@@ -354,6 +356,12 @@ module Clusters
end
end
def unique_environment_scope
if clusterable.present? && clusterable.clusters.where(environment_scope: environment_scope).where.not(id: id).exists?
errors.add(:environment_scope, 'cannot add duplicated environment scope')
end
end
def managed_namespace(environment)
Clusters::KubernetesNamespaceFinder.new(
self,
......
# frozen_string_literal: true
module DeploymentPlatform
# EE would override this and utilize environment argument
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def deployment_platform(environment: nil)
@deployment_platform ||= {}
......@@ -20,16 +19,27 @@ module DeploymentPlatform
find_instance_cluster_platform_kubernetes(environment: environment)
end
# EE would override this and utilize environment argument
def find_platform_kubernetes_with_cte(_environment)
def find_platform_kubernetes_with_cte(environment)
if environment
::Clusters::ClustersHierarchy.new(self, include_management_project: cluster_management_project_enabled?)
.base_and_ancestors
.enabled
.on_environment(environment, relevant_only: true)
.first&.platform_kubernetes
else
Clusters::ClustersHierarchy.new(self, include_management_project: cluster_management_project_enabled?).base_and_ancestors
.enabled.default_environment
.first&.platform_kubernetes
end
end
# EE would override this and utilize environment argument
def find_instance_cluster_platform_kubernetes(environment: nil)
if environment
::Clusters::Instance.new.clusters.enabled.on_environment(environment, relevant_only: true)
.first&.platform_kubernetes
else
Clusters::Instance.new.clusters.enabled.default_environment
.first&.platform_kubernetes
end
end
end
......@@ -13,8 +13,7 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
end
def can_add_cluster?
can?(current_user, :add_cluster, clusterable) &&
(has_no_clusters? || multiple_clusters_available?)
can?(current_user, :add_cluster, clusterable)
end
def can_create_cluster?
......@@ -81,17 +80,6 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
def learn_more_link
raise NotImplementedError
end
private
# Overridden on EE module
def multiple_clusters_available?
false
end
def has_no_clusters?
clusterable.clusters.empty?
end
end
ClusterablePresenter.prepend_if_ee('EE::ClusterablePresenter')
......@@ -19,10 +19,6 @@ module Clusters
cluster = Clusters::Cluster.new(cluster_params)
unless can_create_cluster?
cluster.errors.add(:base, _('Instance does not support multiple Kubernetes clusters'))
end
validate_management_project_permissions(cluster)
return cluster if cluster.errors.present?
......@@ -55,16 +51,9 @@ module Clusters
end
end
# EE would override this method
def can_create_cluster?
clusterable.clusters.empty?
end
def validate_management_project_permissions(cluster)
Clusters::Management::ValidateManagementProjectPermissionsService.new(current_user)
.execute(cluster, params[:management_project_id])
end
end
end
Clusters::CreateService.prepend_if_ee('EE::Clusters::CreateService')
- autodevops_help_url = help_page_path('topics/autodevops/index.md', anchor: 'using-multiple-kubernetes-clusters-premium')
- autodevops_help_url = help_page_path('topics/autodevops/index.md', anchor: 'using-multiple-kubernetes-clusters')
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- help_link_end = '</a>'.html_safe
......
......@@ -5,4 +5,4 @@
%p
= clusterable.learn_more_link
= render_if_exists 'clusters/multiple_clusters_message'
= render 'clusters/clusters/multiple_clusters_message'
---
title: 'Multiple Kubernetes clusters now available in GitLab core'
merge_request: 35094
author:
type: changed
......@@ -316,7 +316,7 @@ The following documentation relates to the DevOps **Configure** stage:
| [GitLab ChatOps](ci/chatops/README.md) | Interact with CI/CD jobs through chat services. |
| [Installing Applications](user/project/clusters/index.md#installing-applications) | Install Helm charts such as Ingress and Prometheus on Kubernetes. |
| [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. |
| [Multiple Kubernetes Clusters](user/project/clusters/index.md#multiple-kubernetes-clusters-premium) **(PREMIUM)** | Associate more than one Kubernetes clusters to your project. |
| [Multiple Kubernetes Clusters](user/project/clusters/index.md#multiple-kubernetes-clusters) | Associate more than one Kubernetes clusters to your project. |
| [Protected variables](ci/variables/README.md#protect-a-custom-variable) | Restrict variables to protected branches and tags. |
| [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. |
| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. |
......
......@@ -893,8 +893,8 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then
++ CI_SERVER_VERSION_PATCH=0
++ export CI_SERVER_REVISION=f4cc00ae823
++ CI_SERVER_REVISION=f4cc00ae823
++ export GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_clusters,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
++ GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_clusters,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
++ export GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
++ GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal
++ export CI_PROJECT_ID=17893
++ CI_PROJECT_ID=17893
++ export CI_PROJECT_NAME=ci-debug-trace
......
......@@ -248,11 +248,11 @@ TIP: **Tip:**
Use the [blue-green deployment](../../ci/environments/incremental_rollouts.md#blue-green-deployment) technique
to minimize downtime and risk.
## Using multiple Kubernetes clusters **(PREMIUM)**
## Using multiple Kubernetes clusters
When using Auto DevOps, you can deploy different environments to
different Kubernetes clusters, due to the 1:1 connection
[existing between them](../../user/project/clusters/index.md#multiple-kubernetes-clusters-premium).
[existing between them](../../user/project/clusters/index.md#multiple-kubernetes-clusters).
The [Deploy Job template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml)
used by Auto DevOps currently defines 3 environment names:
......
......@@ -291,7 +291,7 @@ all within GitLab. Despite its automatic nature, Auto DevOps can also be configu
and customized to fit your workflow. Here are some helpful resources for further reading:
1. [Auto DevOps](index.md)
1. [Multiple Kubernetes clusters](index.md#using-multiple-kubernetes-clusters-premium) **(PREMIUM)**
1. [Multiple Kubernetes clusters](index.md#using-multiple-kubernetes-clusters)
1. [Incremental rollout to production](customize.md#incremental-rollout-to-production-premium) **(PREMIUM)**
1. [Disable jobs you don't need with environment variables](customize.md#environment-variables)
1. [Use a static IP for your cluster](../../user/clusters/applications.md#using-a-static-ip)
......
......@@ -38,10 +38,11 @@ the project.
In the case of sub-groups, GitLab uses the cluster of the closest ancestor group
to the project, provided the cluster is not disabled.
## Multiple Kubernetes clusters **(PREMIUM)**
## Multiple Kubernetes clusters
With [GitLab Premium](https://about.gitlab.com/pricing/premium/), you can associate
more than one Kubernetes cluster to your group, and maintain different clusters
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35094) to GitLab Core in 13.2.
You can associate more than one Kubernetes cluster to your group, and maintain different clusters
for different environments, such as development, staging, and production.
When adding another cluster,
......@@ -93,7 +94,7 @@ To clear the cache:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/24580) in GitLab 11.8.
Domains at the cluster level permit support for multiple domains
per [multiple Kubernetes clusters](#multiple-kubernetes-clusters-premium). When specifying a domain,
per [multiple Kubernetes clusters](#multiple-kubernetes-clusters) When specifying a domain,
this will be automatically set as an environment variable (`KUBE_INGRESS_BASE_DOMAIN`) during
the [Auto DevOps](../../../topics/autodevops/index.md) stages.
......
......@@ -64,11 +64,12 @@ to:
(EKS) using GitLab's UI.
- Add an integration to an existing cluster from any Kubernetes platform.
### Multiple Kubernetes clusters **(PREMIUM)**
### Multiple Kubernetes clusters
> Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3.
> - Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35094) to GitLab core in 13.2.
With GitLab Premium, you can associate more than one Kubernetes cluster to your
You can associate more than one Kubernetes cluster to your
project. That way you can have different clusters for different environments,
like dev, staging, production, and so on.
......
# frozen_string_literal: true
module EE
module ClustersHelper
extend ::Gitlab::Utils::Override
override :has_multiple_clusters?
def has_multiple_clusters?
clusterable.feature_available?(:multiple_clusters)
end
end
end
# frozen_string_literal: true
module EE
module DeploymentPlatform
extend ::Gitlab::Utils::Override
override :find_platform_kubernetes_with_cte
def find_platform_kubernetes_with_cte(environment)
return super unless environment && feature_available?(:multiple_clusters)
::Clusters::ClustersHierarchy.new(self, include_management_project: cluster_management_project_enabled?)
.base_and_ancestors
.enabled
.on_environment(environment, relevant_only: true)
.first&.platform_kubernetes
end
override :find_instance_cluster_platform_kubernetes
def find_instance_cluster_platform_kubernetes(environment: nil)
return super unless environment && feature_available?(:multiple_clusters)
::Clusters::Instance.new.clusters.enabled.on_environment(environment, relevant_only: true)
.first&.platform_kubernetes
end
end
end
......@@ -6,23 +6,12 @@ module EE
extend ActiveSupport::Concern
prepended do
prepend HasEnvironmentScope
include UsageStatistics
validate :unique_environment_scope
end
def prometheus_adapter
application_prometheus
end
private
def unique_environment_scope
if clusterable.present? && clusterable.clusters.where(environment_scope: environment_scope).where.not(id: id).exists?
errors.add(:environment_scope, 'cannot add duplicated environment scope')
end
end
end
end
end
......@@ -780,4 +780,3 @@ module EE
end
EE::Project.include_if_ee('::EE::GitlabRoutingHelper')
EE::Project.include_if_ee('::EE::DeploymentPlatform')
......@@ -89,7 +89,6 @@ class License < ApplicationRecord
merge_trains
metrics_reports
multiple_approval_rules
multiple_clusters
multiple_group_issue_boards
object_storage
operations_dashboard
......
......@@ -2,8 +2,6 @@
module EE
module ClusterablePresenter
extend ::Gitlab::Utils::Override
def metrics_cluster_path(cluster, params = {})
raise NotImplementedError
end
......@@ -11,12 +9,5 @@ module EE
def metrics_dashboard_path(cluster)
raise NotImplementedError
end
private
override :multiple_clusters_available?
def multiple_clusters_available?
clusterable.feature_available?(:multiple_clusters)
end
end
end
# frozen_string_literal: true
module EE
module Clusters
module CreateService
extend ::Gitlab::Utils::Override
override :can_create_cluster?
def can_create_cluster?
super || clusterable.feature_available?(:multiple_clusters)
end
end
end
end
# frozen_string_literal: true
module EE
module API
module GroupClusters
extend ActiveSupport::Concern
prepended do
helpers do
params :create_params_ee do
optional :environment_scope, default: '*', type: String, desc: 'The associated environment to the cluster'
end
params :update_params_ee do
optional :environment_scope, type: String, desc: 'The associated environment to the cluster'
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module API
module ProjectClusters
extend ActiveSupport::Concern
prepended do
helpers do
params :create_params_ee do
optional :environment_scope, default: '*', type: String, desc: 'The associated environment to the cluster'
end
params :update_params_ee do
optional :environment_scope, type: String, desc: 'The associated environment to the cluster'
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Gcp Cluster', :js do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.add_maintainer(user)
gitlab_sign_in(user)
allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
end
context 'when a user has a licence to use multiple clusers' do
before do
stub_licensed_features(multiple_clusters: true)
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
click_link 'Add existing cluster'
end
it 'user sees the "Environment scope" field' do
expect(page).to have_css('#cluster_environment_scope')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'EE Clusters', :js do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.add_maintainer(user)
gitlab_sign_in(user)
stub_feature_flags(clusters_list_redesign: false)
end
context 'when user has a cluster' do
context 'when license has multiple clusters feature' do
before do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:multiple_clusters).and_return(true)
allow_any_instance_of(Clusters::Cluster).to receive(:retrieve_connection_status).and_return(:connected)
end
context 'when user adds an existing cluster' do
before do
create(:cluster, :provided_by_user, name: 'default-cluster', environment_scope: '*', projects: [project])
visit project_clusters_path(project)
end
it 'user sees a add cluster button ' do
expect(page).not_to have_selector('.js-add-cluster.readonly')
expect(page).to have_selector('.js-add-cluster')
end
context 'when user filled form with environment scope' do
before do
click_link 'Add Kubernetes cluster'
click_link 'Add existing cluster'
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: 'staging/*'
click_button 'Add Kubernetes cluster'
end
it 'user sees a cluster details page' do
expect(page.find_field('cluster[name]').value).to eq('staging-cluster')
expect(page.find_field('cluster[environment_scope]').value).to eq('staging/*')
end
end
context 'when user updates environment scope' do
before do
click_link 'default-cluster'
fill_in 'cluster_environment_scope', with: 'production/*'
within '.js-cluster-integration-form' do
click_button 'Save changes'
end
end
it 'user sees a cluster details page' do
expect(page.find_field('cluster[environment_scope]').value).to eq('production/*')
end
end
context 'when user updates duplicated environment scope' do
before do
click_link 'Add Kubernetes cluster'
click_link 'Add existing cluster'
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: '*'
fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'https://0.0.0.0'
fill_in 'cluster_platform_kubernetes_attributes_token', with: 'token'
click_button 'Add Kubernetes cluster'
end
it 'users sees an environment scope validation error' do
expect(page).to have_content('cannot add duplicated environment scope')
end
end
end
context 'when user adds an Google Kubernetes Engine cluster' do
before do
allow_any_instance_of(Projects::ClustersController)
.to receive(:token_in_session).and_return('token')
allow_any_instance_of(Projects::ClustersController)
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
allow_any_instance_of(Projects::ClustersController).to receive(:authorize_google_project_billing)
allow_any_instance_of(Projects::ClustersController).to receive(:google_project_billing_status).and_return(true)
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)
create(:cluster, :provided_by_gcp, name: 'default-cluster', environment_scope: '*', projects: [project])
visit project_clusters_path(project)
end
it 'user sees a add cluster button ' do
expect(page).not_to have_selector('.js-add-cluster.readonly')
expect(page).to have_selector('.js-add-cluster')
end
context 'when user filled form with environment scope' do
before do
click_link 'Add Kubernetes cluster'
click_link 'Create new cluster'
click_link 'Google GKE'
sleep 2 # wait for ajax
execute_script('document.querySelector(".js-gcp-project-id-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gcp-zone-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gcp-machine-type-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gke-cluster-creation-submit").removeAttribute("disabled")')
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: 'staging/*'
fill_in 'cluster[provider_gcp_attributes][gcp_project_id]', with: 'gcp-project-123'
fill_in 'cluster[provider_gcp_attributes][zone]', with: 'us-central1-a'
fill_in 'cluster[provider_gcp_attributes][machine_type]', with: 'n1-standard-2'
click_button 'Create Kubernetes cluster'
# The frontend won't show the details until the cluster is
# created, and we don't want to make calls out to GCP.
provider = Clusters::Cluster.last.provider
provider.make_created
end
it 'user sees a cluster details page' do
expect(page).to have_content('GitLab Integration')
expect(page.find_field('cluster[environment_scope]').value).to eq('staging/*')
end
end
context 'when user updates environment scope' do
before do
click_link 'default-cluster'
fill_in 'cluster_environment_scope', with: 'production/*'
within ".js-cluster-integration-form" do
click_button 'Save changes'
end
end
it 'user sees a cluster details page' do
expect(page.find_field('cluster[environment_scope]').value).to eq('production/*')
end
end
context 'when user updates duplicated environment scope' do
before do
click_link 'Add Kubernetes cluster'
click_link 'Create new cluster'
click_link 'Google GKE'
sleep 2 # wait for ajax
execute_script('document.querySelector(".js-gcp-project-id-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gcp-zone-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gcp-machine-type-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gke-cluster-creation-submit").removeAttribute("disabled")')
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: '*'
fill_in 'cluster[provider_gcp_attributes][gcp_project_id]', with: 'gcp-project-123'
fill_in 'cluster[provider_gcp_attributes][zone]', with: 'us-central1-a'
fill_in 'cluster[provider_gcp_attributes][machine_type]', with: 'n1-standard-2'
click_button 'Create Kubernetes cluster'
end
it 'users sees an environment scope validation error' do
expect(page).to have_content('cannot add duplicated environment scope')
end
end
end
end
context 'when license does not have multiple clusters feature' do
before do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:multiple_clusters).and_return(false)
create(:cluster, :provided_by_user, name: 'default-cluster', environment_scope: '*', projects: [project])
end
context 'when user visits cluster index page' do
before do
visit project_clusters_path(project)
end
it 'user sees a disabled add cluster button ' do
expect(page).to have_selector('.js-add-cluster.disabled')
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ClustersHelper do
shared_examples 'feature availablilty' do |feature|
before do
# clusterable is provided as a `helper_method`
allow(helper).to receive(:clusterable).and_return(clusterable)
expect(clusterable)
.to receive(:feature_available?)
.with(feature)
.and_return(feature_available)
end
context 'feature unavailable' do
let(:feature_available) { true }
it { is_expected.to be_truthy }
end
context 'feature available' do
let(:feature_available) { false }
it { is_expected.to be_falsey }
end
end
describe '#has_multiple_clusters?' do
subject { helper.has_multiple_clusters? }
context 'project level' do
let(:clusterable) { instance_double(Project) }
it_behaves_like 'feature availablilty', :multiple_clusters
end
context 'group level' do
let(:clusterable) { instance_double(Group) }
it_behaves_like 'feature availablilty', :multiple_clusters
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe EE::DeploymentPlatform do
describe '#deployment_platform' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
shared_examples 'matching environment scope' do
context 'when multiple clusters license is available' do
before do
stub_licensed_features(multiple_clusters: true)
end
it 'returns environment specific cluster' do
is_expected.to eq(cluster.platform_kubernetes)
end
end
context 'when multiple clusters licence is unavailable' do
before do
stub_licensed_features(multiple_clusters: false)
end
it 'returns a kubernetes platform' do
is_expected.not_to eq(cluster.platform_kubernetes)
is_expected.to be_kind_of(Clusters::Platforms::Kubernetes)
end
end
end
shared_examples 'not matching environment scope' do
context 'when multiple clusters license is available' do
before do
stub_licensed_features(multiple_clusters: true)
end
it 'returns default cluster' do
is_expected.to eq(default_cluster.platform_kubernetes)
end
end
context 'when multiple clusters license is unavailable' do
before do
stub_licensed_features(multiple_clusters: false)
end
it 'returns default cluster' do
is_expected.to eq(default_cluster.platform_kubernetes)
end
end
end
context 'multiple clusters use the same management project' do
let(:management_project) { create(:project, group: group) }
let!(:default_cluster) do
create(:cluster_for_group, groups: [group], environment_scope: '*', management_project: management_project)
end
let!(:cluster) do
create(:cluster_for_group, groups: [group], environment_scope: 'review/*', management_project: management_project)
end
let(:environment) { 'review/name' }
subject { management_project.deployment_platform(environment: environment) }
it_behaves_like 'matching environment scope'
end
context 'when project does not have a cluster but has group clusters' do
let!(:default_cluster) do
create(:cluster, :provided_by_user,
cluster_type: :group_type, groups: [group], environment_scope: '*')
end
let!(:cluster) do
create(:cluster, :provided_by_user,
cluster_type: :group_type, environment_scope: 'review/*', groups: [group])
end
let(:environment) { 'review/name' }
subject { project.deployment_platform(environment: environment) }
context 'when environment scope is exactly matched' do
before do
cluster.update!(environment_scope: 'review/name')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope is matched by wildcard' do
before do
cluster.update!(environment_scope: 'review/*')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope does not match' do
before do
cluster.update!(environment_scope: 'review/*/special')
end
it_behaves_like 'not matching environment scope'
end
context 'when group belongs to a parent group' do
let(:parent_group) { create(:group) }
let(:group) { create(:group, parent: parent_group) }
context 'when parent_group has a cluster with default scope' do
let!(:parent_group_cluster) do
create(:cluster, :provided_by_user,
cluster_type: :group_type, environment_scope: '*', groups: [parent_group])
end
it_behaves_like 'matching environment scope'
end
context 'when parent_group has a cluster that is an exact match' do
let!(:parent_group_cluster) do
create(:cluster, :provided_by_user,
cluster_type: :group_type, environment_scope: 'review/name', groups: [parent_group])
end
it_behaves_like 'matching environment scope'
end
end
end
context 'with instance clusters' do
let!(:default_cluster) do
create(:cluster, :provided_by_user, :instance, environment_scope: '*')
end
let!(:cluster) do
create(:cluster, :provided_by_user, :instance, environment_scope: 'review/*')
end
let(:environment) { 'review/name' }
subject { project.deployment_platform(environment: environment) }
context 'when environment scope is exactly matched' do
before do
cluster.update!(environment_scope: 'review/name')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope is matched by wildcard' do
before do
cluster.update!(environment_scope: 'review/*')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope does not match' do
before do
cluster.update!(environment_scope: 'review/*/special')
end
it_behaves_like 'not matching environment scope'
end
end
context 'when environment is specified' do
let!(:default_cluster) { create(:cluster, :provided_by_user, projects: [project], environment_scope: '*') }
let!(:cluster) { create(:cluster, :provided_by_user, environment_scope: 'review/*', projects: [project]) }
let!(:group_default_cluster) do
create(:cluster, :provided_by_user,
cluster_type: :group_type, groups: [group], environment_scope: '*')
end
let(:environment) { 'review/name' }
subject { project.deployment_platform(environment: environment) }
context 'when environment scope is exactly matched' do
before do
cluster.update!(environment_scope: 'review/name')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope is matched by wildcard' do
before do
cluster.update!(environment_scope: 'review/*')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope does not match' do
before do
cluster.update!(environment_scope: 'review/*/special')
end
it_behaves_like 'not matching environment scope'
end
context 'when environment scope has _' do
before do
stub_licensed_features(multiple_clusters: true)
end
it 'does not treat it as wildcard' do
cluster.update!(environment_scope: 'foo_bar/*')
is_expected.to eq(default_cluster.platform_kubernetes)
end
context 'when environment name contains an underscore' do
let(:environment) { 'foo_bar/test' }
it 'matches literally for _' do
cluster.update!(environment_scope: 'foo_bar/*')
is_expected.to eq(cluster.platform_kubernetes)
end
end
end
# The environment name and scope cannot have % at the moment,
# but we're considering relaxing it and we should also make sure
# it doesn't break in case some data sneaked in somehow as we're
# not checking this integrity in database level.
context 'when environment scope has %' do
before do
stub_licensed_features(multiple_clusters: true)
end
it 'does not treat it as wildcard' do
cluster.update_attribute(:environment_scope, '*%*')
is_expected.to eq(default_cluster.platform_kubernetes)
end
context 'when environment name contains a percent char' do
let(:environment) { 'foo%bar/test' }
it 'matches literally for %' do
cluster.update_attribute(:environment_scope, 'foo%bar/*')
is_expected.to eq(cluster.platform_kubernetes)
end
end
end
context 'when perfectly matched cluster exists' do
let!(:perfectly_matched_cluster) { create(:cluster, :provided_by_user, projects: [project], environment_scope: 'review/name') }
before do
stub_licensed_features(multiple_clusters: true)
end
it 'returns perfectly matched cluster as highest precedence' do
is_expected.to eq(perfectly_matched_cluster.platform_kubernetes)
end
end
end
context 'with multiple clusters and multiple environments' do
let!(:cluster_1) { create(:cluster, :provided_by_user, projects: [project], environment_scope: 'staging/*') }
let!(:cluster_2) { create(:cluster, :provided_by_user, projects: [project], environment_scope: 'test/*') }
let(:environment_1) { 'staging/name' }
let(:environment_2) { 'test/name' }
before do
stub_licensed_features(multiple_clusters: true)
end
it 'returns the appropriate cluster' do
expect(project.deployment_platform(environment: environment_1)).to eq(cluster_1.platform_kubernetes)
expect(project.deployment_platform(environment: environment_2)).to eq(cluster_2.platform_kubernetes)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::Cluster do
it { is_expected.to include_module(HasEnvironmentScope) }
describe 'validation' do
subject { cluster.valid? }
context 'when validates unique_environment_scope' do
context 'for a project cluster' do
let(:project) { create(:project) }
before do
create(:cluster, projects: [project], environment_scope: 'product/*')
end
context 'when identical environment scope exists in project' do
let(:cluster) { build(:cluster, projects: [project], environment_scope: 'product/*') }
it { is_expected.to be_falsey }
end
context 'when identical environment scope does not exist in project' do
let(:cluster) { build(:cluster, projects: [project], environment_scope: '*') }
it { is_expected.to be_truthy }
end
context 'when identical environment scope exists in different project' do
let(:project2) { create(:project) }
let(:cluster) { build(:cluster, projects: [project2], environment_scope: 'product/*') }
it { is_expected.to be_truthy }
end
end
context 'for a group cluster' do
let(:group) { create(:group) }
before do
create(:cluster, cluster_type: :group_type, groups: [group], environment_scope: 'product/*')
end
context 'when identical environment scope exists in group' do
let(:cluster) { build(:cluster, cluster_type: :group_type, groups: [group], environment_scope: 'product/*') }
it { is_expected.to be_falsey }
end
context 'when identical environment scope does not exist in group' do
let(:cluster) { build(:cluster, cluster_type: :group_type, groups: [group], environment_scope: '*') }
it { is_expected.to be_truthy }
end
context 'when identical environment scope exists in different group' do
let(:cluster) { build(:cluster, :group, environment_scope: 'product/*') }
it { is_expected.to be_truthy }
end
end
context 'for an instance cluster' do
before do
create(:cluster, :instance, environment_scope: 'product/*')
end
context 'identical environment scope exists' do
let(:cluster) { build(:cluster, :instance, environment_scope: 'product/*') }
it { is_expected.to be_falsey }
end
context 'identical environment scope does not exist' do
let(:cluster) { build(:cluster, :instance, environment_scope: '*') }
it { is_expected.to be_truthy }
end
end
end
end
end
......@@ -442,12 +442,6 @@ RSpec.describe Project do
end
describe '#deployment_variables' do
context 'when project has a deployment platforms' do
context 'when multiple clusters (EEP) is enabled' do
before do
stub_licensed_features(multiple_clusters: true)
end
let(:project) { create(:project) }
let!(:default_cluster) do
......@@ -489,8 +483,6 @@ RSpec.describe Project do
end
end
end
end
end
describe '#ensure_external_webhook_token' do
let(:project) { create(:project, :repository) }
......
......@@ -52,25 +52,6 @@ RSpec.describe API::GroupClusters do
expect(json_response['environment_scope']).to eq('*')
end
end
context 'when license has multiple clusters feature' do
before do
stub_licensed_features(multiple_clusters: true)
create(:cluster, :provided_by_gcp, :group,
groups: [group])
post api("/groups/#{group.id}/clusters/user", current_user), params: cluster_params
end
it 'responds with 201' do
expect(response).to have_gitlab_http_status(:created)
end
it 'allows multiple clusters to be associated to group' do
expect(group.reload.clusters.count).to eq(2)
end
end
end
describe 'PUT /groups/:id/clusters/:cluster_id' do
......
......@@ -78,10 +78,8 @@ RSpec.describe API::ProjectClusters do
end
end
context 'when license has multiple clusters feature' do
context 'when another cluster exists' do
before do
stub_licensed_features(multiple_clusters: true)
create(:cluster, :provided_by_gcp, :project,
projects: [project])
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::CreateService do
let(:access_token) { 'xxx' }
let(:project) { create(:project) }
let(:user) { create(:user) }
let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
subject { described_class.new(user, params).execute(access_token: access_token) }
before do
allow(project).to receive(:feature_available?).and_call_original
end
context 'when license has multiple clusters feature' do
before do
allow(project).to receive(:feature_available?).with(:multiple_clusters).and_return(true)
end
context 'when correct params' do
let(:params) do
{
name: 'test-cluster',
provider_type: :gcp,
provider_gcp_attributes: {
gcp_project_id: 'gcp-project',
zone: 'us-central1-a',
num_nodes: 1,
machine_type: 'machine_type-a',
legacy_abac: 'true'
},
clusterable: project
}
end
include_examples 'create cluster service success'
end
context 'when invalid params' do
let(:params) do
{
name: 'test-cluster',
provider_type: :gcp,
provider_gcp_attributes: {
gcp_project_id: '!!!!!!!',
zone: 'us-central1-a',
num_nodes: 1,
machine_type: 'machine_type-a'
},
clusterable: project
}
end
include_examples 'create cluster service error'
end
end
context 'when license does not have multiple clusters feature' do
include_context 'valid cluster create params'
before do
allow(project).to receive(:feature_available?).with(:multiple_clusters).and_return(false)
end
it 'does not create a cluster' do
expect(ClusterProvisionWorker).not_to receive(:perform_async)
expect { subject }.to raise_error(ArgumentError).and change { Clusters::Cluster.count }.by(0)
end
end
end
......@@ -118,8 +118,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
before do
stub_licensed_features(multiple_clusters: true)
create(:clusters_applications_prometheus, :installed,
cluster: prd_cluster, alert_manager_token: token)
create(:clusters_applications_prometheus, :installed,
......
......@@ -6,18 +6,6 @@ module API
before { authenticate! }
# EE::API::GroupClusters will
# override these methods
helpers do
params :create_params_ee do
end
params :update_params_ee do
end
end
prepend_if_ee('EE::API::GroupClusters') # rubocop: disable Cop/InjectEnterpriseEditionModule
params do
requires :id, type: String, desc: 'The ID of the group'
end
......@@ -52,6 +40,7 @@ module API
params do
requires :name, type: String, desc: 'Cluster name'
optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true'
optional :environment_scope, default: '*', type: String, desc: 'The associated environment to the cluster'
optional :domain, type: String, desc: 'Cluster base domain'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true'
......@@ -62,7 +51,6 @@ module API
optional :namespace, type: String, desc: 'Unique namespace related to Group'
optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
end
use :create_params_ee
end
post ':id/clusters/user' do
authorize! :add_cluster, user_group
......@@ -85,6 +73,7 @@ module API
requires :cluster_id, type: Integer, desc: 'The cluster ID'
optional :name, type: String, desc: 'Cluster name'
optional :domain, type: String, desc: 'Cluster base domain'
optional :environment_scope, type: String, desc: 'The associated environment to the cluster'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
......@@ -92,7 +81,6 @@ module API
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Group'
end
use :update_params_ee
end
put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster
......
......@@ -6,18 +6,6 @@ module API
before { authenticate! }
# EE::API::ProjectClusters will
# override these methods
helpers do
params :create_params_ee do
end
params :update_params_ee do
end
end
prepend_if_ee('EE::API::ProjectClusters') # rubocop: disable Cop/InjectEnterpriseEditionModule
params do
requires :id, type: String, desc: 'The ID of the project'
end
......@@ -56,6 +44,7 @@ module API
requires :name, type: String, desc: 'Cluster name'
optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true'
optional :domain, type: String, desc: 'Cluster base domain'
optional :environment_scope, default: '*', type: String, desc: 'The associated environment to the cluster'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true'
requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
......@@ -65,7 +54,6 @@ module API
optional :namespace, type: String, desc: 'Unique namespace related to Project'
optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
end
use :create_params_ee
end
post ':id/clusters/user' do
authorize! :add_cluster, user_project
......@@ -89,6 +77,7 @@ module API
requires :cluster_id, type: Integer, desc: 'The cluster ID'
optional :name, type: String, desc: 'Cluster name'
optional :domain, type: String, desc: 'Cluster base domain'
optional :environment_scope, type: String, desc: 'The associated environment to the cluster'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
......@@ -96,7 +85,6 @@ module API
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Project'
end
use :update_params_ee
end
put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster
......
......@@ -12490,9 +12490,6 @@ msgstr ""
msgid "Instance administrators group already exists"
msgstr ""
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
msgid "Instance license"
msgstr ""
......
......@@ -139,6 +139,19 @@ RSpec.describe 'Gcp Cluster', :js, :do_not_mock_admin_mode do
end
end
context 'when a user adds an existing cluster' do
before do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
click_link 'Add existing cluster'
end
it 'user sees the "Environment scope" field' do
expect(page).to have_css('#cluster_environment_scope')
end
end
context 'when user destroys the cluster' do
before do
click_link 'Advanced Settings'
......@@ -155,19 +168,6 @@ RSpec.describe 'Gcp Cluster', :js, :do_not_mock_admin_mode do
end
end
context 'when a user cannot edit the environment scope' do
before do
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
click_link 'Add existing cluster'
end
it 'user does not see the "Environment scope" field' do
expect(page).not_to have_css('#cluster_environment_scope')
end
end
context 'when user has not dismissed GCP signup offer' do
before do
visit project_clusters_path(project)
......
......@@ -25,6 +25,168 @@ RSpec.describe 'Clusters', :js do
end
end
context 'when user has a cluster' do
before do
allow_any_instance_of(Clusters::Cluster).to receive(:retrieve_connection_status).and_return(:connected)
end
context 'when user adds an existing cluster' do
before do
create(:cluster, :provided_by_user, name: 'default-cluster', environment_scope: '*', projects: [project])
visit project_clusters_path(project)
end
it 'user sees an add cluster button' do
expect(page).to have_selector('.js-add-cluster:not(.readonly)')
end
context 'when user filled form with environment scope' do
before do
click_link 'Add Kubernetes cluster'
click_link 'Add existing cluster'
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: 'staging/*'
click_button 'Add Kubernetes cluster'
end
it 'user sees a cluster details page' do
expect(page.find_field('cluster[name]').value).to eq('staging-cluster')
expect(page.find_field('cluster[environment_scope]').value).to eq('staging/*')
end
end
context 'when user updates environment scope' do
before do
click_link 'default-cluster'
fill_in 'cluster_environment_scope', with: 'production/*'
within '.js-cluster-integration-form' do
click_button 'Save changes'
end
end
it 'updates the environment scope' do
expect(page.find_field('cluster[environment_scope]').value).to eq('production/*')
end
end
context 'when user updates duplicated environment scope' do
before do
click_link 'Add Kubernetes cluster'
click_link 'Add existing cluster'
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: '*'
fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'https://0.0.0.0'
fill_in 'cluster_platform_kubernetes_attributes_token', with: 'token'
click_button 'Add Kubernetes cluster'
end
it 'users sees an environment scope validation error' do
expect(page).to have_content('cannot add duplicated environment scope')
end
end
end
context 'when user adds a Google Kubernetes Engine cluster' do
before do
allow_any_instance_of(Projects::ClustersController)
.to receive(:token_in_session).and_return('token')
allow_any_instance_of(Projects::ClustersController)
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
allow_any_instance_of(Projects::ClustersController).to receive(:authorize_google_project_billing)
allow_any_instance_of(Projects::ClustersController).to receive(:google_project_billing_status).and_return(true)
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)
create(:cluster, :provided_by_gcp, name: 'default-cluster', environment_scope: '*', projects: [project])
visit project_clusters_path(project)
end
it 'user sees a add cluster button ' do
expect(page).to have_selector('.js-add-cluster:not(.readonly)')
end
context 'when user filled form with environment scope' do
before do
click_link 'Add Kubernetes cluster'
click_link 'Create new cluster'
click_link 'Google GKE'
sleep 2 # wait for ajax
execute_script('document.querySelector(".js-gcp-project-id-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gcp-zone-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gcp-machine-type-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gke-cluster-creation-submit").removeAttribute("disabled")')
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: 'staging/*'
fill_in 'cluster[provider_gcp_attributes][gcp_project_id]', with: 'gcp-project-123'
fill_in 'cluster[provider_gcp_attributes][zone]', with: 'us-central1-a'
fill_in 'cluster[provider_gcp_attributes][machine_type]', with: 'n1-standard-2'
click_button 'Create Kubernetes cluster'
# The frontend won't show the details until the cluster is
# created, and we don't want to make calls out to GCP.
provider = Clusters::Cluster.last.provider
provider.make_created
end
it 'user sees a cluster details page' do
expect(page).to have_content('GitLab Integration')
expect(page.find_field('cluster[environment_scope]').value).to eq('staging/*')
end
end
context 'when user updates environment scope' do
before do
click_link 'default-cluster'
fill_in 'cluster_environment_scope', with: 'production/*'
within ".js-cluster-integration-form" do
click_button 'Save changes'
end
end
it 'updates the environment scope' do
expect(page.find_field('cluster[environment_scope]').value).to eq('production/*')
end
end
context 'when user updates duplicated environment scope' do
before do
click_link 'Add Kubernetes cluster'
click_link 'Create new cluster'
click_link 'Google GKE'
sleep 2 # wait for ajax
execute_script('document.querySelector(".js-gcp-project-id-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gcp-zone-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gcp-machine-type-dropdown input").setAttribute("type", "text")')
execute_script('document.querySelector(".js-gke-cluster-creation-submit").removeAttribute("disabled")')
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: '*'
fill_in 'cluster[provider_gcp_attributes][gcp_project_id]', with: 'gcp-project-123'
fill_in 'cluster[provider_gcp_attributes][zone]', with: 'us-central1-a'
fill_in 'cluster[provider_gcp_attributes][machine_type]', with: 'n1-standard-2'
click_button 'Create Kubernetes cluster'
end
it 'users sees an environment scope validation error' do
expect(page).to have_content('cannot add duplicated environment scope')
end
end
end
end
context 'when user has a cluster and visits cluster index page' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
......
......@@ -101,6 +101,12 @@ RSpec.describe ClustersHelper do
end
end
describe '#has_multiple_clusters?' do
subject { helper.has_multiple_clusters? }
it { is_expected.to be_truthy }
end
describe '#cluster_type_label' do
subject { helper.cluster_type_label(cluster_type) }
......
......@@ -10,6 +10,7 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
subject { build(:cluster) }
it { is_expected.to include_module(HasEnvironmentScope) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:management_project).class_name('::Project') }
it { is_expected.to have_many(:cluster_projects) }
......@@ -289,6 +290,79 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
describe 'validations' do
subject { cluster.valid? }
context 'when validates unique_environment_scope' do
context 'for a project cluster' do
let(:project) { create(:project) }
before do
create(:cluster, projects: [project], environment_scope: 'product/*')
end
context 'when identical environment scope exists in project' do
let(:cluster) { build(:cluster, projects: [project], environment_scope: 'product/*') }
it { is_expected.to be_falsey }
end
context 'when identical environment scope does not exist in project' do
let(:cluster) { build(:cluster, projects: [project], environment_scope: '*') }
it { is_expected.to be_truthy }
end
context 'when identical environment scope exists in different project' do
let(:project2) { create(:project) }
let(:cluster) { build(:cluster, projects: [project2], environment_scope: 'product/*') }
it { is_expected.to be_truthy }
end
end
context 'for a group cluster' do
let(:group) { create(:group) }
before do
create(:cluster, cluster_type: :group_type, groups: [group], environment_scope: 'product/*')
end
context 'when identical environment scope exists in group' do
let(:cluster) { build(:cluster, cluster_type: :group_type, groups: [group], environment_scope: 'product/*') }
it { is_expected.to be_falsey }
end
context 'when identical environment scope does not exist in group' do
let(:cluster) { build(:cluster, cluster_type: :group_type, groups: [group], environment_scope: '*') }
it { is_expected.to be_truthy }
end
context 'when identical environment scope exists in different group' do
let(:cluster) { build(:cluster, :group, environment_scope: 'product/*') }
it { is_expected.to be_truthy }
end
end
context 'for an instance cluster' do
before do
create(:cluster, :instance, environment_scope: 'product/*')
end
context 'identical environment scope exists' do
let(:cluster) { build(:cluster, :instance, environment_scope: 'product/*') }
it { is_expected.to be_falsey }
end
context 'identical environment scope does not exist' do
let(:cluster) { build(:cluster, :instance, environment_scope: '*') }
it { is_expected.to be_truthy }
end
end
end
context 'when validates name' do
context 'when provided by user' do
let!(:cluster) { build(:cluster, :provided_by_user, name: name) }
......
......@@ -8,6 +8,241 @@ RSpec.describe DeploymentPlatform do
describe '#deployment_platform' do
subject { project.deployment_platform }
context 'multiple clusters' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
shared_examples 'matching environment scope' do
it 'returns environment specific cluster' do
is_expected.to eq(cluster.platform_kubernetes)
end
end
shared_examples 'not matching environment scope' do
it 'returns default cluster' do
is_expected.to eq(default_cluster.platform_kubernetes)
end
end
context 'multiple clusters use the same management project' do
let(:management_project) { create(:project, group: group) }
let!(:default_cluster) do
create(:cluster_for_group, groups: [group], environment_scope: '*', management_project: management_project)
end
let!(:cluster) do
create(:cluster_for_group, groups: [group], environment_scope: 'review/*', management_project: management_project)
end
let(:environment) { 'review/name' }
subject { management_project.deployment_platform(environment: environment) }
it_behaves_like 'matching environment scope'
end
context 'when project does not have a cluster but has group clusters' do
let!(:default_cluster) do
create(:cluster, :provided_by_user,
cluster_type: :group_type, groups: [group], environment_scope: '*')
end
let!(:cluster) do
create(:cluster, :provided_by_user,
cluster_type: :group_type, environment_scope: 'review/*', groups: [group])
end
let(:environment) { 'review/name' }
subject { project.deployment_platform(environment: environment) }
context 'when environment scope is exactly matched' do
before do
cluster.update!(environment_scope: 'review/name')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope is matched by wildcard' do
before do
cluster.update!(environment_scope: 'review/*')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope does not match' do
before do
cluster.update!(environment_scope: 'review/*/special')
end
it_behaves_like 'not matching environment scope'
end
context 'when group belongs to a parent group' do
let(:parent_group) { create(:group) }
let(:group) { create(:group, parent: parent_group) }
context 'when parent_group has a cluster with default scope' do
let!(:parent_group_cluster) do
create(:cluster, :provided_by_user,
cluster_type: :group_type, environment_scope: '*', groups: [parent_group])
end
it_behaves_like 'matching environment scope'
end
context 'when parent_group has a cluster that is an exact match' do
let!(:parent_group_cluster) do
create(:cluster, :provided_by_user,
cluster_type: :group_type, environment_scope: 'review/name', groups: [parent_group])
end
it_behaves_like 'matching environment scope'
end
end
end
context 'with instance clusters' do
let!(:default_cluster) do
create(:cluster, :provided_by_user, :instance, environment_scope: '*')
end
let!(:cluster) do
create(:cluster, :provided_by_user, :instance, environment_scope: 'review/*')
end
let(:environment) { 'review/name' }
subject { project.deployment_platform(environment: environment) }
context 'when environment scope is exactly matched' do
before do
cluster.update!(environment_scope: 'review/name')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope is matched by wildcard' do
before do
cluster.update!(environment_scope: 'review/*')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope does not match' do
before do
cluster.update!(environment_scope: 'review/*/special')
end
it_behaves_like 'not matching environment scope'
end
end
context 'when environment is specified' do
let!(:default_cluster) { create(:cluster, :provided_by_user, projects: [project], environment_scope: '*') }
let!(:cluster) { create(:cluster, :provided_by_user, environment_scope: 'review/*', projects: [project]) }
let!(:group_default_cluster) do
create(:cluster, :provided_by_user,
cluster_type: :group_type, groups: [group], environment_scope: '*')
end
let(:environment) { 'review/name' }
subject { project.deployment_platform(environment: environment) }
context 'when environment scope is exactly matched' do
before do
cluster.update!(environment_scope: 'review/name')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope is matched by wildcard' do
before do
cluster.update!(environment_scope: 'review/*')
end
it_behaves_like 'matching environment scope'
end
context 'when environment scope does not match' do
before do
cluster.update!(environment_scope: 'review/*/special')
end
it_behaves_like 'not matching environment scope'
end
context 'when environment scope has _' do
it 'does not treat it as wildcard' do
cluster.update!(environment_scope: 'foo_bar/*')
is_expected.to eq(default_cluster.platform_kubernetes)
end
context 'when environment name contains an underscore' do
let(:environment) { 'foo_bar/test' }
it 'matches literally for _' do
cluster.update!(environment_scope: 'foo_bar/*')
is_expected.to eq(cluster.platform_kubernetes)
end
end
end
# The environment name and scope cannot have % at the moment,
# but we're considering relaxing it and we should also make sure
# it doesn't break in case some data sneaked in somehow as we're
# not checking this integrity in database level.
context 'when environment scope has %' do
it 'does not treat it as wildcard' do
cluster.update_attribute(:environment_scope, '*%*')
is_expected.to eq(default_cluster.platform_kubernetes)
end
context 'when environment name contains a percent char' do
let(:environment) { 'foo%bar/test' }
it 'matches literally for %' do
cluster.update_attribute(:environment_scope, 'foo%bar/*')
is_expected.to eq(cluster.platform_kubernetes)
end
end
end
context 'when perfectly matched cluster exists' do
let!(:perfectly_matched_cluster) { create(:cluster, :provided_by_user, projects: [project], environment_scope: 'review/name') }
it 'returns perfectly matched cluster as highest precedence' do
is_expected.to eq(perfectly_matched_cluster.platform_kubernetes)
end
end
end
context 'with multiple clusters and multiple environments' do
let!(:cluster_1) { create(:cluster, :provided_by_user, projects: [project], environment_scope: 'staging/*') }
let!(:cluster_2) { create(:cluster, :provided_by_user, projects: [project], environment_scope: 'test/*') }
let(:environment_1) { 'staging/name' }
let(:environment_2) { 'test/name' }
it 'returns the appropriate cluster' do
expect(project.deployment_platform(environment: environment_1)).to eq(cluster_1.platform_kubernetes)
expect(project.deployment_platform(environment: environment_2)).to eq(cluster_2.platform_kubernetes)
end
end
end
context 'with no Kubernetes configuration on CI/CD, no Kubernetes Service' do
it { is_expected.to be_nil }
end
......
......@@ -2905,17 +2905,12 @@ RSpec.describe Project do
subject { project.deployment_variables(environment: environment, kubernetes_namespace: namespace) }
context 'when the deployment platform is stubbed' do
before do
expect(project).to receive(:deployment_platform).with(environment: environment)
.and_return(deployment_platform)
end
context 'when project has no deployment platform' do
let(:deployment_platform) { nil }
it { is_expected.to eq [] }
end
context 'when project has a deployment platform' do
let(:platform_variables) { %w(platform variables) }
let(:deployment_platform) { double }
......@@ -2928,6 +2923,56 @@ RSpec.describe Project do
it { is_expected.to eq platform_variables }
end
context 'when project has no deployment platform' do
let(:deployment_platform) { nil }
it { is_expected.to eq [] }
end
end
context 'when project has a deployment platforms' do
let(:project) { create(:project) }
let!(:default_cluster) do
create(:cluster,
:not_managed,
platform_type: :kubernetes,
projects: [project],
environment_scope: '*',
platform_kubernetes: default_cluster_kubernetes)
end
let!(:review_env_cluster) do
create(:cluster,
:not_managed,
platform_type: :kubernetes,
projects: [project],
environment_scope: 'review/*',
platform_kubernetes: review_env_cluster_kubernetes)
end
let(:default_cluster_kubernetes) { create(:cluster_platform_kubernetes, token: 'default-AAA') }
let(:review_env_cluster_kubernetes) { create(:cluster_platform_kubernetes, token: 'review-AAA') }
context 'when environment name is review/name' do
let!(:environment) { create(:environment, project: project, name: 'review/name') }
it 'returns variables from this service' do
expect(project.deployment_variables(environment: 'review/name'))
.to include(key: 'KUBE_TOKEN', value: 'review-AAA', public: false, masked: true)
end
end
context 'when environment name is other' do
let!(:environment) { create(:environment, project: project, name: 'staging/name') }
it 'returns variables from this service' do
expect(project.deployment_variables(environment: 'staging/name'))
.to include(key: 'KUBE_TOKEN', value: 'default-AAA', public: false, masked: true)
end
end
end
end
describe '#default_environment' do
......
......@@ -266,29 +266,51 @@ RSpec.describe API::GroupClusters do
end
end
context 'when user tries to add multiple clusters' do
context 'non-authorized user' do
before do
create(:cluster, :provided_by_gcp, :group,
groups: [group])
post api("/groups/#{group.id}/clusters/user", developer_user), params: cluster_params
end
post api("/groups/#{group.id}/clusters/user", current_user), params: cluster_params
it 'responds with 403' do
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden')
end
end
end
it 'responds with 400' do
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['base'].first).to eq(_('Instance does not support multiple Kubernetes clusters'))
describe 'PUT /groups/:id/clusters/:cluster_id' do
let(:api_url) { 'https://kubernetes.example.com' }
let(:platform_kubernetes_attributes) do
{
api_url: api_url,
token: 'sample-token'
}
end
let(:cluster_params) do
{
name: 'test-cluster',
environment_scope: 'test/*',
platform_kubernetes_attributes: platform_kubernetes_attributes
}
end
context 'non-authorized user' do
context 'when another cluster exists' do
before do
post api("/groups/#{group.id}/clusters/user", developer_user), params: cluster_params
create(:cluster, :provided_by_gcp, :group,
groups: [group])
post api("/groups/#{group.id}/clusters/user", current_user), params: cluster_params
end
it 'responds with 403' do
expect(response).to have_gitlab_http_status(:forbidden)
it 'responds with 201' do
expect(response).to have_gitlab_http_status(:created)
end
expect(json_response['message']).to eq('403 Forbidden')
it 'allows multiple clusters to be associated to group' do
expect(group.reload.clusters.count).to eq(2)
end
end
end
......
......@@ -40,7 +40,7 @@ RSpec.describe API::ProjectClusters do
expect(response).to include_pagination_headers
end
it 'onlies include authorized clusters' do
it 'only includes authorized clusters' do
cluster_ids = json_response.map { |cluster| cluster['id'] }
expect(response).to have_gitlab_http_status(:ok)
......@@ -258,29 +258,52 @@ RSpec.describe API::ProjectClusters do
end
end
context 'when user tries to add multiple clusters' do
context 'non-authorized user' do
before do
create(:cluster, :provided_by_gcp, :project,
projects: [project])
post api("/projects/#{project.id}/clusters/user", developer_user), params: cluster_params
end
post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params
it 'responds with 403' do
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden')
end
end
end
it 'responds with 400' do
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['base'].first)
.to eq(_('Instance does not support multiple Kubernetes clusters'))
describe 'POST /projects/:id/clusters/user with multiple clusters' do
let(:api_url) { 'https://kubernetes.example.com' }
let(:namespace) { project.path }
let(:platform_kubernetes_attributes) do
{
api_url: api_url,
token: 'sample-token',
namespace: namespace
}
end
let(:cluster_params) do
{
name: 'test-cluster',
environment_scope: 'production/*',
platform_kubernetes_attributes: platform_kubernetes_attributes
}
end
context 'non-authorized user' do
context 'when another cluster exists' do
before do
post api("/projects/#{project.id}/clusters/user", developer_user), params: cluster_params
create(:cluster, :provided_by_gcp, :project,
projects: [project])
post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params
end
it 'responds with 403' do
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden')
it 'responds with 201' do
expect(response).to have_gitlab_http_status(:created)
end
it 'allows multiple clusters to be associated to project' do
expect(project.reload.clusters.count).to eq(2)
end
end
end
......
......@@ -53,13 +53,54 @@ RSpec.describe Clusters::CreateService do
include_context 'valid cluster create params'
let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
it 'does not create a cluster' do
expect(ClusterProvisionWorker).not_to receive(:perform_async)
expect { subject }.to raise_error(ArgumentError).and change { Clusters::Cluster.count }.by(0)
it 'creates another cluster' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { subject }.to change { Clusters::Cluster.count }.by(1)
end
end
end
context 'when another cluster exists' do
let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
context 'when correct params' do
let(:params) do
{
name: 'test-cluster',
provider_type: :gcp,
provider_gcp_attributes: {
gcp_project_id: 'gcp-project',
zone: 'us-central1-a',
num_nodes: 1,
machine_type: 'machine_type-a',
legacy_abac: 'true'
},
clusterable: project
}
end
include_examples 'create cluster service success'
end
context 'when invalid params' do
let(:params) do
{
name: 'test-cluster',
provider_type: :gcp,
provider_gcp_attributes: {
gcp_project_id: '!!!!!!!',
zone: 'us-central1-a',
num_nodes: 1,
machine_type: 'machine_type-a'
},
clusterable: project
}
end
include_examples 'create cluster service error'
end
end
context 'when params includes :management_project_id' do
subject(:cluster) { described_class.new(user, params).execute(access_token: access_token) }
......
......@@ -102,6 +102,41 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
let(:payload_alert_firing) { payload_raw['alerts'].first }
let(:token) { 'token' }
context 'with environment specific clusters' do
let(:prd_cluster) do
cluster
end
let(:stg_cluster) do
create(:cluster, :provided_by_user, projects: [project], enabled: true, environment_scope: 'stg/*')
end
let(:stg_environment) do
create(:environment, project: project, name: 'stg/1')
end
let(:alert_firing) do
create(:prometheus_alert, project: project, environment: stg_environment)
end
before do
create(:clusters_applications_prometheus, :installed,
cluster: prd_cluster, alert_manager_token: token)
create(:clusters_applications_prometheus, :installed,
cluster: stg_cluster, alert_manager_token: nil)
end
context 'without token' do
let(:token_input) { nil }
it_behaves_like 'notifies alerts'
end
context 'with token' do
it_behaves_like 'no notifications', http_status: :unauthorized
end
end
context 'with project specific cluster' do
using RSpec::Parameterized::TableSyntax
......
# frozen_string_literal: true
RSpec.shared_context 'valid cluster create params' do
let(:clusterable) { Clusters::Instance.new }
let(:params) do
{
name: 'test-cluster',
......@@ -11,12 +12,14 @@ RSpec.shared_context 'valid cluster create params' do
num_nodes: 1,
machine_type: 'machine_type-a',
legacy_abac: 'true'
}
},
clusterable: clusterable
}
end
end
RSpec.shared_context 'invalid cluster create params' do
let(:clusterable) { Clusters::Instance.new }
let(:params) do
{
name: 'test-cluster',
......@@ -26,7 +29,9 @@ RSpec.shared_context 'invalid cluster create params' do
zone: 'us-central1-a',
num_nodes: 1,
machine_type: 'machine_type-a'
}
},
clusterable: clusterable
}
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment