summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/projects/clusters_controller.rb28
-rw-r--r--app/models/clusters/cluster.rb73
-rw-r--r--app/models/clusters/platforms/kubernetes.rb101
-rw-r--r--app/models/clusters/project.rb8
-rw-r--r--app/models/clusters/providers/gcp.rb79
-rw-r--r--app/models/gcp/cluster.rb116
-rw-r--r--app/models/project.rb4
-rw-r--r--app/policies/clusters/cluster_policy.rb (renamed from app/policies/gcp/cluster_policy.rb)4
-rw-r--r--app/presenters/clusters/cluster_presenter.rb (renamed from app/presenters/gcp/cluster_presenter.rb)4
-rw-r--r--app/services/ci/create_cluster_service.rb15
-rw-r--r--app/services/ci/fetch_gcp_operation_service.rb17
-rw-r--r--app/services/ci/finalize_cluster_creation_service.rb33
-rw-r--r--app/services/ci/integrate_cluster_service.rb26
-rw-r--r--app/services/ci/provision_cluster_service.rb36
-rw-r--r--app/services/ci/update_cluster_service.rb22
-rw-r--r--app/services/clusters/create_service.rb29
-rw-r--r--app/services/clusters/gcp/fetch_operation_service.rb16
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb56
-rw-r--r--app/services/clusters/gcp/provision_service.rb47
-rw-r--r--app/services/clusters/gcp/verify_provision_status_service.rb48
-rw-r--r--app/services/clusters/update_service.rb7
-rw-r--r--app/validators/cluster_name_validator.rb24
-rw-r--r--app/views/projects/clusters/_form.html.haml40
-rw-r--r--app/views/projects/clusters/show.html.haml6
-rw-r--r--app/workers/cluster_provision_worker.rb6
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb21
-rw-r--r--db/migrate/20171013094327_create_new_clusters_architectures.rb68
-rw-r--r--db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb99
-rw-r--r--db/schema.rb62
-rw-r--r--lib/gitlab/gcp/model.rb13
-rw-r--r--lib/gitlab/import_export/relation_factory.rb4
-rw-r--r--lib/gitlab/usage_data.rb6
-rw-r--r--lib/google_api/cloud_platform/client.rb1
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb486
-rw-r--r--spec/factories/clusters/cluster.rb39
-rw-r--r--spec/factories/clusters/platforms/kubernetes.rb20
-rw-r--r--spec/factories/clusters/providers/gcp.rb32
-rw-r--r--spec/factories/gcp/cluster.rb38
-rw-r--r--spec/features/projects/clusters_spec.rb19
-rw-r--r--spec/fixtures/clusters/sample_cert.pem33
-rw-r--r--spec/javascripts/fixtures/clusters.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml14
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml45
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb6
-rw-r--r--spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb166
-rw-r--r--spec/models/clusters/cluster_spec.rb181
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb188
-rw-r--r--spec/models/clusters/project_spec.rb6
-rw-r--r--spec/models/clusters/providers/gcp_spec.rb183
-rw-r--r--spec/models/gcp/cluster_spec.rb264
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb4
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb (renamed from spec/policies/gcp/cluster_policy_spec.rb)6
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb (renamed from spec/presenters/gcp/cluster_presenter_spec.rb)11
-rw-r--r--spec/serializers/cluster_entity_spec.rb38
-rw-r--r--spec/serializers/cluster_serializer_spec.rb19
-rw-r--r--spec/services/ci/create_cluster_service_spec.rb47
-rw-r--r--spec/services/ci/fetch_gcp_operation_service_spec.rb36
-rw-r--r--spec/services/ci/finalize_cluster_creation_service_spec.rb61
-rw-r--r--spec/services/ci/integrate_cluster_service_spec.rb42
-rw-r--r--spec/services/ci/provision_cluster_service_spec.rb85
-rw-r--r--spec/services/ci/update_cluster_service_spec.rb37
-rw-r--r--spec/services/clusters/create_service_spec.rb64
-rw-r--r--spec/services/clusters/gcp/fetch_operation_service_spec.rb43
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb111
-rw-r--r--spec/services/clusters/gcp/provision_service_spec.rb69
-rw-r--r--spec/services/clusters/gcp/verify_provision_status_service_spec.rb107
-rw-r--r--spec/services/clusters/update_service_spec.rb59
-rw-r--r--spec/support/google_api/cloud_platform_helpers.rb119
-rw-r--r--spec/support/kubernetes_helpers.rb37
-rw-r--r--spec/workers/cluster_provision_worker_spec.rb19
-rw-r--r--spec/workers/wait_for_cluster_creation_worker_spec.rb61
71 files changed, 2676 insertions, 1240 deletions
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 03019b0becc..c1692ea2569 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -27,11 +27,13 @@ class Projects::ClustersController < Projects::ApplicationController
end
def new
- @cluster = project.build_cluster
+ @cluster = Clusters::Cluster.new.tap do |cluster|
+ cluster.build_provider_gcp
+ end
end
def create
- @cluster = Ci::CreateClusterService
+ @cluster = Clusters::CreateService
.new(project, current_user, create_params)
.execute(token_in_session)
@@ -58,7 +60,7 @@ class Projects::ClustersController < Projects::ApplicationController
end
def update
- Ci::UpdateClusterService
+ Clusters::UpdateService
.new(project, current_user, update_params)
.execute(cluster)
@@ -88,19 +90,19 @@ class Projects::ClustersController < Projects::ApplicationController
def create_params
params.require(:cluster).permit(
- :gcp_project_id,
- :gcp_cluster_zone,
- :gcp_cluster_name,
- :gcp_cluster_size,
- :gcp_machine_type,
- :project_namespace,
- :enabled)
+ :enabled,
+ :name,
+ :provider_type,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type
+ ])
end
def update_params
- params.require(:cluster).permit(
- :project_namespace,
- :enabled)
+ params.require(:cluster).permit(:enabled)
end
def authorize_google_api
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
new file mode 100644
index 00000000000..955dba51745
--- /dev/null
+++ b/app/models/clusters/cluster.rb
@@ -0,0 +1,73 @@
+module Clusters
+ class Cluster < ActiveRecord::Base
+ include Presentable
+
+ self.table_name = 'clusters'
+
+ belongs_to :user
+
+ has_many :cluster_projects, class_name: 'Clusters::Project'
+ has_many :projects, through: :cluster_projects, class_name: '::Project'
+
+ # we force autosave to happen when we save `Cluster` model
+ has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
+
+ # We have to ":destroy" it today to ensure that we clean also the Kubernetes Integration
+ has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
+ accepts_nested_attributes_for :provider_gcp, update_only: true
+ accepts_nested_attributes_for :platform_kubernetes, update_only: true
+
+ validates :name, cluster_name: true
+ validate :restrict_modification, on: :update
+
+ # TODO: Move back this into Clusters::Platforms::Kubernetes in 10.3
+ # We need callback here because `enabled` belongs to Clusters::Cluster
+ # Callbacks in Clusters::Platforms::Kubernetes will not be called after update
+ after_save :update_kubernetes_integration!
+
+ delegate :status, to: :provider, allow_nil: true
+ delegate :status_reason, to: :provider, allow_nil: true
+ delegate :status_name, to: :provider, allow_nil: true
+ delegate :on_creation?, to: :provider, allow_nil: true
+ delegate :update_kubernetes_integration!, to: :platform, allow_nil: true
+
+ enum platform_type: {
+ kubernetes: 1
+ }
+
+ enum provider_type: {
+ user: 0,
+ gcp: 1
+ }
+
+ scope :enabled, -> { where(enabled: true) }
+ scope :disabled, -> { where(enabled: false) }
+
+ def provider
+ return provider_gcp if gcp?
+ end
+
+ def platform
+ return platform_kubernetes if kubernetes?
+ end
+
+ def first_project
+ return @first_project if defined?(@first_project)
+
+ @first_project = projects.first
+ end
+ alias_method :project, :first_project
+
+ private
+
+ def restrict_modification
+ if provider&.on_creation?
+ errors.add(:base, "cannot modify during creation")
+ return false
+ end
+
+ true
+ end
+ end
+end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
new file mode 100644
index 00000000000..b11701797c2
--- /dev/null
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -0,0 +1,101 @@
+module Clusters
+ module Platforms
+ class Kubernetes < ActiveRecord::Base
+ self.table_name = 'cluster_platforms_kubernetes'
+
+ belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
+
+ attr_encrypted :password,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ before_validation :enforce_namespace_to_lower_case
+
+ validates :namespace,
+ allow_blank: true,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ # 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
+
+ # TODO: Glue code till we migrate Kubernetes Integration into Platforms::Kubernetes
+ after_destroy :destroy_kubernetes_integration!
+
+ alias_attribute :ca_pem, :ca_cert
+
+ delegate :project, to: :cluster, allow_nil: true
+ delegate :enabled?, to: :cluster, allow_nil: true
+
+ class << self
+ def namespace_for_project(project)
+ "#{project.path}-#{project.id}"
+ end
+ end
+
+ def actual_namespace
+ if namespace.present?
+ namespace
+ else
+ default_namespace
+ end
+ end
+
+ def default_namespace
+ self.class.namespace_for_project(project) if project
+ end
+
+ def update_kubernetes_integration!
+ raise 'Kubernetes service already configured' unless manages_kubernetes_service?
+
+ # This is neccesary, otheriwse enabled? returns true even though cluster updated with enabled: false
+ cluster.reload
+
+ ensure_kubernetes_service&.update!(
+ active: enabled?,
+ api_url: api_url,
+ namespace: namespace,
+ token: token,
+ ca_pem: ca_cert
+ )
+ end
+
+ private
+
+ def enforce_namespace_to_lower_case
+ self.namespace = self.namespace&.downcase
+ end
+
+ # TODO: glue code till we migrate Kubernetes Service into Platforms::Kubernetes class
+ def manages_kubernetes_service?
+ return true unless kubernetes_service&.active?
+
+ kubernetes_service.api_url == api_url
+ end
+
+ def destroy_kubernetes_integration!
+ return unless manages_kubernetes_service?
+
+ kubernetes_service&.destroy!
+ end
+
+ def kubernetes_service
+ @kubernetes_service ||= project&.kubernetes_service
+ end
+
+ def ensure_kubernetes_service
+ @kubernetes_service ||= kubernetes_service || project&.build_kubernetes_service
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb
new file mode 100644
index 00000000000..eeb734b20b8
--- /dev/null
+++ b/app/models/clusters/project.rb
@@ -0,0 +1,8 @@
+module Clusters
+ class Project < ActiveRecord::Base
+ self.table_name = 'cluster_projects'
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster'
+ belongs_to :project, class_name: '::Project'
+ end
+end
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
new file mode 100644
index 00000000000..c4391729dd7
--- /dev/null
+++ b/app/models/clusters/providers/gcp.rb
@@ -0,0 +1,79 @@
+module Clusters
+ module Providers
+ class Gcp < ActiveRecord::Base
+ self.table_name = 'cluster_providers_gcp'
+
+ belongs_to :cluster, inverse_of: :provider_gcp, class_name: 'Clusters::Cluster'
+
+ default_value_for :zone, 'us-central1-a'
+ default_value_for :num_nodes, 3
+ default_value_for :machine_type, 'n1-standard-4'
+
+ attr_encrypted :access_token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ validates :gcp_project_id,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ validates :zone, presence: true
+
+ validates :num_nodes,
+ presence: true,
+ numericality: {
+ only_integer: true,
+ greater_than: 0
+ }
+
+ state_machine :status, initial: :scheduled do
+ state :scheduled, value: 1
+ state :creating, value: 2
+ state :created, value: 3
+ state :errored, value: 4
+
+ event :make_creating do
+ transition any - [:creating] => :creating
+ end
+
+ event :make_created do
+ transition any - [:created] => :created
+ end
+
+ event :make_errored do
+ transition any - [:errored] => :errored
+ end
+
+ before_transition any => [:errored, :created] do |provider|
+ provider.access_token = nil
+ provider.operation_id = nil
+ end
+
+ before_transition any => [:creating] do |provider, transition|
+ operation_id = transition.args.first
+ raise ArgumentError.new('operation_id is required') unless operation_id.present?
+ provider.operation_id = operation_id
+ end
+
+ before_transition any => [:errored] do |provider, transition|
+ status_reason = transition.args.first
+ provider.status_reason = status_reason if status_reason
+ end
+ end
+
+ def on_creation?
+ scheduled? || creating?
+ end
+
+ def api_client
+ return unless access_token
+
+ @api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil)
+ end
+ end
+ end
+end
diff --git a/app/models/gcp/cluster.rb b/app/models/gcp/cluster.rb
deleted file mode 100644
index 162a690c0e3..00000000000
--- a/app/models/gcp/cluster.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-module Gcp
- class Cluster < ActiveRecord::Base
- extend Gitlab::Gcp::Model
- include Presentable
-
- belongs_to :project, inverse_of: :cluster
- belongs_to :user
- belongs_to :service
-
- scope :enabled, -> { where(enabled: true) }
- scope :disabled, -> { where(enabled: false) }
-
- default_value_for :gcp_cluster_zone, 'us-central1-a'
- default_value_for :gcp_cluster_size, 3
- default_value_for :gcp_machine_type, 'n1-standard-4'
-
- attr_encrypted :password,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- attr_encrypted :kubernetes_token,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- attr_encrypted :gcp_token,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- validates :gcp_project_id,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- validates :gcp_cluster_name,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- validates :gcp_cluster_zone, presence: true
-
- validates :gcp_cluster_size,
- presence: true,
- numericality: {
- only_integer: true,
- greater_than: 0
- }
-
- validates :project_namespace,
- allow_blank: true,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- # if we do not do status transition we prevent change
- validate :restrict_modification, on: :update, unless: :status_changed?
-
- state_machine :status, initial: :scheduled do
- state :scheduled, value: 1
- state :creating, value: 2
- state :created, value: 3
- state :errored, value: 4
-
- event :make_creating do
- transition any - [:creating] => :creating
- end
-
- event :make_created do
- transition any - [:created] => :created
- end
-
- event :make_errored do
- transition any - [:errored] => :errored
- end
-
- before_transition any => [:errored, :created] do |cluster|
- cluster.gcp_token = nil
- cluster.gcp_operation_id = nil
- end
-
- before_transition any => [:errored] do |cluster, transition|
- status_reason = transition.args.first
- cluster.status_reason = status_reason if status_reason
- end
- end
-
- def project_namespace_placeholder
- "#{project.path}-#{project.id}"
- end
-
- def on_creation?
- scheduled? || creating?
- end
-
- def api_url
- 'https://' + endpoint if endpoint
- end
-
- def restrict_modification
- if on_creation?
- errors.add(:base, "cannot modify during creation")
- return false
- end
-
- true
- end
- end
-end
diff --git a/app/models/project.rb b/app/models/project.rb
index 3a300a4a03c..d8607d04a98 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -186,7 +186,9 @@ class Project < ActiveRecord::Base
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
- has_one :cluster, class_name: 'Gcp::Cluster', inverse_of: :project
+
+ has_one :cluster_project, class_name: 'Clusters::Project'
+ has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
diff --git a/app/policies/gcp/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb
index e77173ea6e1..1f7c13072b9 100644
--- a/app/policies/gcp/cluster_policy.rb
+++ b/app/policies/clusters/cluster_policy.rb
@@ -1,8 +1,8 @@
-module Gcp
+module Clusters
class ClusterPolicy < BasePolicy
alias_method :cluster, :subject
- delegate { @subject.project }
+ delegate { cluster.project }
rule { can?(:master_access) }.policy do
enable :update_cluster
diff --git a/app/presenters/gcp/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index f7908f92a37..01cb59d0d44 100644
--- a/app/presenters/gcp/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -1,9 +1,9 @@
-module Gcp
+module Clusters
class ClusterPresenter < Gitlab::View::Presenter::Delegated
presents :cluster
def gke_cluster_url
- "https://console.cloud.google.com/kubernetes/clusters/details/#{gcp_cluster_zone}/#{gcp_cluster_name}"
+ "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end
end
end
diff --git a/app/services/ci/create_cluster_service.rb b/app/services/ci/create_cluster_service.rb
deleted file mode 100644
index f7ee0e468e2..00000000000
--- a/app/services/ci/create_cluster_service.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Ci
- class CreateClusterService < BaseService
- def execute(access_token)
- params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE
-
- cluster_params =
- params.merge(user: current_user,
- gcp_token: access_token)
-
- project.create_cluster(cluster_params).tap do |cluster|
- ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
- end
- end
- end
-end
diff --git a/app/services/ci/fetch_gcp_operation_service.rb b/app/services/ci/fetch_gcp_operation_service.rb
deleted file mode 100644
index 0b68e4d6ea9..00000000000
--- a/app/services/ci/fetch_gcp_operation_service.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module Ci
- class FetchGcpOperationService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- operation = api_client.projects_zones_operations(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_operation_id)
-
- yield(operation) if block_given?
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
- end
-end
diff --git a/app/services/ci/finalize_cluster_creation_service.rb b/app/services/ci/finalize_cluster_creation_service.rb
deleted file mode 100644
index 347875c5697..00000000000
--- a/app/services/ci/finalize_cluster_creation_service.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module Ci
- class FinalizeClusterCreationService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- begin
- gke_cluster = api_client.projects_zones_clusters_get(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_cluster_name)
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
-
- endpoint = gke_cluster.endpoint
- api_url = 'https://' + endpoint
- ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate)
- username = gke_cluster.master_auth.username
- password = gke_cluster.master_auth.password
-
- kubernetes_token = Ci::FetchKubernetesTokenService.new(
- api_url, ca_cert, username, password).execute
-
- unless kubernetes_token
- return cluster.make_errored!('Failed to get a default token of kubernetes')
- end
-
- Ci::IntegrateClusterService.new.execute(
- cluster, endpoint, ca_cert, kubernetes_token, username, password)
- end
- end
-end
diff --git a/app/services/ci/integrate_cluster_service.rb b/app/services/ci/integrate_cluster_service.rb
deleted file mode 100644
index d123ce8d26b..00000000000
--- a/app/services/ci/integrate_cluster_service.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Ci
- class IntegrateClusterService
- def execute(cluster, endpoint, ca_cert, token, username, password)
- Gcp::Cluster.transaction do
- cluster.update!(
- enabled: true,
- endpoint: endpoint,
- ca_cert: ca_cert,
- kubernetes_token: token,
- username: username,
- password: password,
- service: cluster.project.find_or_initialize_service('kubernetes'),
- status_event: :make_created)
-
- cluster.service.update!(
- active: true,
- api_url: cluster.api_url,
- ca_pem: ca_cert,
- namespace: cluster.project_namespace,
- token: token)
- end
- rescue ActiveRecord::RecordInvalid => e
- cluster.make_errored!("Failed to integrate cluster into kubernetes_service: #{e.message}")
- end
- end
-end
diff --git a/app/services/ci/provision_cluster_service.rb b/app/services/ci/provision_cluster_service.rb
deleted file mode 100644
index 52d80b01813..00000000000
--- a/app/services/ci/provision_cluster_service.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module Ci
- class ProvisionClusterService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- begin
- operation = api_client.projects_zones_clusters_create(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_cluster_name,
- cluster.gcp_cluster_size,
- machine_type: cluster.gcp_machine_type)
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
-
- unless operation.status == 'RUNNING' || operation.status == 'PENDING'
- return cluster.make_errored!("Operation status is unexpected; #{operation.status_message}")
- end
-
- cluster.gcp_operation_id = api_client.parse_operation_id(operation.self_link)
-
- unless cluster.gcp_operation_id
- return cluster.make_errored!('Can not find operation_id from self_link')
- end
-
- if cluster.make_creating
- WaitForClusterCreationWorker.perform_in(
- WaitForClusterCreationWorker::INITIAL_INTERVAL, cluster.id)
- else
- return cluster.make_errored!("Failed to update cluster record; #{cluster.errors}")
- end
- end
- end
-end
diff --git a/app/services/ci/update_cluster_service.rb b/app/services/ci/update_cluster_service.rb
deleted file mode 100644
index 70d88fca660..00000000000
--- a/app/services/ci/update_cluster_service.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module Ci
- class UpdateClusterService < BaseService
- def execute(cluster)
- Gcp::Cluster.transaction do
- cluster.update!(params)
-
- if params['enabled'] == 'true'
- cluster.service.update!(
- active: true,
- api_url: cluster.api_url,
- ca_pem: cluster.ca_cert,
- namespace: cluster.project_namespace,
- token: cluster.kubernetes_token)
- else
- cluster.service.update!(active: false)
- end
- end
- rescue ActiveRecord::RecordInvalid => e
- cluster.errors.add(:base, e.message)
- end
- end
-end
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
new file mode 100644
index 00000000000..1d407739b21
--- /dev/null
+++ b/app/services/clusters/create_service.rb
@@ -0,0 +1,29 @@
+module Clusters
+ class CreateService < BaseService
+ attr_reader :access_token
+
+ def execute(access_token)
+ @access_token = access_token
+
+ create_cluster.tap do |cluster|
+ ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
+ end
+ end
+
+ private
+
+ def create_cluster
+ Clusters::Cluster.create(cluster_params)
+ end
+
+ def cluster_params
+ return @cluster_params if defined?(@cluster_params)
+
+ params[:provider_gcp_attributes].try do |provider|
+ provider[:access_token] = access_token
+ end
+
+ @cluster_params = params.merge(user: current_user, projects: [project])
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/fetch_operation_service.rb b/app/services/clusters/gcp/fetch_operation_service.rb
new file mode 100644
index 00000000000..a4cd3ca5c11
--- /dev/null
+++ b/app/services/clusters/gcp/fetch_operation_service.rb
@@ -0,0 +1,16 @@
+module Clusters
+ module Gcp
+ class FetchOperationService
+ def execute(provider)
+ operation = provider.api_client.projects_zones_operations(
+ provider.gcp_project_id,
+ provider.zone,
+ provider.operation_id)
+
+ yield(operation) if block_given?
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
new file mode 100644
index 00000000000..cea56f4e849
--- /dev/null
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -0,0 +1,56 @@
+module Clusters
+ module Gcp
+ class FinalizeCreationService
+ attr_reader :provider
+
+ def execute(provider)
+ @provider = provider
+
+ configure_provider
+ configure_kubernetes
+
+ cluster.save!
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ rescue ActiveRecord::RecordInvalid => e
+ provider.make_errored!("Failed to configure GKE Cluster: #{e.message}")
+ end
+
+ private
+
+ def configure_provider
+ provider.endpoint = gke_cluster.endpoint
+ provider.status_event = :make_created
+ end
+
+ def configure_kubernetes
+ cluster.platform_type = :kubernetes
+ cluster.build_platform_kubernetes(
+ api_url: 'https://' + gke_cluster.endpoint,
+ ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
+ username: gke_cluster.master_auth.username,
+ password: gke_cluster.master_auth.password,
+ token: request_kuberenetes_token)
+ end
+
+ def request_kuberenetes_token
+ Ci::FetchKubernetesTokenService.new(
+ 'https://' + gke_cluster.endpoint,
+ Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
+ gke_cluster.master_auth.username,
+ gke_cluster.master_auth.password).execute
+ end
+
+ def gke_cluster
+ @gke_cluster ||= provider.api_client.projects_zones_clusters_get(
+ provider.gcp_project_id,
+ provider.zone,
+ cluster.name)
+ end
+
+ def cluster
+ @cluster ||= provider.cluster
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb
new file mode 100644
index 00000000000..8beea5a8cfb
--- /dev/null
+++ b/app/services/clusters/gcp/provision_service.rb
@@ -0,0 +1,47 @@
+module Clusters
+ module Gcp
+ class ProvisionService
+ attr_reader :provider
+
+ def execute(provider)
+ @provider = provider
+
+ get_operation_id do |operation_id|
+ if provider.make_creating(operation_id)
+ WaitForClusterCreationWorker.perform_in(
+ Clusters::Gcp::VerifyProvisionStatusService::INITIAL_INTERVAL,
+ provider.cluster_id)
+ else
+ provider.make_errored!("Failed to update provider record; #{provider.errors}")
+ end
+ end
+ end
+
+ private
+
+ def get_operation_id
+ operation = provider.api_client.projects_zones_clusters_create(
+ provider.gcp_project_id,
+ provider.zone,
+ provider.cluster.name,
+ provider.num_nodes,
+ machine_type: provider.machine_type)
+
+ unless operation.status == 'PENDING' || operation.status == 'RUNNING'
+ return provider.make_errored!("Operation status is unexpected; #{operation.status_message}")
+ end
+
+ operation_id = provider.api_client.parse_operation_id(operation.self_link)
+
+ unless operation_id
+ return provider.make_errored!('Can not find operation_id from self_link')
+ end
+
+ yield(operation_id)
+
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb
new file mode 100644
index 00000000000..bc33756f27c
--- /dev/null
+++ b/app/services/clusters/gcp/verify_provision_status_service.rb
@@ -0,0 +1,48 @@
+module Clusters
+ module Gcp
+ class VerifyProvisionStatusService
+ attr_reader :provider
+
+ INITIAL_INTERVAL = 2.minutes
+ EAGER_INTERVAL = 10.seconds
+ TIMEOUT = 20.minutes
+
+ def execute(provider)
+ @provider = provider
+
+ request_operation do |operation|
+ case operation.status
+ when 'PENDING', 'RUNNING'
+ continue_creation(operation)
+ when 'DONE'
+ finalize_creation
+ else
+ return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
+ end
+ end
+ end
+
+ private
+
+ def continue_creation(operation)
+ if elapsed_time_from_creation(operation) < TIMEOUT
+ WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id)
+ else
+ provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
+ end
+ end
+
+ def elapsed_time_from_creation(operation)
+ Time.now.utc - operation.start_time.to_time.utc
+ end
+
+ def finalize_creation
+ Clusters::Gcp::FinalizeCreationService.new.execute(provider)
+ end
+
+ def request_operation(&blk)
+ Clusters::Gcp::FetchOperationService.new.execute(provider, &blk)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/update_service.rb b/app/services/clusters/update_service.rb
new file mode 100644
index 00000000000..989218e32a2
--- /dev/null
+++ b/app/services/clusters/update_service.rb
@@ -0,0 +1,7 @@
+module Clusters
+ class UpdateService < BaseService
+ def execute(cluster)
+ cluster.update(params)
+ end
+ end
+end
diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb
new file mode 100644
index 00000000000..13ec342f399
--- /dev/null
+++ b/app/validators/cluster_name_validator.rb
@@ -0,0 +1,24 @@
+# ClusterNameValidator
+#
+# Custom validator for ClusterName.
+class ClusterNameValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if record.user?
+ unless value.present?
+ record.errors.add(attribute, " has to be present")
+ end
+ elsif record.gcp?
+ if record.persisted? && record.name_changed?
+ record.errors.add(attribute, " can not be changed because it's synchronized with provider")
+ end
+
+ unless value.length >= 1 && value.length <= 63
+ record.errors.add(attribute, " is invalid syntax")
+ end
+
+ unless value =~ Gitlab::Regex.kubernetes_namespace_regex
+ record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message)
+ end
+ end
+ end
+end
diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml
index 371cdb1e403..d8e5b55bb88 100644
--- a/app/views/projects/clusters/_form.html.haml
+++ b/app/views/projects/clusters/_form.html.haml
@@ -4,34 +4,32 @@
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
- = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_for @cluster, url: namespace_project_clusters_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+ = field.hidden_field :provider_type, value: :gcp
= form_errors(@cluster)
.form-group
- = field.label :gcp_cluster_name, s_('ClusterIntegration|Cluster name')
- = field.text_field :gcp_cluster_name, class: 'form-control'
+ = field.label :name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :name, class: 'form-control'
- .form-group
- = field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
- = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_project_id, class: 'form-control'
-
- .form-group
- = field.label :gcp_cluster_zone, s_('ClusterIntegration|Zone')
- = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_cluster_zone, class: 'form-control', placeholder: 'us-central1-a'
+ = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
+ .form-group
+ = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
+ = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :gcp_project_id, class: 'form-control'
- .form-group
- = field.label :gcp_cluster_size, s_('ClusterIntegration|Number of nodes')
- = field.text_field :gcp_cluster_size, class: 'form-control', placeholder: '3'
+ .form-group
+ = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
+ = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a'
- .form-group
- = field.label :gcp_machine_type, s_('ClusterIntegration|Machine type')
- = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_machine_type, class: 'form-control', placeholder: 'n1-standard-4'
+ .form-group
+ = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
+ = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
.form-group
- = field.label :project_namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
- = field.text_field :project_namespace, class: 'form-control', placeholder: @cluster.project_namespace_placeholder
+ = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
+ = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4'
.form-group
= field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index dbe6f8beb95..ebb9383ca12 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -33,7 +33,7 @@
- else
= s_('ClusterIntegration|Cluster integration is disabled for this project.')
- = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group.append-bottom-20
%label.append-bottom-10
@@ -62,9 +62,9 @@
%label.append-bottom-10{ for: 'cluter-name' }
= s_('ClusterIntegration|Cluster name')
.input-group
- %input.form-control.cluster-name{ value: @cluster.gcp_cluster_name, disabled: true }
+ %input.form-control.cluster-name{ value: @cluster.name, disabled: true }
%span.input-group-addon.clipboard-addon
- = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name'))
+ = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name'))
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index 63300b58a25..b01f9708424 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -3,8 +3,10 @@ class ClusterProvisionWorker
include ClusterQueue
def perform(cluster_id)
- Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
- Ci::ProvisionClusterService.new.execute(cluster)
+ Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
+ cluster.provider.try do |provider|
+ Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp?
+ end
end
end
end
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
index 5aa3bbdaa9d..241ed3901dc 100644
--- a/app/workers/wait_for_cluster_creation_worker.rb
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -2,25 +2,10 @@ class WaitForClusterCreationWorker
include Sidekiq::Worker
include ClusterQueue
- INITIAL_INTERVAL = 2.minutes
- EAGER_INTERVAL = 10.seconds
- TIMEOUT = 20.minutes
-
def perform(cluster_id)
- Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
- Ci::FetchGcpOperationService.new.execute(cluster) do |operation|
- case operation.status
- when 'RUNNING'
- if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc
- return cluster.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
- end
-
- WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, cluster.id)
- when 'DONE'
- Ci::FinalizeClusterCreationService.new.execute(cluster)
- else
- return cluster.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
- end
+ Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
+ cluster.provider.try do |provider|
+ Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) if cluster.gcp?
end
end
end
diff --git a/db/migrate/20171013094327_create_new_clusters_architectures.rb b/db/migrate/20171013094327_create_new_clusters_architectures.rb
new file mode 100644
index 00000000000..dabb3e25e48
--- /dev/null
+++ b/db/migrate/20171013094327_create_new_clusters_architectures.rb
@@ -0,0 +1,68 @@
+class CreateNewClustersArchitectures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :clusters do |t|
+ t.references :user, index: true, foreign_key: { on_delete: :nullify }
+
+ t.integer :provider_type
+ t.integer :platform_type
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.boolean :enabled, index: true, default: true
+
+ t.string :name, null: false # If manual, read-write. If gcp, read-only.
+ end
+
+ create_table :cluster_projects do |t|
+ t.references :project, null: false, index: true, foreign_key: { on_delete: :cascade }
+ t.references :cluster, null: false, index: true, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+ end
+
+ create_table :cluster_platforms_kubernetes do |t|
+ t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.text :api_url
+ t.text :ca_cert
+
+ t.string :namespace
+
+ t.string :username
+ t.text :encrypted_password
+ t.string :encrypted_password_iv
+
+ t.text :encrypted_token
+ t.string :encrypted_token_iv
+ end
+
+ create_table :cluster_providers_gcp do |t|
+ t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+
+ t.integer :status
+ t.integer :num_nodes, null: false
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.text :status_reason
+
+ t.string :gcp_project_id, null: false
+ t.string :zone, null: false
+ t.string :machine_type
+ t.string :operation_id
+
+ t.string :endpoint
+
+ t.text :encrypted_access_token
+ t.string :encrypted_access_token_iv
+ end
+ end
+end
diff --git a/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb b/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb
new file mode 100644
index 00000000000..4758c694563
--- /dev/null
+++ b/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb
@@ -0,0 +1,99 @@
+class MigrateGcpClustersToNewClustersArchitectures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ class GcpCluster < ActiveRecord::Base
+ self.table_name = 'gcp_clusters'
+
+ belongs_to :project, class_name: 'Project'
+
+ include EachBatch
+ end
+
+ class Cluster < ActiveRecord::Base
+ self.table_name = 'clusters'
+
+ has_many :cluster_projects, class_name: 'ClustersProject'
+ has_many :projects, through: :cluster_projects, class_name: 'Project'
+ has_one :provider_gcp, class_name: 'ProvidersGcp'
+ has_one :platform_kubernetes, class_name: 'PlatformsKubernetes'
+
+ accepts_nested_attributes_for :provider_gcp
+ accepts_nested_attributes_for :platform_kubernetes
+
+ enum platform_type: {
+ kubernetes: 1
+ }
+
+ enum provider_type: {
+ user: 0,
+ gcp: 1
+ }
+ end
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ has_one :cluster_project, class_name: 'ClustersProject'
+ has_one :cluster, through: :cluster_project, class_name: 'Cluster'
+ end
+
+ class ClustersProject < ActiveRecord::Base
+ self.table_name = 'cluster_projects'
+
+ belongs_to :cluster, class_name: 'Cluster'
+ belongs_to :project, class_name: 'Project'
+ end
+
+ class ProvidersGcp < ActiveRecord::Base
+ self.table_name = 'cluster_providers_gcp'
+ end
+
+ class PlatformsKubernetes < ActiveRecord::Base
+ self.table_name = 'cluster_platforms_kubernetes'
+ end
+
+ def up
+ GcpCluster.all.find_each(batch_size: 1) do |gcp_cluster|
+ Cluster.create(
+ enabled: gcp_cluster.enabled,
+ user_id: gcp_cluster.user_id,
+ name: gcp_cluster.gcp_cluster_name,
+ provider_type: Cluster.provider_types[:gcp],
+ platform_type: Cluster.platform_types[:kubernetes],
+ projects: [gcp_cluster.project],
+ provider_gcp_attributes: {
+ status: gcp_cluster.status,
+ status_reason: gcp_cluster.status_reason,
+ gcp_project_id: gcp_cluster.gcp_project_id,
+ zone: gcp_cluster.gcp_cluster_zone,
+ num_nodes: gcp_cluster.gcp_cluster_size,
+ machine_type: gcp_cluster.gcp_machine_type,
+ operation_id: gcp_cluster.gcp_operation_id,
+ endpoint: gcp_cluster.endpoint,
+ encrypted_access_token: gcp_cluster.encrypted_gcp_token,
+ encrypted_access_token_iv: gcp_cluster.encrypted_gcp_token_iv
+ },
+ platform_kubernetes_attributes: {
+ cluster_id: gcp_cluster.id,
+ api_url: api_url(gcp_cluster.endpoint),
+ ca_cert: gcp_cluster.ca_cert,
+ namespace: gcp_cluster.project_namespace,
+ username: gcp_cluster.username,
+ encrypted_password: gcp_cluster.encrypted_password,
+ encrypted_password_iv: gcp_cluster.encrypted_password_iv,
+ encrypted_token: gcp_cluster.encrypted_kubernetes_token,
+ encrypted_token_iv: gcp_cluster.encrypted_kubernetes_token_iv
+ } )
+ end
+ end
+
+ def down
+ execute('DELETE FROM clusters')
+ end
+
+ private
+
+ def api_url(endpoint)
+ endpoint ? 'https://' + endpoint : nil
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 7f1cfed4bb8..c58555f664f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -462,6 +462,63 @@ ActiveRecord::Schema.define(version: 20171101134435) do
add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree
+ create_table "cluster_platforms_kubernetes", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.text "api_url"
+ t.text "ca_cert"
+ t.string "namespace"
+ t.string "username"
+ t.text "encrypted_password"
+ t.string "encrypted_password_iv"
+ t.text "encrypted_token"
+ t.string "encrypted_token_iv"
+ end
+
+ add_index "cluster_platforms_kubernetes", ["cluster_id"], name: "index_cluster_platforms_kubernetes_on_cluster_id", unique: true, using: :btree
+
+ create_table "cluster_projects", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ end
+
+ add_index "cluster_projects", ["cluster_id"], name: "index_cluster_projects_on_cluster_id", using: :btree
+ add_index "cluster_projects", ["project_id"], name: "index_cluster_projects_on_project_id", using: :btree
+
+ create_table "cluster_providers_gcp", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.integer "status"
+ t.integer "num_nodes", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.text "status_reason"
+ t.string "gcp_project_id", null: false
+ t.string "zone", null: false
+ t.string "machine_type"
+ t.string "operation_id"
+ t.string "endpoint"
+ t.text "encrypted_access_token"
+ t.string "encrypted_access_token_iv"
+ end
+
+ add_index "cluster_providers_gcp", ["cluster_id"], name: "index_cluster_providers_gcp_on_cluster_id", unique: true, using: :btree
+
+ create_table "clusters", force: :cascade do |t|
+ t.integer "user_id"
+ t.integer "provider_type"
+ t.integer "platform_type"
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.boolean "enabled", default: true
+ t.string "name", null: false
+ end
+
+ add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree
+ add_index "clusters", ["user_id"], name: "index_clusters_on_user_id", using: :btree
+
create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
@@ -1809,6 +1866,11 @@ ActiveRecord::Schema.define(version: 20171101134435) do
add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade
+ add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade
+ add_foreign_key "cluster_projects", "clusters", on_delete: :cascade
+ add_foreign_key "cluster_projects", "projects", on_delete: :cascade
+ add_foreign_key "cluster_providers_gcp", "clusters", on_delete: :cascade
+ add_foreign_key "clusters", "users", on_delete: :nullify
add_foreign_key "container_repositories", "projects"
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
diff --git a/lib/gitlab/gcp/model.rb b/lib/gitlab/gcp/model.rb
deleted file mode 100644
index 195391f0e3c..00000000000
--- a/lib/gitlab/gcp/model.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Gitlab
- module Gcp
- module Model
- def table_name_prefix
- "gcp_"
- end
-
- def model_name
- @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
- end
- end
- end
-end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 469b230377d..a790dcfe8a6 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -8,8 +8,8 @@ module Gitlab
triggers: 'Ci::Trigger',
pipeline_schedules: 'Ci::PipelineSchedule',
builds: 'Ci::Build',
- cluster: 'Gcp::Cluster',
- clusters: 'Gcp::Cluster',
+ cluster: 'Clusters::Cluster',
+ clusters: 'Clusters::Cluster',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 70a403652e7..112d4939582 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -48,9 +48,9 @@ module Gitlab
deploy_keys: DeployKey.count,
deployments: Deployment.count,
environments: ::Environment.count,
- gcp_clusters: ::Gcp::Cluster.count,
- gcp_clusters_enabled: ::Gcp::Cluster.enabled.count,
- gcp_clusters_disabled: ::Gcp::Cluster.disabled.count,
+ clusters: ::Clusters::Cluster.count,
+ clusters_enabled: ::Clusters::Cluster.enabled.count,
+ clusters_disabled: ::Clusters::Cluster.disabled.count,
in_review_folder: ::Environment.in_review_folder.count,
groups: Group.count,
issues: Issue.count,
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index a440a3e3562..9242cbe840c 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -3,7 +3,6 @@ require 'google/apis/container_v1'
module GoogleApi
module CloudPlatform
class Client < GoogleApi::Auth
- DEFAULT_MACHINE_TYPE = 'n1-standard-1'.freeze
SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze
LEAST_TOKEN_LIFE_TIME = 10.minutes
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index bd924a1c7be..de4cf40b492 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -1,68 +1,108 @@
require 'spec_helper'
describe Projects::ClustersController do
- set(:user) { create(:user) }
- set(:project) { create(:project) }
- let(:role) { :master }
+ include AccessMatchersForController
+ include GoogleApi::CloudPlatformHelpers
- before do
- project.team << [user, role]
+ describe 'GET index' do
+ describe 'functionality' do
+ let(:user) { create(:user) }
- sign_in(user)
- end
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
- describe 'GET index' do
- subject do
- get :index, namespace_id: project.namespace,
- project_id: project
- end
+ context 'when project has a cluster' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- context 'when cluster is already created' do
- let!(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ it { expect(go).to redirect_to(project_cluster_path(project, project.cluster)) }
+ end
- it 'redirects to show a cluster' do
- subject
+ context 'when project does not have a cluster' do
+ let(:project) { create(:project) }
- expect(response).to redirect_to(project_cluster_path(project, cluster))
+ it { expect(go).to redirect_to(new_project_cluster_path(project)) }
end
end
- context 'when we do not have cluster' do
- it 'redirects to create a cluster' do
- subject
+ describe 'security' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to redirect_to(new_project_cluster_path(project))
- end
+ def go
+ get :index, namespace_id: project.namespace.to_param, project_id: project
end
end
describe 'GET login' do
- render_views
+ let(:project) { create(:project) }
- subject do
- get :login, namespace_id: project.namespace,
- project_id: project
- end
-
- context 'when we do have omniauth configured' do
- it 'shows login button' do
- subject
+ describe 'functionality' do
+ let(:user) { create(:user) }
- expect(response.body).to include('auth_buttons/signin_with_google')
+ before do
+ project.add_master(user)
+ sign_in(user)
end
- end
- context 'when we do not have omniauth configured' do
- before do
- stub_omniauth_setting(providers: [])
+ context 'when omniauth has been configured' do
+ let(:key) { 'secere-key' }
+
+ let(:session_key_for_redirect_uri) do
+ GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
+ end
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(key)
+ end
+
+ it 'has authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to include(key)
+ expect(session[session_key_for_redirect_uri]).to eq(project_clusters_url(project))
+ end
end
- it 'shows notice message' do
- subject
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'does not have authorize_url' do
+ go
- expect(response.body).to include('Ask your GitLab administrator if you want to use this service.')
+ expect(assigns(:authorize_url)).to be_nil
+ end
end
end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :login, namespace_id: project.namespace, project_id: project
+ end
end
shared_examples 'requires to login' do
@@ -74,235 +114,335 @@ describe Projects::ClustersController do
end
describe 'GET new' do
- render_views
+ let(:project) { create(:project) }
- subject do
- get :new, namespace_id: project.namespace,
- project_id: project
- end
+ describe 'functionality' do
+ let(:user) { create(:user) }
- context 'when logged' do
before do
- make_logged_in
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'has new object' do
+ go
+
+ expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
+ end
end
- it 'shows a creation form' do
- subject
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(go).to redirect_to(login_project_clusters_path(project)) }
+ end
- expect(response.body).to include('Create cluster')
+ context 'when access token is not stored in session' do
+ it { expect(go).to redirect_to(login_project_clusters_path(project)) }
end
end
- context 'when not logged' do
- it_behaves_like 'requires to login'
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :new, namespace_id: project.namespace, project_id: project
end
end
describe 'POST create' do
- subject do
- post :create, params.merge(namespace_id: project.namespace,
- project_id: project)
+ let(:project) { create(:project) }
+
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: '111'
+ }
+ }
+ }
end
- context 'when not logged' do
- let(:params) { {} }
-
- it_behaves_like 'requires to login'
- end
+ describe 'functionality' do
+ let(:user) { create(:user) }
- context 'when logged in' do
before do
- make_logged_in
+ project.add_master(user)
+ sign_in(user)
end
- context 'when all required parameters are set' do
- let(:params) do
- {
- cluster: {
- gcp_cluster_name: 'new-cluster',
- gcp_project_id: '111'
- }
- }
- end
-
+ context 'when access token is valid' do
before do
- expect(ClusterProvisionWorker).to receive(:perform_async) { }
+ stub_google_api_validate_token
end
- it 'creates a new cluster' do
- expect { subject }.to change { Gcp::Cluster.count }
-
- expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ context 'when creates a cluster on gke' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ end
end
end
- context 'when not all required parameters are set' do
- render_views
-
- let(:params) do
- {
- cluster: {
- project_namespace: 'some namespace'
- }
- }
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
end
- it 'shows an error message' do
- expect { subject }.not_to change { Gcp::Cluster.count }
+ it 'redirects to login page' do
+ expect(go).to redirect_to(login_project_clusters_path(project))
+ end
+ end
- expect(response).to render_template(:new)
+ context 'when access token is not stored in session' do
+ it 'redirects to login page' do
+ expect(go).to redirect_to(login_project_clusters_path(project))
end
end
end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(namespace_id: project.namespace, project_id: project)
+ end
end
describe 'GET status' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:project) { cluster.project }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
- subject do
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it "responds with matching schema" do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('cluster_status')
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
get :status, namespace_id: project.namespace,
project_id: project,
id: cluster,
format: :json
end
-
- it "responds with matching schema" do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('cluster_status')
- end
end
describe 'GET show' do
- render_views
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ describe 'functionality' do
+ let(:user) { create(:user) }
- subject do
- get :show, namespace_id: project.namespace,
- project_id: project,
- id: cluster
- end
-
- context 'when logged as master' do
- it "allows to update cluster" do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to include("Save")
+ before do
+ project.add_master(user)
+ sign_in(user)
end
- it "allows remove integration" do
- subject
+ it "renders view" do
+ go
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to include("Remove integration")
+ expect(assigns(:cluster)).to eq(cluster)
end
end
- context 'when logged as developer' do
- let(:role) { :developer }
-
- it "does not allow to access page" do
- subject
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ def go
+ get :show, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
end
end
describe 'PUT update' do
- render_views
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- let(:service) { project.build_kubernetes_service }
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project, service: service) }
- let(:params) { {} }
+ describe 'functionality' do
+ let(:user) { create(:user) }
- subject do
- put :update, params.merge(namespace_id: project.namespace,
- project_id: project,
- id: cluster)
- end
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
- context 'when logged as master' do
- context 'when valid params are used' do
+ context 'when update enabled' do
let(:params) do
{
cluster: { enabled: false }
}
end
- it "redirects back to show page" do
- subject
+ it "updates and redirects back to show page" do
+ go
+ cluster.reload
expect(response).to redirect_to(project_cluster_path(project, project.cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
end
- end
- context 'when invalid params are used' do
- let(:params) do
- {
- cluster: { project_namespace: 'my Namespace 321321321 #' }
- }
- end
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
- it "rejects changes" do
- subject
+ it "rejects changes" do
+ go
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(cluster.enabled).to be_truthy
+ end
end
end
end
- context 'when logged as developer' do
- let(:role) { :developer }
+ describe 'security' do
+ let(:params) do
+ {
+ cluster: { enabled: false }
+ }
+ end
- it "does not allow to update cluster" do
- subject
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ def go
+ put :update, params.merge(namespace_id: project.namespace,
+ project_id: project,
+ id: cluster)
end
end
describe 'delete update' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- subject do
- delete :destroy, namespace_id: project.namespace,
- project_id: project,
- id: cluster
- end
+ describe 'functionality' do
+ let(:user) { create(:user) }
- context 'when logged as master' do
- it "redirects back to clusters list" do
- subject
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ 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(project_clusters_path(project))
expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
end
- end
- context 'when logged as developer' do
- let(:role) { :developer }
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+
+ 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(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
+ end
+
+ context 'when provider is user' do
+ let(:cluster) { create(:cluster, :project, :provided_by_user) }
- it "does not allow to destroy cluster" do
- subject
+ 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 have_gitlab_http_status(:not_found)
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
end
end
- end
- def make_logged_in
- session[GoogleApi::CloudPlatform::Client.session_key_for_token] = '1234'
- session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = in_hour.to_i.to_s
- end
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- def in_hour
- Time.now + 1.hour
+ def go
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
+ end
end
end
diff --git a/spec/factories/clusters/cluster.rb b/spec/factories/clusters/cluster.rb
new file mode 100644
index 00000000000..c4261178f2d
--- /dev/null
+++ b/spec/factories/clusters/cluster.rb
@@ -0,0 +1,39 @@
+FactoryGirl.define do
+ factory :cluster, class: Clusters::Cluster do
+ user
+ name 'test-cluster'
+
+ trait :project do
+ after(:create) do |cluster, evaluator|
+ cluster.projects << create(:project)
+ end
+ end
+
+ trait :provided_by_user do
+ provider_type :user
+ platform_type :kubernetes
+
+ platform_kubernetes do
+ create(:cluster_platform_kubernetes, :configured)
+ end
+ end
+
+ trait :provided_by_gcp do
+ provider_type :gcp
+ platform_type :kubernetes
+
+ before(:create) do |cluster, evaluator|
+ cluster.platform_kubernetes = build(:cluster_platform_kubernetes, :configured)
+ cluster.provider_gcp = build(:cluster_provider_gcp, :created)
+ end
+ end
+
+ trait :providing_by_gcp do
+ provider_type :gcp
+
+ provider_gcp do
+ create(:cluster_provider_gcp, :creating)
+ end
+ end
+ end
+end
diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb
new file mode 100644
index 00000000000..8b3e6ff35fa
--- /dev/null
+++ b/spec/factories/clusters/platforms/kubernetes.rb
@@ -0,0 +1,20 @@
+FactoryGirl.define do
+ factory :cluster_platform_kubernetes, class: Clusters::Platforms::Kubernetes do
+ cluster
+ namespace nil
+ api_url 'https://kubernetes.example.com'
+ token 'a' * 40
+
+ trait :configured do
+ api_url 'https://kubernetes.example.com'
+ token 'a' * 40
+ username 'xxxxxx'
+ password 'xxxxxx'
+
+ after(: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
+ end
+ end
+end
diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb
new file mode 100644
index 00000000000..a815410512a
--- /dev/null
+++ b/spec/factories/clusters/providers/gcp.rb
@@ -0,0 +1,32 @@
+FactoryGirl.define do
+ factory :cluster_provider_gcp, class: Clusters::Providers::Gcp do
+ cluster
+ gcp_project_id 'test-gcp-project'
+
+ trait :scheduled do
+ access_token 'access_token_123'
+ end
+
+ trait :creating do
+ access_token 'access_token_123'
+
+ after(:build) do |gcp, evaluator|
+ gcp.make_creating('operation-123')
+ end
+ end
+
+ trait :created do
+ endpoint '111.111.111.111'
+
+ after(:build) do |gcp, evaluator|
+ gcp.make_created
+ end
+ end
+
+ trait :errored do
+ after(:build) do |gcp, evaluator|
+ gcp.make_errored('Something wrong')
+ end
+ end
+ end
+end
diff --git a/spec/factories/gcp/cluster.rb b/spec/factories/gcp/cluster.rb
deleted file mode 100644
index 630e40da888..00000000000
--- a/spec/factories/gcp/cluster.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-FactoryGirl.define do
- factory :gcp_cluster, class: Gcp::Cluster do
- project
- user
- enabled true
- gcp_project_id 'gcp-project-12345'
- gcp_cluster_name 'test-cluster'
- gcp_cluster_zone 'us-central1-a'
- gcp_cluster_size 1
- gcp_machine_type 'n1-standard-4'
-
- trait :with_kubernetes_service do
- after(:create) do |cluster, evaluator|
- create(:kubernetes_service, project: cluster.project).tap do |service|
- cluster.update(service: service)
- end
- end
- end
-
- trait :custom_project_namespace do
- project_namespace 'sample-app'
- end
-
- trait :created_on_gke do
- status_event :make_created
- endpoint '111.111.111.111'
- ca_cert 'xxxxxx'
- kubernetes_token 'xxxxxx'
- username 'xxxxxx'
- password 'xxxxxx'
- end
-
- trait :errored do
- status_event :make_errored
- status_reason 'general error'
- end
- end
-end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 810f2c39b43..27c1f5062f5 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Clusters', :js do
+ include GoogleApi::CloudPlatformHelpers
+
let!(:project) { create(:project, :repository) }
let!(:user) { create(:user) }
@@ -11,8 +13,10 @@ feature 'Clusters', :js do
context 'when user has signed in Google' do
before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:validate_token).and_return(true)
+ allow_any_instance_of(Projects::ClustersController)
+ .to receive(:token_in_session).and_return('token')
+ allow_any_instance_of(Projects::ClustersController)
+ .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
end
context 'when user does not have a cluster and visits cluster index page' do
@@ -36,15 +40,15 @@ feature 'Clusters', :js do
allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
- fill_in 'cluster_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_gcp_cluster_name', with: 'dev-cluster'
+ fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
+ fill_in 'cluster_name', with: 'dev-cluster'
click_button 'Create cluster'
end
it 'user sees a cluster details page and creation status' do
expect(page).to have_content('Cluster is being created on Google Container Engine...')
- Gcp::Cluster.last.make_created!
+ Clusters::Cluster.last.provider.make_created!
expect(page).to have_content('Cluster was successfully created on Google Container Engine')
end
@@ -62,7 +66,8 @@ feature 'Clusters', :js do
end
context 'when user has a cluster and visits cluster index page' do
- let!(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service, project: project) }
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
before do
visit project_clusters_path(project)
@@ -70,7 +75,7 @@ feature 'Clusters', :js do
it 'user sees an cluster details page' do
expect(page).to have_button('Save')
- expect(page.find(:css, '.cluster-name').value).to eq(cluster.gcp_cluster_name)
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
end
context 'when user disables the cluster' do
diff --git a/spec/fixtures/clusters/sample_cert.pem b/spec/fixtures/clusters/sample_cert.pem
new file mode 100644
index 00000000000..e39a2b34416
--- /dev/null
+++ b/spec/fixtures/clusters/sample_cert.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFtTCCA52gAwIBAgIJAOutg3Kf2y5dMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTcxMDI5MTgxOTU3WhcNMTgxMDI5MTgxOTU3WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAvQysroM3TLxaavadSPnFIltrYnxCnU4PvCR8971HMWXsq7Z4ShU4BbbE
+8yp7oUFjulSwW6DhdIvnQb8ihLKictLmrA0isQqrD/iNpKZ6/lI4DGWw4QzrvMnW
+V4yy2QZNpg9tzQHd4+xkeeIoG23RijDU/sPd5dqxF+rPHBfCVInmYvSzLvMhneNj
+Bt6gV02gU9e9hsnMatsDvEbvWKp7wcbPot0nWrfZulx2QAWyXy+zG9mJQUds6yc0
+4agAeT9JEb/xtRgR/kS0aUHSGnfSnhZiEn17s0PhTmbu7qSHgzgB+7oJrC9jPoUh
+S2Wo3n0xykAjHrA8wC/Ddw3L38S41VQ58GEfNchistPswyMmXo/Oenv9P3s/kCOI
+fndiksFNdqVo51y9Vjngj589hpOseFDyKmWPIEQZ9kxW/crjP6RZWWLHgz26KtxZ
+uJaoYL8VBbYfrk/bucw0Ma2GEOp8rTsBE7SvgejXZa78q+381Kzc/utW6VwSXqzY
+xeIitft0rXi17SZ+XoiTkIXtHn0ZwMtOXNDBADTpFmKa6wVACQilvcpOYD8gUHyH
+pB+EDRdST3M4Fiq1MBAVhk8Lj3tHSJ/1ymeF1PWSu57AnJlzerzq2fcfPotNNd37
+ZPNkPh0kxPLwxbAyrHflzx9qVVdI1irY9055mNSnhzlec4qJ9cECAwEAAaOBpzCB
+pDAdBgNVHQ4EFgQUnVa5dYPoIG/3+qXml0bX8+N16GwwdQYDVR0jBG4wbIAUnVa5
+dYPoIG/3+qXml0bX8+N16GyhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
+b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDr
+rYNyn9suXTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQAUg4cyxXi1
+VR8ejTpaAruRyJ1pEG9Kc3kiIRXODy60z3hJXnx9LkScPkWGiuL5XacfZ2rMd4bw
+oVXIyi8U1UHWfAH8EZdrFKkU92jCiL5soHUONxLAvQEJ/FTR/qijrpzLCxXBdVQE
+xFEDWUu6rxLFyjEwzwnRTLgpjR606fdb7qXHkuAMvZ/ezJj8j97hok3Odpn4lr2H
+6hMTpK7HmDBX+kmdJJ+yBrm9hG1Pzpl7QU0dkxZ+qJNFjYMLnziiTwkv0c5ZaA9E
+NykZUcOv3Sjb6spu1A/E2BSq4WTjkIjrogFlfimE1vmUmObTRJOqUB0Vky1kHEwN
+pg7QqIJQmof1EAIaSM/YpUWXyumBwGLDUEud1JUz05In9Q4IZjEwZSJwbQW4fUia
+A93m9rk3Lw3xsFcaUdPMFIXk0rPoF1IgmV/oqb0gK95lOWRLbN+AV8qpKPpcKXOc
+TkIdFE47ZisEDhIdF6wC1izEMLeMEsPAO7/Y6MY4nRxsinSe95lRaw+yQpzx+mvJ
+Q7n1kiHI9Pd5M3+CiQda0d/GO1o5ORJnUGJRvr9HKuNmE7Lif0As/N0AlywjzE7A
+6Z8AEiWyRV1ffshu1k2UKmzvZuZeGGKRtrIjbJIRAtpRVtVZZGzhq5/sojCLoJ+u
+texqFBUo/4mFRZa4pDItUdyOlDy2/LO/ag==
+-----END CERTIFICATE-----
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
index 5774f36f026..8e74c4f859c 100644
--- a/spec/javascripts/fixtures/clusters.rb
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -6,7 +6,7 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace) }
- let(:cluster) { project.create_cluster!(gcp_cluster_name: "gke-test-creation-1", gcp_project_id: 'gitlab-internal-153318', gcp_cluster_zone: 'us-central1-a', gcp_cluster_size: '1', project_namespace: 'aaa', gcp_machine_type: 'n1-standard-1')}
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
render_views
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 6c6b9154a0a..96efdd0949b 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -148,9 +148,18 @@ deploy_keys:
- deploy_keys_projects
- projects
cluster:
-- project
+- cluster_projects
+- projects
- user
-- service
+- provider_gcp
+- platform_kubernetes
+cluster_projects:
+- projects
+- clusters
+provider_gcp:
+- cluster
+platform_kubernetes:
+- cluster
services:
- project
- service_hook
@@ -182,6 +191,7 @@ project:
- tags
- chat_services
- cluster
+- cluster_project
- creator
- group
- namespace
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 89d30407077..4b79e9f18c6 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -313,30 +313,47 @@ Ci::PipelineSchedule:
- deleted_at
- created_at
- updated_at
-Gcp::Cluster:
+Clusters::Cluster:
- id
-- project_id
- user_id
-- service_id
- enabled
+- name
+- provider_type
+- platform_type
+- created_at
+- updated_at
+Clusters::Project:
+- id
+- project_id
+- cluster_id
+- created_at
+- updated_at
+Clusters::Providers::Gcp:
+- id
+- cluster_id
- status
- status_reason
-- project_namespace
+- gcp_project_id
+- zone
+- num_nodes
+- machine_type
+- operation_id
- endpoint
+- encrypted_access_token
+- encrypted_access_token_iv
+- created_at
+- updated_at
+Clusters::Platforms::Kubernetes:
+- id
+- cluster_id
+- api_url
- ca_cert
-- encrypted_kubernetes_token
-- encrypted_kubernetes_token_iv
+- namespace
- username
- encrypted_password
- encrypted_password_iv
-- gcp_project_id
-- gcp_cluster_zone
-- gcp_cluster_name
-- gcp_cluster_size
-- gcp_machine_type
-- gcp_operation_id
-- encrypted_gcp_token
-- encrypted_gcp_token_iv
+- encrypted_token
+- encrypted_token_iv
- created_at
- updated_at
DeployKey:
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index a7b65e94706..a4c1113ae37 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -60,9 +60,9 @@ describe Gitlab::UsageData do
deploy_keys
deployments
environments
- gcp_clusters
- gcp_clusters_enabled
- gcp_clusters_disabled
+ clusters
+ clusters_enabled
+ clusters_disabled
in_review_folder
groups
issues
diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
new file mode 100644
index 00000000000..9f41534441b
--- /dev/null
+++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
@@ -0,0 +1,166 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb')
+
+describe MigrateGcpClustersToNewClustersArchitectures, :migration do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { create(:kubernetes_service, project: project) }
+
+ context 'when cluster is being created' do
+ let(:project_id) { project.id }
+ let(:user_id) { user.id }
+ let(:service_id) { service.id }
+ let(:status) { 2 } # creating
+ let(:gcp_cluster_size) { 1 }
+ let(:created_at) { "'2017-10-17 20:24:02'" }
+ let(:updated_at) { "'2017-10-17 20:28:44'" }
+ let(:enabled) { true }
+ let(:status_reason) { "''" }
+ let(:project_namespace) { "'sample-app'" }
+ let(:endpoint) { 'NULL' }
+ let(:ca_cert) { 'NULL' }
+ let(:encrypted_kubernetes_token) { 'NULL' }
+ let(:encrypted_kubernetes_token_iv) { 'NULL' }
+ let(:username) { 'NULL' }
+ let(:encrypted_password) { 'NULL' }
+ let(:encrypted_password_iv) { 'NULL' }
+ let(:gcp_project_id) { "'gcp_project_id'" }
+ let(:gcp_cluster_zone) { "'gcp_cluster_zone'" }
+ let(:gcp_cluster_name) { "'gcp_cluster_name'" }
+ let(:gcp_machine_type) { "'gcp_machine_type'" }
+ let(:gcp_operation_id) { 'NULL' }
+ let(:encrypted_gcp_token) { "'encrypted_gcp_token'" }
+ let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" }
+
+ let(:cluster) { Clusters::Cluster.last }
+ let(:cluster_id) { cluster.id }
+
+ before do
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO gcp_clusters (project_id, user_id, service_id, status, gcp_cluster_size, created_at, updated_at, enabled, status_reason, project_namespace, endpoint, ca_cert, encrypted_kubernetes_token, encrypted_kubernetes_token_iv, username, encrypted_password, encrypted_password_iv, gcp_project_id, gcp_cluster_zone, gcp_cluster_name, gcp_machine_type, gcp_operation_id, encrypted_gcp_token, encrypted_gcp_token_iv)
+ VALUES (#{project_id}, #{user_id}, #{service_id}, #{status}, #{gcp_cluster_size}, #{created_at}, #{updated_at}, #{enabled}, #{status_reason}, #{project_namespace}, #{endpoint}, #{ca_cert}, #{encrypted_kubernetes_token}, #{encrypted_kubernetes_token_iv}, #{username}, #{encrypted_password}, #{encrypted_password_iv}, #{gcp_project_id}, #{gcp_cluster_zone}, #{gcp_cluster_name}, #{gcp_machine_type}, #{gcp_operation_id}, #{encrypted_gcp_token}, #{encrypted_gcp_token_iv});
+ SQL
+ end
+
+ it 'correctly migrate to new clusters architectures' do
+ migrate!
+
+ expect(Clusters::Cluster.count).to eq(1)
+ expect(Clusters::Project.count).to eq(1)
+ expect(Clusters::Providers::Gcp.count).to eq(1)
+ expect(Clusters::Platforms::Kubernetes.count).to eq(1)
+
+ expect(cluster.user).to eq(user)
+ expect(cluster.enabled).to be_truthy
+ expect(cluster.name).to eq(gcp_cluster_name.delete!("'"))
+ expect(cluster.provider_type).to eq('gcp')
+ expect(cluster.platform_type).to eq('kubernetes')
+
+ expect(cluster.project).to eq(project)
+ expect(project.cluster).to eq(cluster)
+
+ expect(cluster.provider_gcp.cluster).to eq(cluster)
+ expect(cluster.provider_gcp.status).to eq(status)
+ expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason))
+ expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id))
+ expect(cluster.provider_gcp.zone).to eq(tr(gcp_cluster_zone))
+ expect(cluster.provider_gcp.num_nodes).to eq(gcp_cluster_size)
+ expect(cluster.provider_gcp.machine_type).to eq(tr(gcp_machine_type))
+ expect(cluster.provider_gcp.operation_id).to be_nil
+ expect(cluster.provider_gcp.endpoint).to be_nil
+ expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token))
+ expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv))
+
+ expect(cluster.platform_kubernetes.cluster).to eq(cluster)
+ expect(cluster.platform_kubernetes.api_url).to be_nil
+ expect(cluster.platform_kubernetes.ca_cert).to be_nil
+ expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace))
+ expect(cluster.platform_kubernetes.username).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_password).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_password_iv).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_token).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_token_iv).to be_nil
+ end
+ end
+
+ context 'when cluster has been created' do
+ let(:project_id) { project.id }
+ let(:user_id) { user.id }
+ let(:service_id) { service.id }
+ let(:status) { 3 } # created
+ let(:gcp_cluster_size) { 1 }
+ let(:created_at) { "'2017-10-17 20:24:02'" }
+ let(:updated_at) { "'2017-10-17 20:28:44'" }
+ let(:enabled) { true }
+ let(:status_reason) { "'general error'" }
+ let(:project_namespace) { "'sample-app'" }
+ let(:endpoint) { "'111.111.111.111'" }
+ let(:ca_cert) { "'ca_cert'" }
+ let(:encrypted_kubernetes_token) { "'encrypted_kubernetes_token'" }
+ let(:encrypted_kubernetes_token_iv) { "'encrypted_kubernetes_token_iv'" }
+ let(:username) { "'username'" }
+ let(:encrypted_password) { "'encrypted_password'" }
+ let(:encrypted_password_iv) { "'encrypted_password_iv'" }
+ let(:gcp_project_id) { "'gcp_project_id'" }
+ let(:gcp_cluster_zone) { "'gcp_cluster_zone'" }
+ let(:gcp_cluster_name) { "'gcp_cluster_name'" }
+ let(:gcp_machine_type) { "'gcp_machine_type'" }
+ let(:gcp_operation_id) { "'gcp_operation_id'" }
+ let(:encrypted_gcp_token) { "'encrypted_gcp_token'" }
+ let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" }
+
+ let(:cluster) { Clusters::Cluster.last }
+ let(:cluster_id) { cluster.id }
+
+ before do
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO gcp_clusters (project_id, user_id, service_id, status, gcp_cluster_size, created_at, updated_at, enabled, status_reason, project_namespace, endpoint, ca_cert, encrypted_kubernetes_token, encrypted_kubernetes_token_iv, username, encrypted_password, encrypted_password_iv, gcp_project_id, gcp_cluster_zone, gcp_cluster_name, gcp_machine_type, gcp_operation_id, encrypted_gcp_token, encrypted_gcp_token_iv)
+ VALUES (#{project_id}, #{user_id}, #{service_id}, #{status}, #{gcp_cluster_size}, #{created_at}, #{updated_at}, #{enabled}, #{status_reason}, #{project_namespace}, #{endpoint}, #{ca_cert}, #{encrypted_kubernetes_token}, #{encrypted_kubernetes_token_iv}, #{username}, #{encrypted_password}, #{encrypted_password_iv}, #{gcp_project_id}, #{gcp_cluster_zone}, #{gcp_cluster_name}, #{gcp_machine_type}, #{gcp_operation_id}, #{encrypted_gcp_token}, #{encrypted_gcp_token_iv});
+ SQL
+ end
+
+ it 'correctly migrate to new clusters architectures' do
+ migrate!
+
+ expect(Clusters::Cluster.count).to eq(1)
+ expect(Clusters::Project.count).to eq(1)
+ expect(Clusters::Providers::Gcp.count).to eq(1)
+ expect(Clusters::Platforms::Kubernetes.count).to eq(1)
+
+ expect(cluster.user).to eq(user)
+ expect(cluster.enabled).to be_truthy
+ expect(cluster.name).to eq(tr(gcp_cluster_name))
+ expect(cluster.provider_type).to eq('gcp')
+ expect(cluster.platform_type).to eq('kubernetes')
+
+ expect(cluster.project).to eq(project)
+ expect(project.cluster).to eq(cluster)
+
+ expect(cluster.provider_gcp.cluster).to eq(cluster)
+ expect(cluster.provider_gcp.status).to eq(status)
+ expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason))
+ expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id))
+ expect(cluster.provider_gcp.zone).to eq(tr(gcp_cluster_zone))
+ expect(cluster.provider_gcp.num_nodes).to eq(gcp_cluster_size)
+ expect(cluster.provider_gcp.machine_type).to eq(tr(gcp_machine_type))
+ expect(cluster.provider_gcp.operation_id).to eq(tr(gcp_operation_id))
+ expect(cluster.provider_gcp.endpoint).to eq(tr(endpoint))
+ expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token))
+ expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv))
+
+ expect(cluster.platform_kubernetes.cluster).to eq(cluster)
+ expect(cluster.platform_kubernetes.api_url).to eq('https://' + tr(endpoint))
+ expect(cluster.platform_kubernetes.ca_cert).to eq(tr(ca_cert))
+ expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace))
+ expect(cluster.platform_kubernetes.username).to eq(tr(username))
+ expect(cluster.platform_kubernetes.encrypted_password).to eq(tr(encrypted_password))
+ expect(cluster.platform_kubernetes.encrypted_password_iv).to eq(tr(encrypted_password_iv))
+ expect(cluster.platform_kubernetes.encrypted_token).to eq(tr(encrypted_kubernetes_token))
+ expect(cluster.platform_kubernetes.encrypted_token_iv).to eq(tr(encrypted_kubernetes_token_iv))
+ end
+ end
+
+ def tr(s)
+ s.delete("'")
+ end
+end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
new file mode 100644
index 00000000000..12123a3d753
--- /dev/null
+++ b/spec/models/clusters/cluster_spec.rb
@@ -0,0 +1,181 @@
+require 'spec_helper'
+
+describe Clusters::Cluster do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:projects) }
+ it { is_expected.to have_one(:provider_gcp) }
+ it { is_expected.to have_one(:platform_kubernetes) }
+ it { is_expected.to delegate_method(:status).to(:provider) }
+ it { is_expected.to delegate_method(:status_reason).to(:provider) }
+ it { is_expected.to delegate_method(:status_name).to(:provider) }
+ it { is_expected.to delegate_method(:on_creation?).to(:provider) }
+ it { is_expected.to delegate_method(:update_kubernetes_integration!).to(:platform) }
+ it { is_expected.to respond_to :project }
+
+ describe '.enabled' do
+ subject { described_class.enabled }
+
+ let!(:cluster) { create(:cluster, enabled: true) }
+
+ before do
+ create(:cluster, enabled: false)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '.disabled' do
+ subject { described_class.disabled }
+
+ let!(:cluster) { create(:cluster, enabled: false) }
+
+ before do
+ create(:cluster, enabled: true)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe 'validation' do
+ subject { cluster.valid? }
+
+ context 'when validates name' do
+ context 'when provided by user' do
+ let!(:cluster) { build(:cluster, :provided_by_user, name: name) }
+
+ context 'when name is empty' do
+ let(:name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is nil' do
+ let(:name) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is present' do
+ let(:name) { 'cluster-name-1' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when provided by gcp' do
+ let!(:cluster) { build(:cluster, :provided_by_gcp, name: name) }
+
+ context 'when name is shorter than 1' do
+ let(:name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is longer than 63' do
+ let(:name) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name includes invalid character' do
+ let(:name) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is present' do
+ let(:name) { 'cluster-name-1' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when record is persisted' do
+ let(:name) { 'cluster-name-1' }
+
+ before do
+ cluster.save!
+ end
+
+ context 'when name is changed' do
+ before do
+ cluster.name = 'new-cluster-name'
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is same' do
+ before do
+ cluster.name = name
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+ end
+
+ context 'when validates restrict_modification' do
+ context 'when creation is on going' do
+ let!(:cluster) { create(:cluster, :providing_by_gcp) }
+
+ it { expect(cluster.update(enabled: false)).to be_falsey }
+ end
+
+ context 'when creation is done' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it { expect(cluster.update(enabled: false)).to be_truthy }
+ end
+ end
+ end
+
+ describe '#provider' do
+ subject { cluster.provider }
+
+ context 'when provider is gcp' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it 'returns a provider' do
+ is_expected.to eq(cluster.provider_gcp)
+ expect(subject.class.name.deconstantize).to eq(Clusters::Providers.to_s)
+ end
+ end
+
+ context 'when provider is user' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#platform' do
+ subject { cluster.platform }
+
+ context 'when platform is kubernetes' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ it 'returns a platform' do
+ is_expected.to eq(cluster.platform_kubernetes)
+ expect(subject.class.name.deconstantize).to eq(Clusters::Platforms.to_s)
+ end
+ end
+ end
+
+ describe '#first_project' do
+ subject { cluster.first_project }
+
+ context 'when cluster belongs to a project' do
+ let(:cluster) { create(:cluster, :project) }
+ let(:project) { Clusters::Project.find_by_cluster_id(cluster.id).project }
+
+ it { is_expected.to eq(project) }
+ end
+
+ context 'when cluster does not belong to projects' do
+ let(:cluster) { create(:cluster) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
new file mode 100644
index 00000000000..ed76be703a5
--- /dev/null
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -0,0 +1,188 @@
+require 'spec_helper'
+
+describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to respond_to :ca_pem }
+
+ describe 'before_validation' do
+ context 'when namespace includes upper case' do
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+ let(:namespace) { 'ABC' }
+
+ it 'converts to lower case' do
+ expect(kubernetes.namespace).to eq('abc')
+ end
+ end
+ end
+
+ describe 'validation' do
+ subject { kubernetes.valid? }
+
+ context 'when validates namespace' do
+ let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+
+ context 'when namespace is blank' do
+ let(:namespace) { '' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when namespace is longer than 63' do
+ let(:namespace) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when namespace includes invalid character' do
+ let(:namespace) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when namespace is vaild' do
+ let(:namespace) { 'namespace-123' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when validates api_url' do
+ let(:kubernetes) { build(:cluster_platform_kubernetes, :configured) }
+
+ before do
+ kubernetes.api_url = api_url
+ end
+
+ context 'when api_url is invalid url' do
+ let(:api_url) { '!!!!!!' }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+
+ context 'when api_url is nil' do
+ let(:api_url) { nil }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+
+ context 'when api_url is valid url' do
+ let(:api_url) { 'https://111.111.111.111' }
+
+ it { expect(kubernetes.save).to be_truthy }
+ end
+ end
+
+ context 'when validates token' do
+ let(:kubernetes) { build(:cluster_platform_kubernetes, :configured) }
+
+ before do
+ kubernetes.token = token
+ end
+
+ context 'when token is nil' do
+ let(:token) { nil }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+ end
+ end
+
+ describe 'after_save from Clusters::Cluster' do
+ context 'when platform_kubernetes is being cerated' do
+ let(:enabled) { true }
+ let(:project) { create(:project) }
+ let(:cluster) { build(:cluster, provider_type: :gcp, platform_type: :kubernetes, platform_kubernetes: platform, provider_gcp: provider, enabled: enabled, projects: [project]) }
+ let(:platform) { build(:cluster_platform_kubernetes, :configured) }
+ let(:provider) { build(:cluster_provider_gcp) }
+ let(:kubernetes_service) { project.kubernetes_service }
+
+ it 'updates KubernetesService' do
+ cluster.save!
+
+ expect(kubernetes_service.active).to eq(enabled)
+ expect(kubernetes_service.api_url).to eq(platform.api_url)
+ expect(kubernetes_service.namespace).to eq(platform.namespace)
+ expect(kubernetes_service.ca_pem).to eq(platform.ca_cert)
+ end
+ end
+
+ context 'when platform_kubernetes has been created' do
+ let(:enabled) { false }
+ let!(:project) { create(:project) }
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let(:platform) { cluster.platform }
+ let(:kubernetes_service) { project.kubernetes_service }
+
+ it 'updates KubernetesService' do
+ cluster.update(enabled: enabled)
+
+ expect(kubernetes_service.active).to eq(enabled)
+ end
+ end
+
+ context 'when kubernetes_service has been configured without cluster integration' do
+ let!(:project) { create(:project) }
+ let(:cluster) { build(:cluster, provider_type: :gcp, platform_type: :kubernetes, platform_kubernetes: platform, provider_gcp: provider, projects: [project]) }
+ let(:platform) { build(:cluster_platform_kubernetes, :configured, api_url: 'https://111.111.111.111') }
+ let(:provider) { build(:cluster_provider_gcp) }
+
+ before do
+ create(:kubernetes_service, project: project)
+ end
+
+ it 'raises an error' do
+ expect { cluster.save! }.to raise_error('Kubernetes service already configured')
+ end
+ end
+ end
+
+ describe '#actual_namespace' do
+ subject { kubernetes.actual_namespace }
+
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+
+ context 'when namespace is present' do
+ let(:namespace) { 'namespace-123' }
+
+ it { is_expected.to eq(namespace) }
+ end
+
+ context 'when namespace is not present' do
+ let(:namespace) { nil }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+ end
+
+ describe '.namespace_for_project' do
+ subject { described_class.namespace_for_project(project) }
+
+ let(:project) { create(:project) }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+
+ describe '#default_namespace' do
+ subject { kubernetes.default_namespace }
+
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured) }
+
+ context 'when cluster belongs to a project' do
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+
+ context 'when cluster belongs to nothing' do
+ let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/models/clusters/project_spec.rb b/spec/models/clusters/project_spec.rb
new file mode 100644
index 00000000000..7d75d6ab345
--- /dev/null
+++ b/spec/models/clusters/project_spec.rb
@@ -0,0 +1,6 @@
+require 'spec_helper'
+
+describe Clusters::Project do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to belong_to(:project) }
+end
diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb
new file mode 100644
index 00000000000..ecd0a08c953
--- /dev/null
+++ b/spec/models/clusters/providers/gcp_spec.rb
@@ -0,0 +1,183 @@
+require 'spec_helper'
+
+describe Clusters::Providers::Gcp do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to validate_presence_of(:zone) }
+
+ describe 'default_value_for' do
+ let(:gcp) { build(:cluster_provider_gcp) }
+
+ it "has default value" do
+ expect(gcp.zone).to eq('us-central1-a')
+ expect(gcp.num_nodes).to eq(3)
+ expect(gcp.machine_type).to eq('n1-standard-4')
+ end
+ end
+
+ describe 'validation' do
+ subject { gcp.valid? }
+
+ context 'when validates gcp_project_id' do
+ let(:gcp) { build(:cluster_provider_gcp, gcp_project_id: gcp_project_id) }
+
+ context 'when gcp_project_id is shorter than 1' do
+ let(:gcp_project_id) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id is longer than 63' do
+ let(:gcp_project_id) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id includes invalid character' do
+ let(:gcp_project_id) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id is valid' do
+ let(:gcp_project_id) { 'gcp-project-1' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when validates num_nodes' do
+ let(:gcp) { build(:cluster_provider_gcp, num_nodes: num_nodes) }
+
+ context 'when num_nodes is string' do
+ let(:num_nodes) { 'A3' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is nil' do
+ let(:num_nodes) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is smaller than 1' do
+ let(:num_nodes) { 0 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is valid' do
+ let(:num_nodes) { 3 }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ describe '#state_machine' do
+ context 'when any => [:created]' do
+ let(:gcp) { build(:cluster_provider_gcp, :creating) }
+
+ before do
+ gcp.make_created
+ end
+
+ it 'nullify access_token and operation_id' do
+ expect(gcp.access_token).to be_nil
+ expect(gcp.operation_id).to be_nil
+ expect(gcp).to be_created
+ end
+ end
+
+ context 'when any => [:creating]' do
+ let(:gcp) { build(:cluster_provider_gcp) }
+
+ context 'when operation_id is present' do
+ let(:operation_id) { 'operation-xxx' }
+
+ before do
+ gcp.make_creating(operation_id)
+ end
+
+ it 'sets operation_id' do
+ expect(gcp.operation_id).to eq(operation_id)
+ expect(gcp).to be_creating
+ end
+ end
+
+ context 'when operation_id is nil' do
+ let(:operation_id) { nil }
+
+ it 'raises an error' do
+ expect { gcp.make_creating(operation_id) }
+ .to raise_error('operation_id is required')
+ end
+ end
+ end
+
+ context 'when any => [:errored]' do
+ let(:gcp) { build(:cluster_provider_gcp, :creating) }
+ let(:status_reason) { 'err msg' }
+
+ it 'nullify access_token and operation_id' do
+ gcp.make_errored(status_reason)
+
+ expect(gcp.access_token).to be_nil
+ expect(gcp.operation_id).to be_nil
+ expect(gcp.status_reason).to eq(status_reason)
+ expect(gcp).to be_errored
+ end
+
+ context 'when status_reason is nil' do
+ let(:gcp) { build(:cluster_provider_gcp, :errored) }
+
+ it 'does not set status_reason' do
+ gcp.make_errored(nil)
+
+ expect(gcp.status_reason).not_to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#on_creation?' do
+ subject { gcp.on_creation? }
+
+ context 'when status is creating' do
+ let(:gcp) { create(:cluster_provider_gcp, :creating) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when status is created' do
+ let(:gcp) { create(:cluster_provider_gcp, :created) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#api_client' do
+ subject { gcp.api_client }
+
+ context 'when status is creating' do
+ let(:gcp) { build(:cluster_provider_gcp, :creating) }
+
+ it 'returns Cloud Platform API clinet' do
+ expect(subject).to be_an_instance_of(GoogleApi::CloudPlatform::Client)
+ expect(subject.access_token).to eq(gcp.access_token)
+ end
+ end
+
+ context 'when status is created' do
+ let(:gcp) { build(:cluster_provider_gcp, :created) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when status is errored' do
+ let(:gcp) { build(:cluster_provider_gcp, :errored) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/models/gcp/cluster_spec.rb b/spec/models/gcp/cluster_spec.rb
deleted file mode 100644
index 8f39fff6394..00000000000
--- a/spec/models/gcp/cluster_spec.rb
+++ /dev/null
@@ -1,264 +0,0 @@
-require 'spec_helper'
-
-describe Gcp::Cluster do
- it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:user) }
- it { is_expected.to belong_to(:service) }
-
- it { is_expected.to validate_presence_of(:gcp_cluster_zone) }
-
- describe '.enabled' do
- subject { described_class.enabled }
-
- let!(:cluster) { create(:gcp_cluster, enabled: true) }
-
- before do
- create(:gcp_cluster, enabled: false)
- end
-
- it { is_expected.to contain_exactly(cluster) }
- end
-
- describe '.disabled' do
- subject { described_class.disabled }
-
- let!(:cluster) { create(:gcp_cluster, enabled: false) }
-
- before do
- create(:gcp_cluster, enabled: true)
- end
-
- it { is_expected.to contain_exactly(cluster) }
- end
-
- describe '#default_value_for' do
- let(:cluster) { described_class.new }
-
- it { expect(cluster.gcp_cluster_zone).to eq('us-central1-a') }
- it { expect(cluster.gcp_cluster_size).to eq(3) }
- it { expect(cluster.gcp_machine_type).to eq('n1-standard-4') }
- end
-
- describe '#validates' do
- subject { cluster.valid? }
-
- context 'when validates gcp_project_id' do
- let(:cluster) { build(:gcp_cluster, gcp_project_id: gcp_project_id) }
-
- context 'when valid' do
- let(:gcp_project_id) { 'gcp-project-12345' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:gcp_project_id) { '' }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when too long' do
- let(:gcp_project_id) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:gcp_project_id) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates gcp_cluster_name' do
- let(:cluster) { build(:gcp_cluster, gcp_cluster_name: gcp_cluster_name) }
-
- context 'when valid' do
- let(:gcp_cluster_name) { 'test-cluster' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:gcp_cluster_name) { '' }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when too long' do
- let(:gcp_cluster_name) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:gcp_cluster_name) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates gcp_cluster_size' do
- let(:cluster) { build(:gcp_cluster, gcp_cluster_size: gcp_cluster_size) }
-
- context 'when valid' do
- let(:gcp_cluster_size) { 1 }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when zero' do
- let(:gcp_cluster_size) { 0 }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates project_namespace' do
- let(:cluster) { build(:gcp_cluster, project_namespace: project_namespace) }
-
- context 'when valid' do
- let(:project_namespace) { 'default-namespace' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:project_namespace) { '' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when too long' do
- let(:project_namespace) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:project_namespace) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates restrict_modification' do
- let(:cluster) { create(:gcp_cluster) }
-
- before do
- cluster.make_creating!
- end
-
- context 'when created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when creating' do
- it { is_expected.to be_falsey }
- end
- end
- end
-
- describe '#state_machine' do
- let(:cluster) { build(:gcp_cluster) }
-
- context 'when transits to created state' do
- before do
- cluster.gcp_token = 'tmp'
- cluster.gcp_operation_id = 'tmp'
- cluster.make_created!
- end
-
- it 'nullify gcp_token and gcp_operation_id' do
- expect(cluster.gcp_token).to be_nil
- expect(cluster.gcp_operation_id).to be_nil
- expect(cluster).to be_created
- end
- end
-
- context 'when transits to errored state' do
- let(:reason) { 'something wrong' }
-
- before do
- cluster.make_errored!(reason)
- end
-
- it 'sets status_reason' do
- expect(cluster.status_reason).to eq(reason)
- expect(cluster).to be_errored
- end
- end
- end
-
- describe '#project_namespace_placeholder' do
- subject { cluster.project_namespace_placeholder }
-
- let(:cluster) { create(:gcp_cluster) }
-
- it 'returns a placeholder' do
- is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}")
- end
- end
-
- describe '#on_creation?' do
- subject { cluster.on_creation? }
-
- let(:cluster) { create(:gcp_cluster) }
-
- context 'when status is creating' do
- before do
- cluster.make_creating!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when status is created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#api_url' do
- subject { cluster.api_url }
-
- let(:cluster) { create(:gcp_cluster, :created_on_gke) }
- let(:api_url) { 'https://' + cluster.endpoint }
-
- it { is_expected.to eq(api_url) }
- end
-
- describe '#restrict_modification' do
- subject { cluster.restrict_modification }
-
- let(:cluster) { create(:gcp_cluster) }
-
- context 'when status is created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when status is creating' do
- before do
- cluster.make_creating!
- end
-
- it { is_expected.to be_falsey }
-
- it 'sets error' do
- is_expected.to be_falsey
- expect(cluster.errors).not_to be_empty
- end
- end
- end
-end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 00de536a18b..7617e1f89b1 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -145,7 +145,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
let(:discovery_url) { 'https://kubernetes.example.com/api/v1' }
before do
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
end
context 'with path prefix in api_url' do
@@ -153,7 +153,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
it 'tests with the prefix' do
service.api_url = 'https://kubernetes.example.com/prefix'
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once
diff --git a/spec/policies/gcp/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
index e213aa3d557..4207f42b07f 100644
--- a/spec/policies/gcp/cluster_policy_spec.rb
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
-describe Gcp::ClusterPolicy, :models do
- set(:project) { create(:project) }
- set(:cluster) { create(:gcp_cluster, project: project) }
+describe Clusters::ClusterPolicy, :models do
+ let(:cluster) { create(:cluster, :project) }
+ let(:project) { cluster.project }
let(:user) { create(:user) }
let(:policy) { described_class.new(user, cluster) }
diff --git a/spec/presenters/gcp/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 8d86dc31582..48d4f3671c5 100644
--- a/spec/presenters/gcp/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -1,8 +1,7 @@
require 'spec_helper'
-describe Gcp::ClusterPresenter do
- let(:project) { create(:project) }
- let(:cluster) { create(:gcp_cluster, project: project) }
+describe Clusters::ClusterPresenter do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
subject(:presenter) do
described_class.new(cluster)
@@ -22,14 +21,14 @@ describe Gcp::ClusterPresenter do
end
it 'forwards missing methods to cluster' do
- expect(presenter.gcp_cluster_zone).to eq(cluster.gcp_cluster_zone)
+ expect(presenter.status).to eq(cluster.status)
end
end
describe '#gke_cluster_url' do
subject { described_class.new(cluster).gke_cluster_url }
- it { is_expected.to include(cluster.gcp_cluster_zone) }
- it { is_expected.to include(cluster.gcp_cluster_name) }
+ it { is_expected.to include(cluster.provider.zone) }
+ it { is_expected.to include(cluster.name) }
end
end
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
index 2c7f49974f1..72f02131211 100644
--- a/spec/serializers/cluster_entity_spec.rb
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -1,22 +1,38 @@
require 'spec_helper'
describe ClusterEntity do
- set(:cluster) { create(:gcp_cluster, :errored) }
- let(:request) { double('request') }
+ describe '#as_json' do
+ subject { described_class.new(cluster).as_json }
- let(:entity) do
- described_class.new(cluster)
- end
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
- describe '#as_json' do
- subject { entity.as_json }
+ context 'when status is creating' do
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
- it 'contains status' do
- expect(subject[:status]).to eq(:errored)
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:creating)
+ expect(subject[:status_reason]).to be_nil
+ end
+ end
+
+ context 'when status is errored' do
+ let(:provider) { create(:cluster_provider_gcp, :errored) }
+
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:errored)
+ expect(subject[:status_reason]).to eq(provider.status_reason)
+ end
+ end
end
- it 'contains status reason' do
- expect(subject[:status_reason]).to eq('general error')
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
+
+ it 'has nil' do
+ expect(subject[:status]).to be_nil
+ expect(subject[:status_reason]).to be_nil
+ end
end
end
end
diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb
index 1ac6784d28f..ff7d1789149 100644
--- a/spec/serializers/cluster_serializer_spec.rb
+++ b/spec/serializers/cluster_serializer_spec.rb
@@ -1,15 +1,20 @@
require 'spec_helper'
describe ClusterSerializer do
- let(:serializer) do
- described_class.new
- end
-
describe '#represent_status' do
- subject { serializer.represent_status(resource) }
+ subject { described_class.new.represent_status(cluster) }
+
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:cluster_provider_gcp, :errored) }
+
+ it 'serializes only status' do
+ expect(subject.keys).to contain_exactly(:status, :status_reason)
+ end
+ end
- context 'when represents only status' do
- let(:resource) { create(:gcp_cluster, :errored) }
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
it 'serializes only status' do
expect(subject.keys).to contain_exactly(:status, :status_reason)
diff --git a/spec/services/ci/create_cluster_service_spec.rb b/spec/services/ci/create_cluster_service_spec.rb
deleted file mode 100644
index 6e7398fbffa..00000000000
--- a/spec/services/ci/create_cluster_service_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'spec_helper'
-
-describe Ci::CreateClusterService do
- describe '#execute' do
- let(:access_token) { 'xxx' }
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:result) { described_class.new(project, user, params).execute(access_token) }
-
- context 'when correct params' do
- let(:params) do
- {
- gcp_project_id: 'gcp-project',
- gcp_cluster_name: 'test-cluster',
- gcp_cluster_zone: 'us-central1-a',
- gcp_cluster_size: 1
- }
- end
-
- it 'creates a cluster object' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
- expect { result }.to change { Gcp::Cluster.count }.by(1)
- expect(result.gcp_project_id).to eq('gcp-project')
- expect(result.gcp_cluster_name).to eq('test-cluster')
- expect(result.gcp_cluster_zone).to eq('us-central1-a')
- expect(result.gcp_cluster_size).to eq(1)
- expect(result.gcp_token).to eq(access_token)
- end
- end
-
- context 'when invalid params' do
- let(:params) do
- {
- gcp_project_id: 'gcp-project',
- gcp_cluster_name: 'test-cluster',
- gcp_cluster_zone: 'us-central1-a',
- gcp_cluster_size: 'ABC'
- }
- end
-
- it 'returns an error' do
- expect(ClusterProvisionWorker).not_to receive(:perform_async)
- expect { result }.to change { Gcp::Cluster.count }.by(0)
- end
- end
- end
-end
diff --git a/spec/services/ci/fetch_gcp_operation_service_spec.rb b/spec/services/ci/fetch_gcp_operation_service_spec.rb
deleted file mode 100644
index 7792979c5cb..00000000000
--- a/spec/services/ci/fetch_gcp_operation_service_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-require 'google/apis'
-
-describe Ci::FetchGcpOperationService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { double }
-
- context 'when suceeded' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_operations).and_return(operation)
- end
-
- it 'fetch the gcp operaion' do
- expect { |b| described_class.new.execute(cluster, &b) }
- .to yield_with_args(operation)
- end
- end
-
- context 'when raises an error' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_operations).and_raise(error)
- end
-
- it 'sets an error to cluster object' do
- expect { |b| described_class.new.execute(cluster, &b) }
- .not_to yield_with_args
- expect(cluster.reload).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/finalize_cluster_creation_service_spec.rb b/spec/services/ci/finalize_cluster_creation_service_spec.rb
deleted file mode 100644
index def3709fdb4..00000000000
--- a/spec/services/ci/finalize_cluster_creation_service_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'spec_helper'
-
-describe Ci::FinalizeClusterCreationService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:result) { described_class.new.execute(cluster) }
-
- context 'when suceeded to get cluster from api' do
- let(:gke_cluster) { double }
-
- before do
- allow(gke_cluster).to receive(:endpoint).and_return('111.111.111.111')
- allow(gke_cluster).to receive(:master_auth).and_return(spy)
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_get).and_return(gke_cluster)
- end
-
- context 'when suceeded to get kubernetes token' do
- let(:kubernetes_token) { 'abc' }
-
- before do
- allow_any_instance_of(Ci::FetchKubernetesTokenService)
- .to receive(:execute).and_return(kubernetes_token)
- end
-
- it 'executes integration cluster' do
- expect_any_instance_of(Ci::IntegrateClusterService).to receive(:execute)
- described_class.new.execute(cluster)
- end
- end
-
- context 'when failed to get kubernetes token' do
- before do
- allow_any_instance_of(Ci::FetchKubernetesTokenService)
- .to receive(:execute).and_return(nil)
- end
-
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
- end
-
- context 'when failed to get cluster from api' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_get).and_raise(error)
- end
-
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/integrate_cluster_service_spec.rb b/spec/services/ci/integrate_cluster_service_spec.rb
deleted file mode 100644
index 3a79c205bd1..00000000000
--- a/spec/services/ci/integrate_cluster_service_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-require 'spec_helper'
-
-describe Ci::IntegrateClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster, :custom_project_namespace) }
- let(:endpoint) { '123.123.123.123' }
- let(:ca_cert) { 'ca_cert_xxx' }
- let(:token) { 'token_xxx' }
- let(:username) { 'username_xxx' }
- let(:password) { 'password_xxx' }
-
- before do
- described_class
- .new.execute(cluster, endpoint, ca_cert, token, username, password)
-
- cluster.reload
- end
-
- context 'when correct params' do
- it 'creates a cluster object' do
- expect(cluster.endpoint).to eq(endpoint)
- expect(cluster.ca_cert).to eq(ca_cert)
- expect(cluster.kubernetes_token).to eq(token)
- expect(cluster.username).to eq(username)
- expect(cluster.password).to eq(password)
- expect(cluster.service.active).to be_truthy
- expect(cluster.service.api_url).to eq(cluster.api_url)
- expect(cluster.service.ca_pem).to eq(ca_cert)
- expect(cluster.service.namespace).to eq(cluster.project_namespace)
- expect(cluster.service.token).to eq(token)
- end
- end
-
- context 'when invalid params' do
- let(:endpoint) { nil }
-
- it 'sets an error to cluster object' do
- expect(cluster).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/provision_cluster_service_spec.rb b/spec/services/ci/provision_cluster_service_spec.rb
deleted file mode 100644
index 5ce5c788314..00000000000
--- a/spec/services/ci/provision_cluster_service_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-require 'spec_helper'
-
-describe Ci::ProvisionClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { spy }
-
- shared_examples 'error' do
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
-
- context 'when suceeded to request provision' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create).and_return(operation)
- end
-
- context 'when operation status is RUNNING' do
- before do
- allow(operation).to receive(:status).and_return('RUNNING')
- end
-
- context 'when suceeded to parse gcp operation id' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return('operation-123')
- end
-
- context 'when cluster status is scheduled' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return('operation-123')
- end
-
- it 'schedules a worker for status minitoring' do
- expect(WaitForClusterCreationWorker).to receive(:perform_in)
-
- described_class.new.execute(cluster)
- end
- end
-
- context 'when cluster status is creating' do
- before do
- cluster.make_creating!
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when failed to parse gcp operation id' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return(nil)
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when operation status is others' do
- before do
- allow(operation).to receive(:status).and_return('others')
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when failed to request provision' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create).and_raise(error)
- end
-
- it_behaves_like 'error'
- end
- end
-end
diff --git a/spec/services/ci/update_cluster_service_spec.rb b/spec/services/ci/update_cluster_service_spec.rb
deleted file mode 100644
index a289385b88f..00000000000
--- a/spec/services/ci/update_cluster_service_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'spec_helper'
-
-describe Ci::UpdateClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service) }
-
- before do
- described_class.new(cluster.project, cluster.user, params).execute(cluster)
-
- cluster.reload
- end
-
- context 'when correct params' do
- context 'when enabled is true' do
- let(:params) { { 'enabled' => 'true' } }
-
- it 'enables cluster and overwrite kubernetes service' do
- expect(cluster.enabled).to be_truthy
- expect(cluster.service.active).to be_truthy
- expect(cluster.service.api_url).to eq(cluster.api_url)
- expect(cluster.service.ca_pem).to eq(cluster.ca_cert)
- expect(cluster.service.namespace).to eq(cluster.project_namespace)
- expect(cluster.service.token).to eq(cluster.kubernetes_token)
- end
- end
-
- context 'when enabled is false' do
- let(:params) { { 'enabled' => 'false' } }
-
- it 'disables cluster and kubernetes service' do
- expect(cluster.enabled).to be_falsy
- expect(cluster.service.active).to be_falsy
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
new file mode 100644
index 00000000000..5b6edb73beb
--- /dev/null
+++ b/spec/services/clusters/create_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Clusters::CreateService do
+ let(:access_token) { 'xxx' }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:result) { described_class.new(project, user, params).execute(access_token) }
+
+ context 'when provider is gcp' do
+ 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'
+ }
+ }
+ end
+
+ it 'creates a cluster object and performs a worker' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { result }
+ .to change { Clusters::Cluster.count }.by(1)
+ .and change { Clusters::Providers::Gcp.count }.by(1)
+
+ expect(result.name).to eq('test-cluster')
+ expect(result.user).to eq(user)
+ expect(result.project).to eq(project)
+ expect(result.provider.gcp_project_id).to eq('gcp-project')
+ expect(result.provider.zone).to eq('us-central1-a')
+ expect(result.provider.num_nodes).to eq(1)
+ expect(result.provider.machine_type).to eq('machine_type-a')
+ expect(result.provider.access_token).to eq(access_token)
+ expect(result.platform).to be_nil
+ end
+ end
+
+ 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'
+ }
+ }
+ end
+
+ it 'returns an error' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { result }.to change { Clusters::Cluster.count }.by(0)
+ expect(result.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/fetch_operation_service_spec.rb b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
new file mode 100644
index 00000000000..e2fa93904c5
--- /dev/null
+++ b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::FetchOperationService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:operation_id) { provider.operation_id }
+
+ shared_examples 'success' do
+ it 'yields' do
+ expect { |b| described_class.new.execute(provider, &b) }
+ .to yield_with_args
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ expect { |b| described_class.new.execute(provider, &b) }
+ .not_to yield_with_args
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to fetch operation' do
+ before do
+ stub_cloud_platform_get_zone_operation(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'success'
+ end
+
+ context 'when Internal Server Error happened' do
+ before do
+ stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
new file mode 100644
index 00000000000..0cf91307589
--- /dev/null
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::FinalizeCreationService do
+ include GoogleApi::CloudPlatformHelpers
+ include KubernetesHelpers
+
+ describe '#execute' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:provider) { cluster.provider }
+ let(:platform) { cluster.platform }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:cluster_name) { cluster.name }
+
+ shared_examples 'success' do
+ it 'configures provider and kubernetes' do
+ described_class.new.execute(provider)
+
+ expect(provider).to be_created
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to fetch gke cluster info' do
+ let(:endpoint) { '111.111.111.111' }
+ let(:api_url) { 'https://' + endpoint }
+ let(:username) { 'sample-username' }
+ let(:password) { 'sample-password' }
+
+ before do
+ stub_cloud_platform_get_zone_cluster(
+ gcp_project_id, zone, cluster_name,
+ {
+ endpoint: endpoint,
+ username: username,
+ password: password
+ }
+ )
+
+ stub_kubeclient_discover(api_url)
+ end
+
+ context 'when suceeded to fetch kuberenetes token' do
+ let(:token) { 'sample-token' }
+
+ before do
+ stub_kubeclient_get_secrets(
+ api_url,
+ {
+ token: Base64.encode64(token)
+ } )
+ end
+
+ it_behaves_like 'success'
+
+ it 'has corresponded data' do
+ described_class.new.execute(provider)
+ cluster.reload
+ provider.reload
+ platform.reload
+
+ expect(provider.endpoint).to eq(endpoint)
+ expect(platform.api_url).to eq(api_url)
+ expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert))
+ expect(platform.username).to eq(username)
+ expect(platform.password).to eq(password)
+ expect(platform.token).to eq(token)
+ end
+ end
+
+ context 'when default-token is not found' do
+ before do
+ stub_kubeclient_get_secrets(api_url, metadata_name: 'aaaa')
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when token is empty' do
+ before do
+ stub_kubeclient_get_secrets(api_url, token: '')
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when failed to fetch kuberenetes token' do
+ before do
+ stub_kubeclient_get_secrets_error(api_url)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when failed to fetch gke cluster info' do
+ before do
+ stub_cloud_platform_get_zone_cluster_error(gcp_project_id, zone, cluster_name)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb
new file mode 100644
index 00000000000..f48afdc83b2
--- /dev/null
+++ b/spec/services/clusters/gcp/provision_service_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::ProvisionService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_gcp, :scheduled) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+
+ shared_examples 'success' do
+ it 'schedules a worker for status minitoring' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_creating
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to request provision' do
+ before do
+ stub_cloud_platform_create_cluster(gcp_project_id, zone)
+ end
+
+ it_behaves_like 'success'
+ end
+
+ context 'when operation status is unexpected' do
+ before do
+ stub_cloud_platform_create_cluster(
+ gcp_project_id, zone,
+ {
+ "status": 'unexpected'
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when selfLink is unexpected' do
+ before do
+ stub_cloud_platform_create_cluster(
+ gcp_project_id, zone,
+ {
+ "selfLink": 'unexpected'
+ })
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when Internal Server Error happened' do
+ before do
+ stub_cloud_platform_create_cluster_error(gcp_project_id, zone)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
new file mode 100644
index 00000000000..2ee2fa51f63
--- /dev/null
+++ b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::VerifyProvisionStatusService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:operation_id) { provider.operation_id }
+
+ shared_examples 'continue_creation' do
+ it 'schedules a worker for status minitoring' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+
+ described_class.new.execute(provider)
+ end
+ end
+
+ shared_examples 'finalize_creation' do
+ it 'schedules a worker for status minitoring' do
+ expect_any_instance_of(Clusters::Gcp::FinalizeCreationService).to receive(:execute)
+
+ described_class.new.execute(provider)
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when operation status is RUNNING' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'RUNNING',
+ "startTime": 1.minute.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'continue_creation'
+
+ context 'when cluster creation time exceeds timeout' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'RUNNING',
+ "startTime": 30.minutes.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when operation status is PENDING' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'PENDING',
+ "startTime": 1.minute.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'continue_creation'
+ end
+
+ context 'when operation status is DONE' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'DONE'
+ } )
+ end
+
+ it_behaves_like 'finalize_creation'
+ end
+
+ context 'when operation status is unexpected' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'unexpected'
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when failed to get operation status' do
+ before do
+ stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb
new file mode 100644
index 00000000000..2d91a21035d
--- /dev/null
+++ b/spec/services/clusters/update_service_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Clusters::UpdateService do
+ describe '#execute' do
+ subject { described_class.new(cluster.project, cluster.user, params).execute(cluster) }
+
+ let(:cluster) { create(:cluster, :project, :provided_by_user) }
+
+ context 'when correct params' do
+ context 'when enabled is true' do
+ let(:params) { { enabled: true } }
+
+ it 'enables cluster' do
+ is_expected.to eq(true)
+ expect(cluster.enabled).to be_truthy
+ end
+ end
+
+ context 'when enabled is false' do
+ let(:params) { { enabled: false } }
+
+ it 'disables cluster' do
+ is_expected.to eq(true)
+ expect(cluster.enabled).to be_falsy
+ end
+ end
+
+ context 'when namespace is specified' do
+ let(:params) do
+ {
+ platform_kubernetes_attributes: {
+ namespace: 'custom-namespace'
+ }
+ }
+ end
+
+ it 'updates namespace' do
+ is_expected.to eq(true)
+ expect(cluster.platform.namespace).to eq('custom-namespace')
+ end
+ end
+ end
+
+ context 'when invalid params' do
+ let(:params) do
+ {
+ platform_kubernetes_attributes: {
+ namespace: '!!!'
+ }
+ }
+ end
+
+ it 'returns false' do
+ is_expected.to eq(false)
+ expect(cluster.errors[:"platform_kubernetes.namespace"]).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb
new file mode 100644
index 00000000000..dabf0db7666
--- /dev/null
+++ b/spec/support/google_api/cloud_platform_helpers.rb
@@ -0,0 +1,119 @@
+module GoogleApi
+ module CloudPlatformHelpers
+ def stub_google_api_validate_token
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_token] = 'token'
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = 1.hour.since.to_i.to_s
+ end
+
+ def stub_google_api_expired_token
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_token] = 'token'
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = 1.hour.ago.to_i.to_s
+ end
+
+ def stub_cloud_platform_get_zone_cluster(project_id, zone, cluster_id, **options)
+ WebMock.stub_request(:get, cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id))
+ .to_return(cloud_platform_response(cloud_platform_cluster_body(options)))
+ end
+
+ def stub_cloud_platform_get_zone_cluster_error(project_id, zone, cluster_id)
+ WebMock.stub_request(:get, cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def stub_cloud_platform_create_cluster(project_id, zone, **options)
+ WebMock.stub_request(:post, cloud_platform_create_cluster_url(project_id, zone))
+ .to_return(cloud_platform_response(cloud_platform_operation_body(options)))
+ end
+
+ def stub_cloud_platform_create_cluster_error(project_id, zone)
+ WebMock.stub_request(:post, cloud_platform_create_cluster_url(project_id, zone))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def stub_cloud_platform_get_zone_operation(project_id, zone, operation_id, **options)
+ WebMock.stub_request(:get, cloud_platform_get_zone_operation_url(project_id, zone, operation_id))
+ .to_return(cloud_platform_response(cloud_platform_operation_body(options)))
+ end
+
+ def stub_cloud_platform_get_zone_operation_error(project_id, zone, operation_id)
+ WebMock.stub_request(:get, cloud_platform_get_zone_operation_url(project_id, zone, operation_id))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters/#{cluster_id}"
+ end
+
+ def cloud_platform_create_cluster_url(project_id, zone)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters"
+ end
+
+ def cloud_platform_get_zone_operation_url(project_id, zone, operation_id)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/operations/#{operation_id}"
+ end
+
+ def cloud_platform_response(body)
+ { status: 200, headers: { 'Content-Type' => 'application/json' }, body: body.to_json }
+ end
+
+ def load_sample_cert
+ pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
+ Base64.encode64(File.read(pem_file))
+ end
+
+ ##
+ # gcloud container clusters create
+ # https://cloud.google.com/container-engine/reference/rest/v1/projects.zones.clusters/create
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def cloud_platform_cluster_body(**options)
+ {
+ "name": options[:name] || 'string',
+ "description": options[:description] || 'string',
+ "initialNodeCount": options[:initialNodeCount] || 'number',
+ "masterAuth": {
+ "username": options[:username] || 'string',
+ "password": options[:password] || 'string',
+ "clusterCaCertificate": options[:clusterCaCertificate] || load_sample_cert,
+ "clientCertificate": options[:clientCertificate] || 'string',
+ "clientKey": options[:clientKey] || 'string'
+ },
+ "loggingService": options[:loggingService] || 'string',
+ "monitoringService": options[:monitoringService] || 'string',
+ "network": options[:network] || 'string',
+ "clusterIpv4Cidr": options[:clusterIpv4Cidr] || 'string',
+ "subnetwork": options[:subnetwork] || 'string',
+ "enableKubernetesAlpha": options[:enableKubernetesAlpha] || 'boolean',
+ "labelFingerprint": options[:labelFingerprint] || 'string',
+ "selfLink": options[:selfLink] || 'string',
+ "zone": options[:zone] || 'string',
+ "endpoint": options[:endpoint] || 'string',
+ "initialClusterVersion": options[:initialClusterVersion] || 'string',
+ "currentMasterVersion": options[:currentMasterVersion] || 'string',
+ "currentNodeVersion": options[:currentNodeVersion] || 'string',
+ "createTime": options[:createTime] || 'string',
+ "status": options[:status] || 'RUNNING',
+ "statusMessage": options[:statusMessage] || 'string',
+ "nodeIpv4CidrSize": options[:nodeIpv4CidrSize] || 'number',
+ "servicesIpv4Cidr": options[:servicesIpv4Cidr] || 'string',
+ "currentNodeCount": options[:currentNodeCount] || 'number',
+ "expireTime": options[:expireTime] || 'string'
+ }
+ end
+
+ def cloud_platform_operation_body(**options)
+ {
+ "name": options[:name] || 'operation-1234567891234-1234567',
+ "zone": options[:zone] || 'us-central1-a',
+ "operationType": options[:operationType] || 'CREATE_CLUSTER',
+ "status": options[:status] || 'PENDING',
+ "detail": options[:detail] || 'detail',
+ "statusMessage": options[:statusMessage] || '',
+ "selfLink": options[:selfLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/operations/operation-1234567891234-1234567',
+ "targetLink": options[:targetLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/clusters/test-cluster',
+ "startTime": options[:startTime] || '2017-09-13T16:49:13.055601589Z',
+ "endTime": options[:endTime] || ''
+ }
+ end
+ end
+end
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index c92f78b324c..e46b61b6461 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -9,22 +9,51 @@ module KubernetesHelpers
kube_response(kube_pods_body)
end
- def stub_kubeclient_discover
- WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
+ def stub_kubeclient_discover(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
end
def stub_kubeclient_pods(response = nil)
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods"
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
+ def stub_kubeclient_get_secrets(api_url, **options)
+ WebMock.stub_request(:get, api_url + '/api/v1/secrets')
+ .to_return(kube_response(kube_v1_secrets_body(options)))
+ end
+
+ def stub_kubeclient_get_secrets_error(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1/secrets')
+ .to_return(status: [404, "Internal Server Error"])
+ end
+
+ def kube_v1_secrets_body(**options)
+ {
+ "kind" => "SecretList",
+ "apiVersion": "v1",
+ "items" => [
+ {
+ "metadata": {
+ "name": options[:metadata_name] || "default-token-1",
+ "namespace": "kube-system"
+ },
+ "data": {
+ "token": options[:token] || Base64.encode64('token-sample-123')
+ }
+ }
+ ]
+ }
+ end
+
def kube_v1_discovery_body
{
"kind" => "APIResourceList",
"resources" => [
- { "name" => "pods", "namespaced" => true, "kind" => "Pod" }
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
]
}
end
diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb
index 11f208289db..8054ec11a48 100644
--- a/spec/workers/cluster_provision_worker_spec.rb
+++ b/spec/workers/cluster_provision_worker_spec.rb
@@ -2,11 +2,22 @@ require 'spec_helper'
describe ClusterProvisionWorker do
describe '#perform' do
- context 'when cluster exists' do
- let(:cluster) { create(:gcp_cluster) }
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:cluster_provider_gcp, :scheduled) }
it 'provision a cluster' do
- expect_any_instance_of(Ci::ProvisionClusterService).to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
+
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
described_class.new.perform(cluster.id)
end
@@ -14,7 +25,7 @@ describe ClusterProvisionWorker do
context 'when cluster does not exist' do
it 'does not provision a cluster' do
- expect_any_instance_of(Ci::ProvisionClusterService).not_to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
described_class.new.perform(123)
end
diff --git a/spec/workers/wait_for_cluster_creation_worker_spec.rb b/spec/workers/wait_for_cluster_creation_worker_spec.rb
index dcd4a3b9aec..0e92b298178 100644
--- a/spec/workers/wait_for_cluster_creation_worker_spec.rb
+++ b/spec/workers/wait_for_cluster_creation_worker_spec.rb
@@ -2,65 +2,32 @@ require 'spec_helper'
describe WaitForClusterCreationWorker do
describe '#perform' do
- context 'when cluster exists' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { double }
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
- before do
- allow(operation).to receive(:status).and_return(status)
- allow(operation).to receive(:start_time).and_return(1.minute.ago)
- allow(operation).to receive(:status_message).and_return('error')
- allow_any_instance_of(Ci::FetchGcpOperationService).to receive(:execute).and_yield(operation)
- end
-
- context 'when operation status is RUNNING' do
- let(:status) { 'RUNNING' }
-
- it 'reschedules worker' do
- expect(described_class).to receive(:perform_in)
-
- described_class.new.perform(cluster.id)
- end
-
- context 'when operation timeout' do
- before do
- allow(operation).to receive(:start_time).and_return(30.minutes.ago.utc)
- end
-
- it 'sets an error message on cluster' do
- described_class.new.perform(cluster.id)
+ it 'provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).to receive(:execute)
- expect(cluster.reload).to be_errored
- end
- end
- end
-
- context 'when operation status is DONE' do
- let(:status) { 'DONE' }
-
- it 'finalizes cluster creation' do
- expect_any_instance_of(Ci::FinalizeClusterCreationService).to receive(:execute)
-
- described_class.new.perform(cluster.id)
- end
+ described_class.new.perform(cluster.id)
end
+ end
- context 'when operation status is others' do
- let(:status) { 'others' }
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
- it 'sets an error message on cluster' do
- described_class.new.perform(cluster.id)
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute)
- expect(cluster.reload).to be_errored
- end
+ described_class.new.perform(cluster.id)
end
end
context 'when cluster does not exist' do
it 'does not provision a cluster' do
- expect_any_instance_of(Ci::FetchGcpOperationService).not_to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute)
- described_class.new.perform(1234)
+ described_class.new.perform(123)
end
end
end