Commit c71cf908 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'refactor-clusters' into 'master'

Refactor Clusters to be consisted from GcpProvider and KubernetesPlatform

See merge request gitlab-org/gitlab-ce!14879
parents 4da03e99 a99ad59e
...@@ -27,11 +27,13 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -27,11 +27,13 @@ class Projects::ClustersController < Projects::ApplicationController
end end
def new def new
@cluster = project.build_cluster @cluster = Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
end
end end
def create def create
@cluster = Ci::CreateClusterService @cluster = Clusters::CreateService
.new(project, current_user, create_params) .new(project, current_user, create_params)
.execute(token_in_session) .execute(token_in_session)
...@@ -58,7 +60,7 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -58,7 +60,7 @@ class Projects::ClustersController < Projects::ApplicationController
end end
def update def update
Ci::UpdateClusterService Clusters::UpdateService
.new(project, current_user, update_params) .new(project, current_user, update_params)
.execute(cluster) .execute(cluster)
...@@ -88,19 +90,19 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -88,19 +90,19 @@ class Projects::ClustersController < Projects::ApplicationController
def create_params def create_params
params.require(:cluster).permit( params.require(:cluster).permit(
:enabled,
:name,
:provider_type,
provider_gcp_attributes: [
:gcp_project_id, :gcp_project_id,
:gcp_cluster_zone, :zone,
:gcp_cluster_name, :num_nodes,
:gcp_cluster_size, :machine_type
:gcp_machine_type, ])
:project_namespace,
:enabled)
end end
def update_params def update_params
params.require(:cluster).permit( params.require(:cluster).permit(:enabled)
:project_namespace,
:enabled)
end end
def authorize_google_api def authorize_google_api
......
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
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
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
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
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
...@@ -186,7 +186,9 @@ class Project < ActiveRecord::Base ...@@ -186,7 +186,9 @@ class Project < ActiveRecord::Base
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics' 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, # Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy # which is not managed by the DB. Hence we're still using dependent: :destroy
......
module Gcp module Clusters
class ClusterPolicy < BasePolicy class ClusterPolicy < BasePolicy
alias_method :cluster, :subject alias_method :cluster, :subject
delegate { @subject.project } delegate { cluster.project }
rule { can?(:master_access) }.policy do rule { can?(:master_access) }.policy do
enable :update_cluster enable :update_cluster
......
module Gcp module Clusters
class ClusterPresenter < Gitlab::View::Presenter::Delegated class ClusterPresenter < Gitlab::View::Presenter::Delegated
presents :cluster presents :cluster
def gke_cluster_url 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 end
end end
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
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
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
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
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
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
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
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
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
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
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
module Clusters
class UpdateService < BaseService
def execute(cluster)
cluster.update(params)
end
end
end
# 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
...@@ -4,34 +4,32 @@ ...@@ -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') - 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} = 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_errors(@cluster)
.form-group .form-group
= field.label :gcp_cluster_name, s_('ClusterIntegration|Cluster name') = field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :gcp_cluster_name, class: 'form-control' = field.text_field :name, class: 'form-control'
= field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
.form-group .form-group
= field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') = 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') = 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' = provider_gcp_field.text_field :gcp_project_id, class: 'form-control'
.form-group .form-group
= field.label :gcp_cluster_zone, s_('ClusterIntegration|Zone') = 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') = 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' = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a'
.form-group .form-group
= field.label :gcp_cluster_size, s_('ClusterIntegration|Number of nodes') = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
= field.text_field :gcp_cluster_size, class: 'form-control', placeholder: '3' = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
.form-group .form-group
= field.label :gcp_machine_type, s_('ClusterIntegration|Machine type') = 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') = 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' = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4'
.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
.form-group .form-group
= field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save' = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save'
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
- else - else
= s_('ClusterIntegration|Cluster integration is disabled for this project.') = 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_errors(@cluster)
.form-group.append-bottom-20 .form-group.append-bottom-20
%label.append-bottom-10 %label.append-bottom-10
...@@ -62,9 +62,9 @@ ...@@ -62,9 +62,9 @@
%label.append-bottom-10{ for: 'cluter-name' } %label.append-bottom-10{ for: 'cluter-name' }
= s_('ClusterIntegration|Cluster name') = s_('ClusterIntegration|Cluster name')
.input-group .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 %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) } %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
......
...@@ -3,8 +3,10 @@ class ClusterProvisionWorker ...@@ -3,8 +3,10 @@ class ClusterProvisionWorker
include ClusterQueue include ClusterQueue
def perform(cluster_id) def perform(cluster_id)
Gcp::Cluster.find_by_id(cluster_id).try do |cluster| Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
Ci::ProvisionClusterService.new.execute(cluster) cluster.provider.try do |provider|
Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp?
end
end end
end end
end end
...@@ -2,25 +2,10 @@ class WaitForClusterCreationWorker ...@@ -2,25 +2,10 @@ class WaitForClusterCreationWorker
include Sidekiq::Worker include Sidekiq::Worker
include ClusterQueue include ClusterQueue
INITIAL_INTERVAL = 2.minutes
EAGER_INTERVAL = 10.seconds
TIMEOUT = 20.minutes
def perform(cluster_id) def perform(cluster_id)
Gcp::Cluster.find_by_id(cluster_id).try do |cluster| Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
Ci::FetchGcpOperationService.new.execute(cluster) do |operation| cluster.provider.try do |provider|
case operation.status Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) if cluster.gcp?
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
end end
end end
end end
......
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
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
...@@ -462,6 +462,63 @@ ActiveRecord::Schema.define(version: 20171101134435) do ...@@ -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 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| create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false t.integer "project_id", null: false
t.string "name", null: false t.string "name", null: false
...@@ -1809,6 +1866,11 @@ ActiveRecord::Schema.define(version: 20171101134435) do ...@@ -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", "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_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 "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 "container_repositories", "projects"
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
......
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
...@@ -8,8 +8,8 @@ module Gitlab ...@@ -8,8 +8,8 @@ module Gitlab
triggers: 'Ci::Trigger', triggers: 'Ci::Trigger',
pipeline_schedules: 'Ci::PipelineSchedule', pipeline_schedules: 'Ci::PipelineSchedule',
builds: 'Ci::Build', builds: 'Ci::Build',
cluster: 'Gcp::Cluster', cluster: 'Clusters::Cluster',
clusters: 'Gcp::Cluster', clusters: 'Clusters::Cluster',
hooks: 'ProjectHook', hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel', merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel', push_access_levels: 'ProtectedBranch::PushAccessLevel',
......
...@@ -48,9 +48,9 @@ module Gitlab ...@@ -48,9 +48,9 @@ module Gitlab
deploy_keys: DeployKey.count, deploy_keys: DeployKey.count,
deployments: Deployment.count, deployments: Deployment.count,
environments: ::Environment.count, environments: ::Environment.count,
gcp_clusters: ::Gcp::Cluster.count, clusters: ::Clusters::Cluster.count,
gcp_clusters_enabled: ::Gcp::Cluster.enabled.count, clusters_enabled: ::Clusters::Cluster.enabled.count,
gcp_clusters_disabled: ::Gcp::Cluster.disabled.count, clusters_disabled: ::Clusters::Cluster.disabled.count,
in_review_folder: ::Environment.in_review_folder.count, in_review_folder: ::Environment.in_review_folder.count,
groups: Group.count, groups: Group.count,
issues: Issue.count, issues: Issue.count,
......
...@@ -3,7 +3,6 @@ require 'google/apis/container_v1' ...@@ -3,7 +3,6 @@ require 'google/apis/container_v1'
module GoogleApi module GoogleApi
module CloudPlatform module CloudPlatform
class Client < GoogleApi::Auth class Client < GoogleApi::Auth
DEFAULT_MACHINE_TYPE = 'n1-standard-1'.freeze
SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze
LEAST_TOKEN_LIFE_TIME = 10.minutes LEAST_TOKEN_LIFE_TIME = 10.minutes
......
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
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
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
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
require 'spec_helper' require 'spec_helper'
feature 'Clusters', :js do feature 'Clusters', :js do
include GoogleApi::CloudPlatformHelpers
let!(:project) { create(:project, :repository) } let!(:project) { create(:project, :repository) }
let!(:user) { create(:user) } let!(:user) { create(:user) }
...@@ -11,8 +13,10 @@ feature 'Clusters', :js do ...@@ -11,8 +13,10 @@ feature 'Clusters', :js do
context 'when user has signed in Google' do context 'when user has signed in Google' do
before do before do
allow_any_instance_of(GoogleApi::CloudPlatform::Client) allow_any_instance_of(Projects::ClustersController)
.to receive(:validate_token).and_return(true) .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 end
context 'when user does not have a cluster and visits cluster index page' do context 'when user does not have a cluster and visits cluster index page' do
...@@ -36,15 +40,15 @@ feature 'Clusters', :js do ...@@ -36,15 +40,15 @@ feature 'Clusters', :js do
allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
fill_in 'cluster_gcp_project_id', with: 'gcp-project-123' fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
fill_in 'cluster_gcp_cluster_name', with: 'dev-cluster' fill_in 'cluster_name', with: 'dev-cluster'
click_button 'Create cluster' click_button 'Create cluster'
end end
it 'user sees a cluster details page and creation status' do it 'user sees a cluster details page and creation status' do
expect(page).to have_content('Cluster is being created on Google Container Engine...') 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') expect(page).to have_content('Cluster was successfully created on Google Container Engine')
end end
...@@ -62,7 +66,8 @@ feature 'Clusters', :js do ...@@ -62,7 +66,8 @@ feature 'Clusters', :js do
end end
context 'when user has a cluster and visits cluster index page' do 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 before do
visit project_clusters_path(project) visit project_clusters_path(project)
...@@ -70,7 +75,7 @@ feature 'Clusters', :js do ...@@ -70,7 +75,7 @@ feature 'Clusters', :js do
it 'user sees an cluster details page' do it 'user sees an cluster details page' do
expect(page).to have_button('Save') 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 end
context 'when user disables the cluster' do context 'when user disables the cluster' do
......
-----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-----
...@@ -6,7 +6,7 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle ...@@ -6,7 +6,7 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace) } 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 render_views
......
...@@ -148,9 +148,18 @@ deploy_keys: ...@@ -148,9 +148,18 @@ deploy_keys:
- deploy_keys_projects - deploy_keys_projects
- projects - projects
cluster: cluster:
- project - cluster_projects
- projects
- user - user
- service - provider_gcp
- platform_kubernetes
cluster_projects:
- projects
- clusters
provider_gcp:
- cluster
platform_kubernetes:
- cluster
services: services:
- project - project
- service_hook - service_hook
...@@ -182,6 +191,7 @@ project: ...@@ -182,6 +191,7 @@ project:
- tags - tags
- chat_services - chat_services
- cluster - cluster
- cluster_project
- creator - creator
- group - group
- namespace - namespace
......
...@@ -313,30 +313,47 @@ Ci::PipelineSchedule: ...@@ -313,30 +313,47 @@ Ci::PipelineSchedule:
- deleted_at - deleted_at
- created_at - created_at
- updated_at - updated_at
Gcp::Cluster: Clusters::Cluster:
- id - id
- project_id
- user_id - user_id
- service_id
- enabled - 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
- status_reason - status_reason
- project_namespace - gcp_project_id
- zone
- num_nodes
- machine_type
- operation_id
- endpoint - endpoint
- encrypted_access_token
- encrypted_access_token_iv
- created_at
- updated_at
Clusters::Platforms::Kubernetes:
- id
- cluster_id
- api_url
- ca_cert - ca_cert
- encrypted_kubernetes_token - namespace
- encrypted_kubernetes_token_iv
- username - username
- encrypted_password - encrypted_password
- encrypted_password_iv - encrypted_password_iv
- gcp_project_id - encrypted_token
- gcp_cluster_zone - encrypted_token_iv
- gcp_cluster_name
- gcp_cluster_size
- gcp_machine_type
- gcp_operation_id
- encrypted_gcp_token
- encrypted_gcp_token_iv
- created_at - created_at
- updated_at - updated_at
DeployKey: DeployKey:
......
...@@ -60,9 +60,9 @@ describe Gitlab::UsageData do ...@@ -60,9 +60,9 @@ describe Gitlab::UsageData do
deploy_keys deploy_keys
deployments deployments
environments environments
gcp_clusters clusters
gcp_clusters_enabled clusters_enabled
gcp_clusters_disabled clusters_disabled
in_review_folder in_review_folder
groups groups
issues issues
......
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
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
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
require 'spec_helper'
describe Clusters::Project do
it { is_expected.to belong_to(:cluster) }
it { is_expected.to belong_to(:project) }
end
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
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
...@@ -145,7 +145,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do ...@@ -145,7 +145,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
let(:discovery_url) { 'https://kubernetes.example.com/api/v1' } let(:discovery_url) { 'https://kubernetes.example.com/api/v1' }
before do before do
stub_kubeclient_discover stub_kubeclient_discover(service.api_url)
end end
context 'with path prefix in api_url' do context 'with path prefix in api_url' do
...@@ -153,7 +153,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do ...@@ -153,7 +153,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
it 'tests with the prefix' do it 'tests with the prefix' do
service.api_url = 'https://kubernetes.example.com/prefix' 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(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once expect(WebMock).to have_requested(:get, discovery_url).once
......
require 'spec_helper' require 'spec_helper'
describe Gcp::ClusterPolicy, :models do describe Clusters::ClusterPolicy, :models do
set(:project) { create(:project) } let(:cluster) { create(:cluster, :project) }
set(:cluster) { create(:gcp_cluster, project: project) } let(:project) { cluster.project }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:policy) { described_class.new(user, cluster) } let(:policy) { described_class.new(user, cluster) }
......
require 'spec_helper' require 'spec_helper'
describe Gcp::ClusterPresenter do describe Clusters::ClusterPresenter do
let(:project) { create(:project) } let(:cluster) { create(:cluster, :provided_by_gcp) }
let(:cluster) { create(:gcp_cluster, project: project) }
subject(:presenter) do subject(:presenter) do
described_class.new(cluster) described_class.new(cluster)
...@@ -22,14 +21,14 @@ describe Gcp::ClusterPresenter do ...@@ -22,14 +21,14 @@ describe Gcp::ClusterPresenter do
end end
it 'forwards missing methods to cluster' do 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
end end
describe '#gke_cluster_url' do describe '#gke_cluster_url' do
subject { described_class.new(cluster).gke_cluster_url } subject { described_class.new(cluster).gke_cluster_url }
it { is_expected.to include(cluster.gcp_cluster_zone) } it { is_expected.to include(cluster.provider.zone) }
it { is_expected.to include(cluster.gcp_cluster_name) } it { is_expected.to include(cluster.name) }
end end
end end
require 'spec_helper' require 'spec_helper'
describe ClusterEntity do describe ClusterEntity do
set(:cluster) { create(:gcp_cluster, :errored) } describe '#as_json' do
let(:request) { double('request') } subject { described_class.new(cluster).as_json }
context 'when provider type is gcp' do
let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
context 'when status is creating' do
let(:provider) { create(:cluster_provider_gcp, :creating) }
let(:entity) do it 'has corresponded data' do
described_class.new(cluster) expect(subject[:status]).to eq(:creating)
expect(subject[:status_reason]).to be_nil
end
end end
describe '#as_json' do context 'when status is errored' do
subject { entity.as_json } let(:provider) { create(:cluster_provider_gcp, :errored) }
it 'contains status' do it 'has corresponded data' do
expect(subject[:status]).to eq(:errored) expect(subject[:status]).to eq(:errored)
expect(subject[:status_reason]).to eq(provider.status_reason)
end
end
end end
it 'contains status reason' do context 'when provider type is user' do
expect(subject[:status_reason]).to eq('general error') 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 end
end end
require 'spec_helper' require 'spec_helper'
describe ClusterSerializer do describe ClusterSerializer do
let(:serializer) do
described_class.new
end
describe '#represent_status' do 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 context 'when provider type is user' do
let(:resource) { create(:gcp_cluster, :errored) } let(:cluster) { create(:cluster, provider_type: :user) }
it 'serializes only status' do it 'serializes only status' do
expect(subject.keys).to contain_exactly(:status, :status_reason) expect(subject.keys).to contain_exactly(:status, :status_reason)
......
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
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
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
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
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
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
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
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
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
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
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
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
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
...@@ -9,22 +9,51 @@ module KubernetesHelpers ...@@ -9,22 +9,51 @@ module KubernetesHelpers
kube_response(kube_pods_body) kube_response(kube_pods_body)
end end
def stub_kubeclient_discover def stub_kubeclient_discover(api_url)
WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body)) WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
end end
def stub_kubeclient_pods(response = nil) 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" pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods"
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response) WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end 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 def kube_v1_discovery_body
{ {
"kind" => "APIResourceList", "kind" => "APIResourceList",
"resources" => [ "resources" => [
{ "name" => "pods", "namespaced" => true, "kind" => "Pod" } { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
{ "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
] ]
} }
end end
......
...@@ -2,11 +2,22 @@ require 'spec_helper' ...@@ -2,11 +2,22 @@ require 'spec_helper'
describe ClusterProvisionWorker do describe ClusterProvisionWorker do
describe '#perform' do describe '#perform' do
context 'when cluster exists' do context 'when provider type is gcp' do
let(:cluster) { create(:gcp_cluster) } let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
let(:provider) { create(:cluster_provider_gcp, :scheduled) }
it 'provision a cluster' do 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) described_class.new.perform(cluster.id)
end end
...@@ -14,7 +25,7 @@ describe ClusterProvisionWorker do ...@@ -14,7 +25,7 @@ describe ClusterProvisionWorker do
context 'when cluster does not exist' do context 'when cluster does not exist' do
it 'does not provision a cluster' 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) described_class.new.perform(123)
end end
......
...@@ -2,65 +2,32 @@ require 'spec_helper' ...@@ -2,65 +2,32 @@ require 'spec_helper'
describe WaitForClusterCreationWorker do describe WaitForClusterCreationWorker do
describe '#perform' do describe '#perform' do
context 'when cluster exists' do context 'when provider type is gcp' do
let(:cluster) { create(:gcp_cluster) } let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
let(:operation) { double } let(:provider) { create(:cluster_provider_gcp, :creating) }
before do it 'provision a cluster' do
allow(operation).to receive(:status).and_return(status) expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).to receive(:execute)
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) described_class.new.perform(cluster.id)
end 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)
expect(cluster.reload).to be_errored
end
end end
end
context 'when operation status is DONE' do
let(:status) { 'DONE' }
it 'finalizes cluster creation' do context 'when provider type is user' do
expect_any_instance_of(Ci::FinalizeClusterCreationService).to receive(:execute) let(:cluster) { create(:cluster, provider_type: :user) }
described_class.new.perform(cluster.id) it 'does not provision a cluster' do
end expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute)
end
context 'when operation status is others' do
let(:status) { 'others' }
it 'sets an error message on cluster' do
described_class.new.perform(cluster.id) described_class.new.perform(cluster.id)
expect(cluster.reload).to be_errored
end
end end
end end
context 'when cluster does not exist' do context 'when cluster does not exist' do
it 'does not provision a cluster' 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 end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment