From 1ea2a85cbe61222765dc3ef50e8c0e4c1b2f5bfa Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Mon, 25 Sep 2017 23:38:34 +0200 Subject: Make Prometheus metrics endpoint return empty response when metrics are disabled --- app/controllers/metrics_controller.rb | 16 +++++++++------- spec/controllers/metrics_controller_spec.rb | 3 ++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index 37587a52eaf..d81ad135198 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -3,10 +3,16 @@ class MetricsController < ActionController::Base protect_from_forgery with: :exception - before_action :validate_prometheus_metrics - def index - render text: metrics_service.metrics_text, content_type: 'text/plain; version=0.0.4' + response = if Gitlab::Metrics.prometheus_metrics_enabled? + metrics_service.metrics_text + else + help_page = help_page_url('administration/monitoring/prometheus/gitlab_metrics', + anchor: 'gitlab-prometheus-metrics' + ) + "# Metrics are disabled, see: #{help_page}\n" + end + render text: response, content_type: 'text/plain; version=0.0.4' end private @@ -14,8 +20,4 @@ class MetricsController < ActionController::Base def metrics_service @metrics_service ||= MetricsService.new end - - def validate_prometheus_metrics - render_404 unless Gitlab::Metrics.prometheus_metrics_enabled? - end end diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb index 7b0976e3e67..d6f68b73428 100644 --- a/spec/controllers/metrics_controller_spec.rb +++ b/spec/controllers/metrics_controller_spec.rb @@ -78,7 +78,8 @@ describe MetricsController do it 'returns proper response' do get :index - expect(response.status).to eq(404) + expect(response.status).to eq(200) + expect(response.body).to eq("# Metrics are disabled, see: http://test.host/help/administration/monitoring/prometheus/gitlab_metrics#gitlab-prometheus-metrics\n") end end end -- cgit v1.2.1 From e1d12ba9b988e61afb9317f3a132d6e2caa93923 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 Oct 2017 19:21:23 +0200 Subject: Refactor Clusters to be consisted from GcpProvider and KubernetesPlatform --- app/controllers/projects/clusters_controller.rb | 28 ++-- app/models/clusters/cluster.rb | 56 +++++++ app/models/clusters/cluster_project.rb | 6 + app/models/clusters/platforms/kubernetes.rb | 172 +++++++++++++++++++++ app/models/clusters/providers/gcp.rb | 79 ++++++++++ app/models/gcp/cluster.rb | 116 -------------- app/models/project.rb | 4 +- app/services/ci/create_cluster_service.rb | 15 -- app/services/ci/fetch_gcp_operation_service.rb | 17 -- app/services/ci/fetch_kubernetes_token_service.rb | 72 --------- .../ci/finalize_cluster_creation_service.rb | 33 ---- app/services/ci/integrate_cluster_service.rb | 26 ---- app/services/ci/provision_cluster_service.rb | 36 ----- app/services/ci/update_cluster_service.rb | 22 --- app/services/clusters/create_service.rb | 14 ++ .../clusters/gcp/fetch_operation_service.rb | 16 ++ .../clusters/gcp/finalize_creation_service.rb | 66 ++++++++ app/services/clusters/gcp/provision_service.rb | 49 ++++++ .../gcp/verify_provision_status_service.rb | 44 ++++++ app/services/clusters/update_service.rb | 7 + app/workers/cluster_provision_worker.rb | 6 +- app/workers/wait_for_cluster_creation_worker.rb | 21 +-- db/migrate/20171013094327_create_clusters.rb | 66 ++++++++ 23 files changed, 603 insertions(+), 368 deletions(-) create mode 100644 app/models/clusters/cluster.rb create mode 100644 app/models/clusters/cluster_project.rb create mode 100644 app/models/clusters/platforms/kubernetes.rb create mode 100644 app/models/clusters/providers/gcp.rb delete mode 100644 app/models/gcp/cluster.rb delete mode 100644 app/services/ci/create_cluster_service.rb delete mode 100644 app/services/ci/fetch_gcp_operation_service.rb delete mode 100644 app/services/ci/fetch_kubernetes_token_service.rb delete mode 100644 app/services/ci/finalize_cluster_creation_service.rb delete mode 100644 app/services/ci/integrate_cluster_service.rb delete mode 100644 app/services/ci/provision_cluster_service.rb delete mode 100644 app/services/ci/update_cluster_service.rb create mode 100644 app/services/clusters/create_service.rb create mode 100644 app/services/clusters/gcp/fetch_operation_service.rb create mode 100644 app/services/clusters/gcp/finalize_creation_service.rb create mode 100644 app/services/clusters/gcp/provision_service.rb create mode 100644 app/services/clusters/gcp/verify_provision_status_service.rb create mode 100644 app/services/clusters/update_service.rb create mode 100644 db/migrate/20171013094327_create_clusters.rb diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 03019b0becc..0679d8c69f0 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -31,7 +31,7 @@ class Projects::ClustersController < Projects::ApplicationController end def create - @cluster = Ci::CreateClusterService + @cluster = Ci::CreateService .new(project, current_user, create_params) .execute(token_in_session) @@ -88,19 +88,27 @@ class Projects::ClustersController < Projects::ApplicationController def create_params params.require(:cluster).permit( - :gcp_project_id, - :gcp_cluster_zone, - :gcp_cluster_name, - :gcp_cluster_size, - :gcp_machine_type, - :project_namespace, - :enabled) + :enabled, + :platform_type, + :provider_type, + kubernetes_platform: [ + :namespace + ], + gcp_provider: [ + :project_id, + :cluster_zone, + :cluster_name, + :cluster_size, + :machine_type + ]) end def update_params params.require(:cluster).permit( - :project_namespace, - :enabled) + :enabled, + kubernetes_platform: [ + :namespace + ]) end def authorize_google_api diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb new file mode 100644 index 00000000000..d7b13ac88f2 --- /dev/null +++ b/app/models/clusters/cluster.rb @@ -0,0 +1,56 @@ +module Clusters + class Cluster < ActiveRecord::Base + include Presentable + + belongs_to :user + belongs_to :service + + enum :platform_type { + kubernetes: 1 + } + + enum :provider_type { + user: 0, + gcp: 1 + } + + has_many :cluster_projects + has_many :projects, through: :cluster_projects + + has_one :gcp_provider + has_one :kubernetes_platform + + accepts_nested_attributes_for :gcp_provider + accepts_nested_attributes_for :kubernetes_platform + + validates :kubernetes_platform, presence: true, if: :kubernetes? + validates :gcp_provider, presence: true, if: :gcp? + validate :restrict_modification, on: :update + + delegate :status, to: :provider, allow_nil: true + delegate :status_reason, to: :provider, allow_nil: true + + def restrict_modification + if provider&.on_creation? + errors.add(:base, "cannot modify during creation") + return false + end + + true + end + + def provider + return gcp_provider if gcp? + end + + def platform + return kubernetes_platform if kubernetes? + end + + def first_project + return @first_project if defined?(@first_project) + + @first_project = projects.first + end + end +end diff --git a/app/models/clusters/cluster_project.rb b/app/models/clusters/cluster_project.rb new file mode 100644 index 00000000000..7b139c2bb08 --- /dev/null +++ b/app/models/clusters/cluster_project.rb @@ -0,0 +1,6 @@ +module Clusters + class ClusterProject < ActiveRecord::Base + belongs_to :cluster + belongs_to :project + end +end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb new file mode 100644 index 00000000000..aed6f733487 --- /dev/null +++ b/app/models/clusters/platforms/kubernetes.rb @@ -0,0 +1,172 @@ +module Clusters + module Platforms + class Kubernetes < ActiveRecord::Base + include Gitlab::Kubernetes + include ReactiveCaching + + TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze + + self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } + + belongs_to :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' + + validates :namespace, + allow_blank: true, + length: 1..63, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message + } + + validates :api_url, url: true, presence: true + validates :token, presence: true + + after_save :clear_reactive_cache! + + before_validation :enforce_namespace_to_lower_case + + def actual_namespace + if namespace.present? + namespace + else + default_namespace + end + end + + def predefined_variables + config = YAML.dump(kubeconfig) + + variables = [ + { key: 'KUBE_URL', value: api_url, public: true }, + { key: 'KUBE_TOKEN', value: token, public: false }, + { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true }, + { key: 'KUBECONFIG', value: config, public: false, file: true } + ] + + if ca_pem.present? + variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } + variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } + end + + variables + end + + # Constructs a list of terminals from the reactive cache + # + # Returns nil if the cache is empty, in which case you should try again a + # short time later + def terminals(environment) + with_reactive_cache do |data| + pods = filter_by_label(data[:pods], app: environment.slug) + terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) } + terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) } + end + end + + # Caches resources in the namespace so other calls don't need to block on + # network access + def calculate_reactive_cache + return unless active? && project && !project.pending_delete? + + # We may want to cache extra things in the future + { pods: read_pods } + end + + def kubeconfig + to_kubeconfig( + url: api_url, + namespace: actual_namespace, + token: token, + ca_pem: ca_pem) + end + + def namespace_placeholder + default_namespace || TEMPLATE_PLACEHOLDER + end + + def default_namespace + "#{cluster.first_project.path}-#{cluster.first_project.id}" if cluster.first_project + end + + def read_secrets + kubeclient = build_kubeclient! + + kubeclient.get_secrets.as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + + # Returns a hash of all pods in the namespace + def read_pods + kubeclient = build_kubeclient! + + kubeclient.get_pods(namespace: actual_namespace).as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + + def kubeclient_ssl_options + opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } + + if ca_pem.present? + opts[:cert_store] = OpenSSL::X509::Store.new + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + end + + opts + end + + private + + def build_kubeclient!(api_path: 'api', api_version: 'v1') + raise "Incomplete settings" unless api_url && actual_namespace && token + + ::Kubeclient::Client.new( + join_api_url(api_path), + api_version, + auth_options: kubeclient_auth_options, + ssl_options: kubeclient_ssl_options, + http_proxy_uri: ENV['http_proxy'] + ) + end + + def kubeclient_auth_options + return { username: username, password: password } if username + return { bearer_token: token } if token + end + + def join_api_url(api_path) + url = URI.parse(api_url) + prefix = url.path.sub(%r{/+\z}, '') + + url.path = [prefix, api_path].join("/") + + url.to_s + end + + def terminal_auth + { + token: token, + ca_pem: ca_pem, + max_session_time: current_application_settings.terminal_max_session_time + } + end + + def enforce_namespace_to_lower_case + self.namespace = self.namespace&.downcase + end + end + end +end diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb new file mode 100644 index 00000000000..5d4618cfe87 --- /dev/null +++ b/app/models/clusters/providers/gcp.rb @@ -0,0 +1,79 @@ +module Clusters + module Providers + class Gcp < ActiveRecord::Base + belongs_to :cluster + + default_value_for :cluster_zone, 'us-central1-a' + default_value_for :cluster_size, 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 :project_id, + length: 1..63, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message + } + + validates :cluster_name, + length: 1..63, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message + } + + validates :cluster_zone, presence: true + + validates :cluster_size, + 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.token = nil + provider.operation_id = nil + provider.save! + end + + before_transition any => [:errored] do |provider, transition| + status_reason = transition.args.first + provider.status_reason = status_reason if status_reason + end + end + + def on_creation? + scheduled? || creating? + end + + def api_client + return unless access_token + + @api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil) + end + end + end +end diff --git a/app/models/gcp/cluster.rb b/app/models/gcp/cluster.rb deleted file mode 100644 index 162a690c0e3..00000000000 --- a/app/models/gcp/cluster.rb +++ /dev/null @@ -1,116 +0,0 @@ -module Gcp - class Cluster < ActiveRecord::Base - extend Gitlab::Gcp::Model - include Presentable - - belongs_to :project, inverse_of: :cluster - belongs_to :user - belongs_to :service - - scope :enabled, -> { where(enabled: true) } - scope :disabled, -> { where(enabled: false) } - - default_value_for :gcp_cluster_zone, 'us-central1-a' - default_value_for :gcp_cluster_size, 3 - default_value_for :gcp_machine_type, 'n1-standard-4' - - attr_encrypted :password, - mode: :per_attribute_iv, - key: Gitlab::Application.secrets.db_key_base, - algorithm: 'aes-256-cbc' - - attr_encrypted :kubernetes_token, - mode: :per_attribute_iv, - key: Gitlab::Application.secrets.db_key_base, - algorithm: 'aes-256-cbc' - - attr_encrypted :gcp_token, - mode: :per_attribute_iv, - key: Gitlab::Application.secrets.db_key_base, - algorithm: 'aes-256-cbc' - - validates :gcp_project_id, - length: 1..63, - format: { - with: Gitlab::Regex.kubernetes_namespace_regex, - message: Gitlab::Regex.kubernetes_namespace_regex_message - } - - validates :gcp_cluster_name, - length: 1..63, - format: { - with: Gitlab::Regex.kubernetes_namespace_regex, - message: Gitlab::Regex.kubernetes_namespace_regex_message - } - - validates :gcp_cluster_zone, presence: true - - validates :gcp_cluster_size, - presence: true, - numericality: { - only_integer: true, - greater_than: 0 - } - - validates :project_namespace, - allow_blank: true, - length: 1..63, - format: { - with: Gitlab::Regex.kubernetes_namespace_regex, - message: Gitlab::Regex.kubernetes_namespace_regex_message - } - - # if we do not do status transition we prevent change - validate :restrict_modification, on: :update, unless: :status_changed? - - state_machine :status, initial: :scheduled do - state :scheduled, value: 1 - state :creating, value: 2 - state :created, value: 3 - state :errored, value: 4 - - event :make_creating do - transition any - [:creating] => :creating - end - - event :make_created do - transition any - [:created] => :created - end - - event :make_errored do - transition any - [:errored] => :errored - end - - before_transition any => [:errored, :created] do |cluster| - cluster.gcp_token = nil - cluster.gcp_operation_id = nil - end - - before_transition any => [:errored] do |cluster, transition| - status_reason = transition.args.first - cluster.status_reason = status_reason if status_reason - end - end - - def project_namespace_placeholder - "#{project.path}-#{project.id}" - end - - def on_creation? - scheduled? || creating? - end - - def api_url - 'https://' + endpoint if endpoint - end - - def restrict_modification - if on_creation? - errors.add(:base, "cannot modify during creation") - return false - end - - true - end - end -end diff --git a/app/models/project.rb b/app/models/project.rb index 4689b588906..bc263b63881 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -177,7 +177,9 @@ class Project < ActiveRecord::Base has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true has_one :project_feature, inverse_of: :project has_one :statistics, class_name: 'ProjectStatistics' - has_one :cluster, class_name: 'Gcp::Cluster', inverse_of: :project + + has_many :cluster_projects, class_name: 'Clusters::ClusterProject' + has_one :cluster, through: :cluster_projects # Container repositories need to remove data from the container registry, # which is not managed by the DB. Hence we're still using dependent: :destroy diff --git a/app/services/ci/create_cluster_service.rb b/app/services/ci/create_cluster_service.rb deleted file mode 100644 index f7ee0e468e2..00000000000 --- a/app/services/ci/create_cluster_service.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Ci - class CreateClusterService < BaseService - def execute(access_token) - params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE - - cluster_params = - params.merge(user: current_user, - gcp_token: access_token) - - project.create_cluster(cluster_params).tap do |cluster| - ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? - end - end - end -end diff --git a/app/services/ci/fetch_gcp_operation_service.rb b/app/services/ci/fetch_gcp_operation_service.rb deleted file mode 100644 index 0b68e4d6ea9..00000000000 --- a/app/services/ci/fetch_gcp_operation_service.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Ci - class FetchGcpOperationService - def execute(cluster) - api_client = - GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil) - - operation = api_client.projects_zones_operations( - cluster.gcp_project_id, - cluster.gcp_cluster_zone, - cluster.gcp_operation_id) - - yield(operation) if block_given? - rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") - end - end -end diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb deleted file mode 100644 index 44da87cb00c..00000000000 --- a/app/services/ci/fetch_kubernetes_token_service.rb +++ /dev/null @@ -1,72 +0,0 @@ -## -# TODO: -# Almost components in this class were copied from app/models/project_services/kubernetes_service.rb -# We should dry up those classes not to repeat the same code. -# Maybe we should have a special facility (e.g. lib/kubernetes_api) to maintain all Kubernetes API caller. -module Ci - class FetchKubernetesTokenService - attr_reader :api_url, :ca_pem, :username, :password - - def initialize(api_url, ca_pem, username, password) - @api_url = api_url - @ca_pem = ca_pem - @username = username - @password = password - end - - def execute - read_secrets.each do |secret| - name = secret.dig('metadata', 'name') - if /default-token/ =~ name - token_base64 = secret.dig('data', 'token') - return Base64.decode64(token_base64) if token_base64 - end - end - - nil - end - - private - - def read_secrets - kubeclient = build_kubeclient! - - kubeclient.get_secrets.as_json - rescue KubeException => err - raise err unless err.error_code == 404 - [] - end - - def build_kubeclient!(api_path: 'api', api_version: 'v1') - raise "Incomplete settings" unless api_url && username && password - - ::Kubeclient::Client.new( - join_api_url(api_path), - api_version, - auth_options: { username: username, password: password }, - ssl_options: kubeclient_ssl_options, - http_proxy_uri: ENV['http_proxy'] - ) - end - - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s - end - - def kubeclient_ssl_options - opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - - if ca_pem.present? - opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) - end - - opts - end - end -end diff --git a/app/services/ci/finalize_cluster_creation_service.rb b/app/services/ci/finalize_cluster_creation_service.rb deleted file mode 100644 index 347875c5697..00000000000 --- a/app/services/ci/finalize_cluster_creation_service.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Ci - class FinalizeClusterCreationService - def execute(cluster) - api_client = - GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil) - - begin - gke_cluster = api_client.projects_zones_clusters_get( - cluster.gcp_project_id, - cluster.gcp_cluster_zone, - cluster.gcp_cluster_name) - rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") - end - - endpoint = gke_cluster.endpoint - api_url = 'https://' + endpoint - ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) - username = gke_cluster.master_auth.username - password = gke_cluster.master_auth.password - - kubernetes_token = Ci::FetchKubernetesTokenService.new( - api_url, ca_cert, username, password).execute - - unless kubernetes_token - return cluster.make_errored!('Failed to get a default token of kubernetes') - end - - Ci::IntegrateClusterService.new.execute( - cluster, endpoint, ca_cert, kubernetes_token, username, password) - end - end -end diff --git a/app/services/ci/integrate_cluster_service.rb b/app/services/ci/integrate_cluster_service.rb deleted file mode 100644 index d123ce8d26b..00000000000 --- a/app/services/ci/integrate_cluster_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Ci - class IntegrateClusterService - def execute(cluster, endpoint, ca_cert, token, username, password) - Gcp::Cluster.transaction do - cluster.update!( - enabled: true, - endpoint: endpoint, - ca_cert: ca_cert, - kubernetes_token: token, - username: username, - password: password, - service: cluster.project.find_or_initialize_service('kubernetes'), - status_event: :make_created) - - cluster.service.update!( - active: true, - api_url: cluster.api_url, - ca_pem: ca_cert, - namespace: cluster.project_namespace, - token: token) - end - rescue ActiveRecord::RecordInvalid => e - cluster.make_errored!("Failed to integrate cluster into kubernetes_service: #{e.message}") - end - end -end diff --git a/app/services/ci/provision_cluster_service.rb b/app/services/ci/provision_cluster_service.rb deleted file mode 100644 index 52d80b01813..00000000000 --- a/app/services/ci/provision_cluster_service.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Ci - class ProvisionClusterService - def execute(cluster) - api_client = - GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil) - - begin - operation = api_client.projects_zones_clusters_create( - cluster.gcp_project_id, - cluster.gcp_cluster_zone, - cluster.gcp_cluster_name, - cluster.gcp_cluster_size, - machine_type: cluster.gcp_machine_type) - rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") - end - - unless operation.status == 'RUNNING' || operation.status == 'PENDING' - return cluster.make_errored!("Operation status is unexpected; #{operation.status_message}") - end - - cluster.gcp_operation_id = api_client.parse_operation_id(operation.self_link) - - unless cluster.gcp_operation_id - return cluster.make_errored!('Can not find operation_id from self_link') - end - - if cluster.make_creating - WaitForClusterCreationWorker.perform_in( - WaitForClusterCreationWorker::INITIAL_INTERVAL, cluster.id) - else - return cluster.make_errored!("Failed to update cluster record; #{cluster.errors}") - end - end - end -end diff --git a/app/services/ci/update_cluster_service.rb b/app/services/ci/update_cluster_service.rb deleted file mode 100644 index 70d88fca660..00000000000 --- a/app/services/ci/update_cluster_service.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Ci - class UpdateClusterService < BaseService - def execute(cluster) - Gcp::Cluster.transaction do - cluster.update!(params) - - if params['enabled'] == 'true' - cluster.service.update!( - active: true, - api_url: cluster.api_url, - ca_pem: cluster.ca_cert, - namespace: cluster.project_namespace, - token: cluster.kubernetes_token) - else - cluster.service.update!(active: false) - end - end - rescue ActiveRecord::RecordInvalid => e - cluster.errors.add(:base, e.message) - end - end -end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb new file mode 100644 index 00000000000..5429bc21256 --- /dev/null +++ b/app/services/clusters/create_service.rb @@ -0,0 +1,14 @@ +module Clusters + class CreateService < BaseService + def execute(access_token) + params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE + + cluster_params = + params.merge(user: current_user) + + project.create_cluster(cluster_params).tap do |cluster| + ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? + end + end + end +end diff --git a/app/services/clusters/gcp/fetch_operation_service.rb b/app/services/clusters/gcp/fetch_operation_service.rb new file mode 100644 index 00000000000..013225efac4 --- /dev/null +++ b/app/services/clusters/gcp/fetch_operation_service.rb @@ -0,0 +1,16 @@ +module Clusters + module Gcp + class FetchOperationService + def execute(provider) + operation = provider.api_client.projects_zones_operations( + provider.project_id, + provider.cluster_zone, + provider.operation_id) + + yield(operation) if block_given? + rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e + return provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") + end + end + end +end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb new file mode 100644 index 00000000000..b536285b368 --- /dev/null +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -0,0 +1,66 @@ +module Clusters + module Gcp + class FinalizeCreationService + attr_reader :provider + + def execute(provider) + @provider = provider + + configure_provider + configure_kubernetes_platform + request_kuberenetes_platform_token + + ActiveRecord::Base.transaction do + kubernetes_platform.update! + provider.make_created! + end + rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e + return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") + rescue ActiveRecord::RecordInvalid => e + cluster.make_errored!("Failed to configure GKE Cluster: #{e.message}") + end + + private + + def configure_provider + provider.endpoint = gke_cluster.endpoint + end + + def configure_kubernetes_platform + kubernetes_platform = cluster.kubernetes_platform + kubernetes_platform.api_url = 'https://' + endpoint + kubernetes_platform.ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) + kubernetes_platform.username = gke_cluster.master_auth.username + kubernetes_platform.password = gke_cluster.master_auth.password + end + + def request_kuberenetes_platform_token + kubernetes_platform.read_secrets.each do |secret| + name = secret.dig('metadata', 'name') + if /default-token/ =~ name + token_base64 = secret.dig('data', 'token') + if token_base64 + kubernetes_platform.token = Base64.decode64(token_base64) + break + end + end + end + end + + def gke_cluster + @gke_cluster ||= provider.api_client.projects_zones_clusters_get( + provider.gcp_project_id, + provider.gcp_cluster_zone, + provider.gcp_cluster_name) + end + + def cluster + provider.cluster + end + + def kubernetes_platform + cluster.kubernetes_platform + end + end + end +end diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb new file mode 100644 index 00000000000..269705000ac --- /dev/null +++ b/app/services/clusters/gcp/provision_service.rb @@ -0,0 +1,49 @@ +module Clusters + module Gcp + class ProvisionService + attr_reader :provider + + def execute(provider) + @provider = provider + + unless operation.status == 'RUNNING' || operation.status == 'PENDING' + return provider.make_errored!("Operation status is unexpected; #{operation.status_message}") + end + + provider.operation_id = operation_id + + unless provider.operation_id + return provider.make_errored!('Can not find operation_id from self_link') + end + + if provider.make_creating + WaitForClusterCreationWorker.perform_in( + WaitForClusterCreationWorker::INITIAL_INTERVAL, provider.id) + else + return provider.make_errored!("Failed to update provider record; #{provider.errors}") + end + rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e + return provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") + end + + private + + def operation_id + api_client.parse_operation_id(operation.self_link) + end + + def operation + @operation ||= api_client.projects_zones_providers_create( + provider.project_id, + provider.provider_zone, + provider.provider_name, + provider.provider_size, + machine_type: provider.machine_type) + end + + def api_client + provider.api_client + end + end + end +end diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb new file mode 100644 index 00000000000..466ea986516 --- /dev/null +++ b/app/services/clusters/gcp/verify_provision_status_service.rb @@ -0,0 +1,44 @@ +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 '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 TIMEOUT < Time.now.utc - operation.start_time.to_time.utc + return provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}") + end + + WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id) + end + + def finalize_creation + Clusters::Gcp::FinalizeCreationService.new.execute(provider) + end + + def request_operation(&blk) + Clusters::FetchGcpOperationService.new.execute(provider, &blk) + end + end + end +end diff --git a/app/services/clusters/update_service.rb b/app/services/clusters/update_service.rb new file mode 100644 index 00000000000..989218e32a2 --- /dev/null +++ b/app/services/clusters/update_service.rb @@ -0,0 +1,7 @@ +module Clusters + class UpdateService < BaseService + def execute(cluster) + cluster.update(params) + end + end +end diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb index 63300b58a25..79f0d73c396 100644 --- a/app/workers/cluster_provision_worker.rb +++ b/app/workers/cluster_provision_worker.rb @@ -3,8 +3,10 @@ class ClusterProvisionWorker include ClusterQueue def perform(cluster_id) - Gcp::Cluster.find_by_id(cluster_id).try do |cluster| - Ci::ProvisionClusterService.new.execute(cluster) + Clusters::Cluster.find_by_id(cluster_id).try do |cluster| + cluster.gcp_provider.try do |provider| + Clusters::Gcp::ProvisionService.new.execute(provider) + end end end end diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb index 5aa3bbdaa9d..d8c42c6bd55 100644 --- a/app/workers/wait_for_cluster_creation_worker.rb +++ b/app/workers/wait_for_cluster_creation_worker.rb @@ -2,25 +2,10 @@ class WaitForClusterCreationWorker include Sidekiq::Worker include ClusterQueue - INITIAL_INTERVAL = 2.minutes - EAGER_INTERVAL = 10.seconds - TIMEOUT = 20.minutes - def perform(cluster_id) - Gcp::Cluster.find_by_id(cluster_id).try do |cluster| - Ci::FetchGcpOperationService.new.execute(cluster) do |operation| - case operation.status - when 'RUNNING' - if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc - return cluster.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}") - end - - WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, cluster.id) - when 'DONE' - Ci::FinalizeClusterCreationService.new.execute(cluster) - else - return cluster.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}") - end + Clusters::Cluster.find_by_id(cluster_id).try do |cluster| + cluster.gcp_provider.try do |provider| + Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) end end end diff --git a/db/migrate/20171013094327_create_clusters.rb b/db/migrate/20171013094327_create_clusters.rb new file mode 100644 index 00000000000..ad30181f984 --- /dev/null +++ b/db/migrate/20171013094327_create_clusters.rb @@ -0,0 +1,66 @@ +class CreateGcpClusters < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :clusters do |t| + t.references :user, foreign_key: { on_delete: :nullify } + + t.boolean :enabled, default: true + + t.integer :provider_type, null: false + t.integer :platform_type, null: false + + t.datetime_with_timezone :created_at, null: false + t.datetime_with_timezone :updated_at, null: false + end + + create_table :cluster_projects do |t| + t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + 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 + end + + create_table :cluster_kubernetes_platforms do |t| + t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + + t.string :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 + + t.datetime_with_timezone :created_at, null: false + t.datetime_with_timezone :updated_at, null: false + end + + create_table :cluster_gcp_providers do |t| + t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + + t.integer :status + t.text :status_reason + + t.string :project_id, null: false + t.string :cluster_zone, null: false + t.string :cluster_name, null: false + t.integer :cluster_size, null: false + t.string :machine_type + t.string :operation_id + + t.string :endpoint + + t.text :encrypted_access_token + t.string :encrypted_access_token_iv + + t.datetime_with_timezone :created_at, null: false + t.datetime_with_timezone :updated_at, null: false + end + end +end -- cgit v1.2.1 From d0cff7f5855f91b5479f9fdaa39d8d95ec691a9e Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Mon, 23 Oct 2017 11:36:35 +0300 Subject: This works --- app/controllers/projects/clusters_controller.rb | 26 ++++--- app/models/clusters/cluster.rb | 52 ++++++++------ app/models/clusters/cluster_project.rb | 6 -- app/models/clusters/platforms/kubernetes.rb | 39 +++++----- app/models/clusters/project.rb | 8 +++ app/models/clusters/providers/gcp.rb | 29 ++++---- app/models/project.rb | 4 +- app/policies/clusters/cluster_policy.rb | 12 ++++ app/policies/gcp/cluster_policy.rb | 12 ---- app/presenters/clusters/cluster_presenter.rb | 9 +++ app/presenters/gcp/cluster_presenter.rb | 9 --- app/services/clusters/create_service.rb | 35 +++++++-- .../clusters/gcp/fetch_operation_service.rb | 6 +- .../clusters/gcp/finalize_creation_service.rb | 38 +++++----- app/services/clusters/gcp/provision_service.rb | 54 +++++++------- .../gcp/verify_provision_status_service.rb | 14 ++-- app/validators/cluster_name_validator.rb | 24 +++++++ app/views/projects/clusters/_form.html.haml | 44 ++++++------ app/views/projects/clusters/show.html.haml | 6 +- app/workers/cluster_provision_worker.rb | 2 +- app/workers/wait_for_cluster_creation_worker.rb | 2 +- db/migrate/20171013094327_create_clusters.rb | 66 ----------------- ...1013094327_create_new_clusters_architectures.rb | 66 +++++++++++++++++ ...cp_clusters_to_new_clusters_architectures.rb.rb | 84 ++++++++++++++++++++++ db/schema.rb | 61 +++++++++++++++- lib/gitlab/gcp/model.rb | 13 ---- spec/factories/gcp/cluster.rb | 68 +++++++++--------- 27 files changed, 496 insertions(+), 293 deletions(-) delete mode 100644 app/models/clusters/cluster_project.rb create mode 100644 app/models/clusters/project.rb create mode 100644 app/policies/clusters/cluster_policy.rb delete mode 100644 app/policies/gcp/cluster_policy.rb create mode 100644 app/presenters/clusters/cluster_presenter.rb delete mode 100644 app/presenters/gcp/cluster_presenter.rb create mode 100644 app/validators/cluster_name_validator.rb delete mode 100644 db/migrate/20171013094327_create_clusters.rb create mode 100644 db/migrate/20171013094327_create_new_clusters_architectures.rb create mode 100644 db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb.rb delete mode 100644 lib/gitlab/gcp/model.rb diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 0679d8c69f0..c07d955f148 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -27,11 +27,17 @@ class Projects::ClustersController < Projects::ApplicationController end def new - @cluster = project.build_cluster + @cluster = Clusters::Cluster.new( + platform_type: :kubernetes, + provider_type: :gcp).tap do |cluster| + cluster.build_provider_gcp + cluster.build_platform_kubernetes + cluster.projects << project + end end def create - @cluster = Ci::CreateService + @cluster = Clusters::CreateService .new(project, current_user, create_params) .execute(token_in_session) @@ -58,7 +64,7 @@ class Projects::ClustersController < Projects::ApplicationController end def update - Ci::UpdateClusterService + Clusters::UpdateService .new(project, current_user, update_params) .execute(cluster) @@ -89,16 +95,16 @@ class Projects::ClustersController < Projects::ApplicationController def create_params params.require(:cluster).permit( :enabled, + :name, :platform_type, :provider_type, - kubernetes_platform: [ + platform_kubernetes_attributes: [ :namespace ], - gcp_provider: [ - :project_id, - :cluster_zone, - :cluster_name, - :cluster_size, + provider_gcp_attributes: [ + :gcp_project_id, + :zone, + :num_nodes, :machine_type ]) end @@ -106,7 +112,7 @@ class Projects::ClustersController < Projects::ApplicationController def update_params params.require(:cluster).permit( :enabled, - kubernetes_platform: [ + platform_kubernetes_attributes: [ :namespace ]) end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index d7b13ac88f2..f1eedad8795 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -2,49 +2,46 @@ module Clusters class Cluster < ActiveRecord::Base include Presentable + self.table_name = 'clusters' + belongs_to :user - belongs_to :service - enum :platform_type { + enum platform_type: { kubernetes: 1 } - enum :provider_type { + enum provider_type: { user: 0, gcp: 1 } - has_many :cluster_projects - has_many :projects, through: :cluster_projects + has_many :cluster_projects, class_name: 'Clusters::Project' + has_many :projects, through: :cluster_projects, class_name: '::Project' - has_one :gcp_provider - has_one :kubernetes_platform + has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp' + has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes' - accepts_nested_attributes_for :gcp_provider - accepts_nested_attributes_for :kubernetes_platform + accepts_nested_attributes_for :provider_gcp + accepts_nested_attributes_for :platform_kubernetes - validates :kubernetes_platform, presence: true, if: :kubernetes? - validates :gcp_provider, presence: true, if: :gcp? + validates :name, cluster_name: true validate :restrict_modification, on: :update delegate :status, to: :provider, allow_nil: true delegate :status_reason, to: :provider, allow_nil: true - - def restrict_modification - if provider&.on_creation? - errors.add(:base, "cannot modify during creation") - return false - end - - true - end + delegate :status_name, to: :provider, allow_nil: true + delegate :on_creation?, to: :provider, allow_nil: true def provider - return gcp_provider if gcp? + return provider_gcp if gcp? end def platform - return kubernetes_platform if kubernetes? + return platform_kubernetes if kubernetes? + end + + def project + first_project end def first_project @@ -52,5 +49,16 @@ module Clusters @first_project = projects.first end + + private + + def restrict_modification + if provider&.on_creation? + errors.add(:base, "cannot modify during creation") + return false + end + + true + end end end diff --git a/app/models/clusters/cluster_project.rb b/app/models/clusters/cluster_project.rb deleted file mode 100644 index 7b139c2bb08..00000000000 --- a/app/models/clusters/cluster_project.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Clusters - class ClusterProject < ActiveRecord::Base - belongs_to :cluster - belongs_to :project - end -end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index aed6f733487..d9f8927f7cc 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -4,11 +4,13 @@ module Clusters include Gitlab::Kubernetes include ReactiveCaching + self.table_name = 'cluster_platforms_kubernetes' + TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze - self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } + self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.cluster_id] } - belongs_to :cluster + belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster' attr_encrypted :password, mode: :per_attribute_iv, @@ -28,8 +30,8 @@ module Clusters message: Gitlab::Regex.kubernetes_namespace_regex_message } - validates :api_url, url: true, presence: true - validates :token, presence: true + validates :api_url, url: true, presence: true, on: :update + validates :token, presence: true, on: :update after_save :clear_reactive_cache! @@ -53,9 +55,9 @@ module Clusters { key: 'KUBECONFIG', value: config, public: false, file: true } ] - if ca_pem.present? - variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } - variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } + if ca_cert.present? + variables << { key: 'KUBE_CA_PEM', value: ca_cert, public: true } + variables << { key: 'KUBE_CA_PEM_FILE', value: ca_cert, public: true, file: true } end variables @@ -76,7 +78,7 @@ module Clusters # Caches resources in the namespace so other calls don't need to block on # network access def calculate_reactive_cache - return unless active? && project && !project.pending_delete? + return unless active? && cluster.project && !cluster.project.pending_delete? # We may want to cache extra things in the future { pods: read_pods } @@ -87,15 +89,16 @@ module Clusters url: api_url, namespace: actual_namespace, token: token, - ca_pem: ca_pem) + ca_pem: ca_cert) end def namespace_placeholder default_namespace || TEMPLATE_PLACEHOLDER end - def default_namespace - "#{cluster.first_project.path}-#{cluster.first_project.id}" if cluster.first_project + def default_namespace(project = nil) + project ||= cluster&.project + "#{project.path}-#{project.id}" if project end def read_secrets @@ -120,9 +123,9 @@ module Clusters def kubeclient_ssl_options opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - if ca_pem.present? + if ca_cert.present? opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_cert)) end opts @@ -131,7 +134,11 @@ module Clusters private def build_kubeclient!(api_path: 'api', api_version: 'v1') - raise "Incomplete settings" unless api_url && actual_namespace && token + raise "Incomplete settings" unless api_url && actual_namespace + + unless (username && password) || token + raise "Either username/password or token is required to access API" + end ::Kubeclient::Client.new( join_api_url(api_path), @@ -143,7 +150,7 @@ module Clusters end def kubeclient_auth_options - return { username: username, password: password } if username + return { username: username, password: password } if username && password return { bearer_token: token } if token end @@ -159,7 +166,7 @@ module Clusters def terminal_auth { token: token, - ca_pem: ca_pem, + ca_pem: ca_cert, max_session_time: current_application_settings.terminal_max_session_time } end diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb new file mode 100644 index 00000000000..69088100420 --- /dev/null +++ b/app/models/clusters/project.rb @@ -0,0 +1,8 @@ +module Clusters + class Project < ActiveRecord::Base + self.table_name = 'cluster_projects' + + belongs_to :cluster, inverse_of: :projects, class_name: 'Clusters::Cluster' + belongs_to :project, inverse_of: :project, class_name: 'Project' + end +end diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index 5d4618cfe87..e4f109d2794 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -1,10 +1,12 @@ module Clusters module Providers class Gcp < ActiveRecord::Base - belongs_to :cluster + self.table_name = 'cluster_providers_gcp' - default_value_for :cluster_zone, 'us-central1-a' - default_value_for :cluster_size, 3 + 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, @@ -12,23 +14,16 @@ module Clusters key: Gitlab::Application.secrets.db_key_base, algorithm: 'aes-256-cbc' - validates :project_id, + validates :gcp_project_id, length: 1..63, format: { with: Gitlab::Regex.kubernetes_namespace_regex, message: Gitlab::Regex.kubernetes_namespace_regex_message } - validates :cluster_name, - length: 1..63, - format: { - with: Gitlab::Regex.kubernetes_namespace_regex, - message: Gitlab::Regex.kubernetes_namespace_regex_message - } - - validates :cluster_zone, presence: true + validates :zone, presence: true - validates :cluster_size, + validates :num_nodes, presence: true, numericality: { only_integer: true, @@ -54,9 +49,13 @@ module Clusters end before_transition any => [:errored, :created] do |provider| - provider.token = nil + provider.access_token = nil provider.operation_id = nil - provider.save! + end + + before_transition any => [:creating] do |provider, transition| + operation_id = transition.args.first + provider.operation_id = operation_id if operation_id end before_transition any => [:errored] do |provider, transition| diff --git a/app/models/project.rb b/app/models/project.rb index bc263b63881..70c75edcda3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -178,8 +178,8 @@ class Project < ActiveRecord::Base has_one :project_feature, inverse_of: :project has_one :statistics, class_name: 'ProjectStatistics' - has_many :cluster_projects, class_name: 'Clusters::ClusterProject' - has_one :cluster, through: :cluster_projects + has_one :cluster_project, class_name: 'Clusters::Project' + has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster' # Container repositories need to remove data from the container registry, # which is not managed by the DB. Hence we're still using dependent: :destroy diff --git a/app/policies/clusters/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb new file mode 100644 index 00000000000..1f7c13072b9 --- /dev/null +++ b/app/policies/clusters/cluster_policy.rb @@ -0,0 +1,12 @@ +module Clusters + class ClusterPolicy < BasePolicy + alias_method :cluster, :subject + + delegate { cluster.project } + + rule { can?(:master_access) }.policy do + enable :update_cluster + enable :admin_cluster + end + end +end diff --git a/app/policies/gcp/cluster_policy.rb b/app/policies/gcp/cluster_policy.rb deleted file mode 100644 index e77173ea6e1..00000000000 --- a/app/policies/gcp/cluster_policy.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Gcp - class ClusterPolicy < BasePolicy - alias_method :cluster, :subject - - delegate { @subject.project } - - rule { can?(:master_access) }.policy do - enable :update_cluster - enable :admin_cluster - end - end -end diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb new file mode 100644 index 00000000000..01cb59d0d44 --- /dev/null +++ b/app/presenters/clusters/cluster_presenter.rb @@ -0,0 +1,9 @@ +module Clusters + class ClusterPresenter < Gitlab::View::Presenter::Delegated + presents :cluster + + def gke_cluster_url + "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp? + end + end +end diff --git a/app/presenters/gcp/cluster_presenter.rb b/app/presenters/gcp/cluster_presenter.rb deleted file mode 100644 index f7908f92a37..00000000000 --- a/app/presenters/gcp/cluster_presenter.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Gcp - class ClusterPresenter < Gitlab::View::Presenter::Delegated - presents :cluster - - def gke_cluster_url - "https://console.cloud.google.com/kubernetes/clusters/details/#{gcp_cluster_zone}/#{gcp_cluster_name}" - end - end -end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index 5429bc21256..3f458e25c14 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -1,14 +1,39 @@ module Clusters class CreateService < BaseService - def execute(access_token) - params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE + attr_reader :access_token - cluster_params = - params.merge(user: current_user) + def execute(access_token) + @access_token = access_token - project.create_cluster(cluster_params).tap do |cluster| + create_cluster.tap do |cluster| ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? end end + + private + + def create_cluster + cluster = nil + + ActiveRecord::Base.transaction do + cluster = Clusters::Cluster.create!(cluster_params) + cluster.projects << project + end + + cluster + rescue ActiveRecord::RecordInvalid => e + e.record + end + + def cluster_params + return @cluster_params if defined?(@cluster_params) + + params[:provider_gcp_attributes][:machine_type] ||= + GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE + + params[:provider_gcp_attributes][:access_token] ||= access_token + + @cluster_params = params.merge(user: current_user) + end end end diff --git a/app/services/clusters/gcp/fetch_operation_service.rb b/app/services/clusters/gcp/fetch_operation_service.rb index 013225efac4..a4cd3ca5c11 100644 --- a/app/services/clusters/gcp/fetch_operation_service.rb +++ b/app/services/clusters/gcp/fetch_operation_service.rb @@ -3,13 +3,13 @@ module Clusters class FetchOperationService def execute(provider) operation = provider.api_client.projects_zones_operations( - provider.project_id, - provider.cluster_zone, + 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 - return provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") + provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") end end end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index b536285b368..9f257bd0e9a 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -7,15 +7,14 @@ module Clusters @provider = provider configure_provider - configure_kubernetes_platform - request_kuberenetes_platform_token + configure_kubernetes ActiveRecord::Base.transaction do - kubernetes_platform.update! + kubernetes.save! provider.make_created! end rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") + cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") rescue ActiveRecord::RecordInvalid => e cluster.make_errored!("Failed to configure GKE Cluster: #{e.message}") end @@ -26,23 +25,20 @@ module Clusters provider.endpoint = gke_cluster.endpoint end - def configure_kubernetes_platform - kubernetes_platform = cluster.kubernetes_platform - kubernetes_platform.api_url = 'https://' + endpoint - kubernetes_platform.ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) - kubernetes_platform.username = gke_cluster.master_auth.username - kubernetes_platform.password = gke_cluster.master_auth.password + def configure_kubernetes + kubernetes.api_url = 'https://' + gke_cluster.endpoint + kubernetes.ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) + kubernetes.username = gke_cluster.master_auth.username + kubernetes.password = gke_cluster.master_auth.password + kubernetes.token = request_kuberenetes_token end - def request_kuberenetes_platform_token - kubernetes_platform.read_secrets.each do |secret| + def request_kuberenetes_token + kubernetes.read_secrets.each do |secret| name = secret.dig('metadata', 'name') if /default-token/ =~ name token_base64 = secret.dig('data', 'token') - if token_base64 - kubernetes_platform.token = Base64.decode64(token_base64) - break - end + return Base64.decode64(token_base64) if token_base64 end end end @@ -50,16 +46,16 @@ module Clusters def gke_cluster @gke_cluster ||= provider.api_client.projects_zones_clusters_get( provider.gcp_project_id, - provider.gcp_cluster_zone, - provider.gcp_cluster_name) + provider.zone, + cluster.name) end def cluster - provider.cluster + @cluster ||= provider.cluster end - def kubernetes_platform - cluster.kubernetes_platform + def kubernetes + @kubernetes ||= cluster.platform_kubernetes end end end diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb index 269705000ac..8ec81e22203 100644 --- a/app/services/clusters/gcp/provision_service.rb +++ b/app/services/clusters/gcp/provision_service.rb @@ -6,43 +6,41 @@ module Clusters def execute(provider) @provider = provider - unless operation.status == 'RUNNING' || operation.status == 'PENDING' - return provider.make_errored!("Operation status is unexpected; #{operation.status_message}") + get_operation_id do |operation_id| + if provider.make_creating(operation_id) + WaitForClusterCreationWorker.perform_in( + Clusters::Gcp::VerifyProvisionStatusService::INITIAL_INTERVAL, + provider.id) + else + provider.make_errored!("Failed to update provider record; #{provider.errors}") + end end + end - provider.operation_id = operation_id + private - unless provider.operation_id - return provider.make_errored!('Can not find operation_id from self_link') - end + 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) - if provider.make_creating - WaitForClusterCreationWorker.perform_in( - WaitForClusterCreationWorker::INITIAL_INTERVAL, provider.id) - else - return provider.make_errored!("Failed to update provider record; #{provider.errors}") + unless operation.status == 'PENDING' || operation.status == 'RUNNING' + return provider.make_errored!("Operation status is unexpected; #{operation.status_message}") end - rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - return provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") - end - private + operation_id = provider.api_client.parse_operation_id(operation.self_link) - def operation_id - api_client.parse_operation_id(operation.self_link) - end + unless operation_id + return provider.make_errored!('Can not find operation_id from self_link') + end - def operation - @operation ||= api_client.projects_zones_providers_create( - provider.project_id, - provider.provider_zone, - provider.provider_name, - provider.provider_size, - machine_type: provider.machine_type) - end + yield(operation_id) - def api_client - provider.api_client + rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e + provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") end end end diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb index 466ea986516..bc33756f27c 100644 --- a/app/services/clusters/gcp/verify_provision_status_service.rb +++ b/app/services/clusters/gcp/verify_provision_status_service.rb @@ -12,7 +12,7 @@ module Clusters request_operation do |operation| case operation.status - when 'RUNNING' + when 'PENDING', 'RUNNING' continue_creation(operation) when 'DONE' finalize_creation @@ -25,11 +25,15 @@ module Clusters private def continue_creation(operation) - if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc - return provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}") + 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 - WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id) + def elapsed_time_from_creation(operation) + Time.now.utc - operation.start_time.to_time.utc end def finalize_creation @@ -37,7 +41,7 @@ module Clusters end def request_operation(&blk) - Clusters::FetchGcpOperationService.new.execute(provider, &blk) + Clusters::Gcp::FetchOperationService.new.execute(provider, &blk) end end end diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb new file mode 100644 index 00000000000..6c9850af30f --- /dev/null +++ b/app/validators/cluster_name_validator.rb @@ -0,0 +1,24 @@ +# ClusterNameValidator +# +# Custom validator for ClusterName. +class ClusterNameValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + if record.user? + unless value.present? + record.errors.add(attribute, " has to be present") + end + elsif record.gcp? + if record.persisted? && record.name != value + record.errors.add(attribute, " can not be changed because it's synchronized with provider") + end + + unless value.length >= 1 && value.length <= 63 + record.errors.add(attribute, " is invalid syntax") + end + + unless value =~ Gitlab::Regex.kubernetes_namespace_regex + record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message) + end + end + end +end diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml index 371cdb1e403..b3020513abf 100644 --- a/app/views/projects/clusters/_form.html.haml +++ b/app/views/projects/clusters/_form.html.haml @@ -4,34 +4,38 @@ - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page} - = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field| + = form_for @cluster, url: namespace_project_clusters_path(@project.namespace, @project, @cluster), as: :cluster do |field| + = field.hidden_field :platform_type, :value => 'kubernetes' + = field.hidden_field :provider_type, :value => 'gcp' = form_errors(@cluster) .form-group - = field.label :gcp_cluster_name, s_('ClusterIntegration|Cluster name') - = field.text_field :gcp_cluster_name, class: 'form-control' + = field.label :name, s_('ClusterIntegration|Cluster name') + = field.text_field :name, class: 'form-control' - .form-group - = field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') - = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer') - = field.text_field :gcp_project_id, class: 'form-control' + = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field| + .form-group + = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') + = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer') + = provider_gcp_field.text_field :gcp_project_id, class: 'form-control' - .form-group - = field.label :gcp_cluster_zone, s_('ClusterIntegration|Zone') - = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') - = field.text_field :gcp_cluster_zone, class: 'form-control', placeholder: 'us-central1-a' + .form-group + = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone') + = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') + = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a' - .form-group - = field.label :gcp_cluster_size, s_('ClusterIntegration|Number of nodes') - = field.text_field :gcp_cluster_size, class: 'form-control', placeholder: '3' + .form-group + = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes') + = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3' - .form-group - = field.label :gcp_machine_type, s_('ClusterIntegration|Machine type') - = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer') - = field.text_field :gcp_machine_type, class: 'form-control', placeholder: 'n1-standard-4' + .form-group + = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type') + = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer') + = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4' + = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .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 + = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') + = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: @cluster.platform_kubernetes.default_namespace(@project) .form-group = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save' diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index ff76abc3553..49adae82454 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -33,7 +33,7 @@ - else = s_('ClusterIntegration|Cluster integration is disabled for this project.') - = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field| + = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_errors(@cluster) .form-group.append-bottom-20 %label.append-bottom-10 @@ -62,9 +62,9 @@ %label.append-bottom-10{ for: 'cluter-name' } = s_('ClusterIntegration|Cluster name') .input-group - %input.form-control.cluster-name{ value: @cluster.gcp_cluster_name, disabled: true } + %input.form-control.cluster-name{ value: @cluster.name, disabled: true } %span.input-group-addon.clipboard-addon - = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name')) + = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name')) %section.settings#js-cluster-advanced-settings .settings-header diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb index 79f0d73c396..0929fffc444 100644 --- a/app/workers/cluster_provision_worker.rb +++ b/app/workers/cluster_provision_worker.rb @@ -4,7 +4,7 @@ class ClusterProvisionWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - cluster.gcp_provider.try do |provider| + cluster.provider_gcp.try do |provider| Clusters::Gcp::ProvisionService.new.execute(provider) end end diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb index d8c42c6bd55..b2f04869636 100644 --- a/app/workers/wait_for_cluster_creation_worker.rb +++ b/app/workers/wait_for_cluster_creation_worker.rb @@ -4,7 +4,7 @@ class WaitForClusterCreationWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - cluster.gcp_provider.try do |provider| + cluster.provider_gcp.try do |provider| Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) end end diff --git a/db/migrate/20171013094327_create_clusters.rb b/db/migrate/20171013094327_create_clusters.rb deleted file mode 100644 index ad30181f984..00000000000 --- a/db/migrate/20171013094327_create_clusters.rb +++ /dev/null @@ -1,66 +0,0 @@ -class CreateGcpClusters < ActiveRecord::Migration - DOWNTIME = false - - def change - create_table :clusters do |t| - t.references :user, foreign_key: { on_delete: :nullify } - - t.boolean :enabled, default: true - - t.integer :provider_type, null: false - t.integer :platform_type, null: false - - t.datetime_with_timezone :created_at, null: false - t.datetime_with_timezone :updated_at, null: false - end - - create_table :cluster_projects do |t| - t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } - 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 - end - - create_table :cluster_kubernetes_platforms do |t| - t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } - - t.string :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 - - t.datetime_with_timezone :created_at, null: false - t.datetime_with_timezone :updated_at, null: false - end - - create_table :cluster_gcp_providers do |t| - t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } - - t.integer :status - t.text :status_reason - - t.string :project_id, null: false - t.string :cluster_zone, null: false - t.string :cluster_name, null: false - t.integer :cluster_size, null: false - t.string :machine_type - t.string :operation_id - - t.string :endpoint - - t.text :encrypted_access_token - t.string :encrypted_access_token_iv - - t.datetime_with_timezone :created_at, null: false - t.datetime_with_timezone :updated_at, null: false - end - end -end diff --git a/db/migrate/20171013094327_create_new_clusters_architectures.rb b/db/migrate/20171013094327_create_new_clusters_architectures.rb new file mode 100644 index 00000000000..35df8cb4a60 --- /dev/null +++ b/db/migrate/20171013094327_create_new_clusters_architectures.rb @@ -0,0 +1,66 @@ +class CreateNewClustersArchitectures < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :clusters do |t| + t.references :user, foreign_key: { on_delete: :nullify } + + t.boolean :enabled, default: true + t.string :name, null: false # If manual, read-write. If gcp, read-only. + + t.integer :provider_type, null: false + t.integer :platform_type, null: false + + t.datetime_with_timezone :created_at, null: false + t.datetime_with_timezone :updated_at, null: false + 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.string :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 + + t.datetime_with_timezone :created_at, null: false + t.datetime_with_timezone :updated_at, null: false + 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.text :status_reason + + t.string :gcp_project_id, null: false + t.string :zone, null: false + t.integer :num_nodes, null: false + t.string :machine_type + t.string :operation_id + + t.string :endpoint + + t.text :encrypted_access_token + t.string :encrypted_access_token_iv + + t.datetime_with_timezone :created_at, null: false + t.datetime_with_timezone :updated_at, null: false + end + end +end diff --git a/db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb.rb b/db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb.rb new file mode 100644 index 00000000000..5510b036d24 --- /dev/null +++ b/db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb.rb @@ -0,0 +1,84 @@ +class MigrateGcpClustersToNewClustersArchitectures < ActiveRecord::Migration + DOWNTIME = false + + def up + # TODO: Chnage to something reaistic + ActiveRecord::Base.connection.select_rows('SELECT * from gcp_clusters;').each do |old_cluster| + id = old_cluster[0] + project_id = old_cluster[1] + user_id = old_cluster[2] + service_id = old_cluster[3] + status = old_cluster[4] + gcp_cluster_size = old_cluster[5] + created_at = old_cluster[6] + updated_at = old_cluster[7] + enabled = old_cluster[8] + status_reason = old_cluster[9] + project_namespace = old_cluster[10] + endpoint = old_cluster[11] + ca_cert = old_cluster[12] + encrypted_kubernetes_token = old_cluster[13] + encrypted_kubernetes_token_iv = old_cluster[14] + username = old_cluster[15] + encrypted_password = old_cluster[16] + encrypted_password_iv = old_cluster[17] + gcp_project_id = old_cluster[18] + gcp_cluster_zone = old_cluster[19] + gcp_cluster_name = old_cluster[20] + gcp_machine_type = old_cluster[21] + gcp_operation_id = old_cluster[22] + encrypted_gcp_token = old_cluster[23] + encrypted_gcp_token_iv = old_cluster[24] + + cluster = Clusters::Cluster.create!( + user_id: user_id, + enabled: enabled, + name: gcp_cluster_name, + provider_type: :gcp, + platform_type: :kubernetes, + created_at: created_at, + updated_at: updated_at) + + Clusters::Project.create!( + cluster: cluster, + project_id: project_id, + created_at: created_at, + updated_at: updated_at) + + Clusters::Platforms::Kubernetes.create!( + cluster: cluster, + api_url: 'https://' + endpoint, + ca_cert: ca_cert, + namespace: project_namespace, + username: username, + encrypted_password: encrypted_password, + encrypted_password_iv: encrypted_password_iv, + encrypted_token: encrypted_kubernetes_token, + encrypted_token_iv: encrypted_kubernetes_token_iv, + created_at: created_at, + updated_at: updated_at) + + Clusters::Providers::Gcp.create!( + cluster: cluster, + status: status, + status_reason: status_reason, + gcp_project_id: gcp_project_id, + zone: gcp_cluster_zone, + num_nodes: gcp_cluster_size, + machine_type: gcp_machine_type, + operation_id: gcp_operation_id, + endpoint: endpoint, + encrypted_access_token: encrypted_gcp_token, + encrypted_access_token_iv: encrypted_gcp_token_iv, + created_at: created_at, + updated_at: updated_at) + end + end + + def down + Clusters::Cluster.delete_all + Clusters::Project.delete_all + Clusters::Providers::Gcp.delete_all + Clusters::Platforms::Kubernetes.delete_all + end +end diff --git a/db/schema.rb b/db/schema.rb index c2c04873d4d..65e8a3120a1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171012101043) do +ActiveRecord::Schema.define(version: 20171013104327) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -460,6 +460,60 @@ ActiveRecord::Schema.define(version: 20171012101043) do add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree + create_table "cluster_platforms_kubernetes", force: :cascade do |t| + t.integer "cluster_id", null: false + t.string "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" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + 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 "created_at", null: false + t.datetime "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.text "status_reason" + t.string "gcp_project_id", null: false + t.string "zone", null: false + t.integer "num_nodes", null: false + t.string "machine_type" + t.string "operation_id" + t.string "endpoint" + t.text "encrypted_access_token" + t.string "encrypted_access_token_iv" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + 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.boolean "enabled", default: true + t.string "name", null: false + t.integer "provider_type", null: false + t.integer "platform_type", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "container_repositories", force: :cascade do |t| t.integer "project_id", null: false t.string "name", null: false @@ -1808,6 +1862,11 @@ ActiveRecord::Schema.define(version: 20171012101043) do add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade + add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade + add_foreign_key "cluster_projects", "clusters", on_delete: :cascade + add_foreign_key "cluster_projects", "projects", on_delete: :cascade + add_foreign_key "cluster_providers_gcp", "clusters", on_delete: :cascade + add_foreign_key "clusters", "users", on_delete: :nullify add_foreign_key "container_repositories", "projects" add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade diff --git a/lib/gitlab/gcp/model.rb b/lib/gitlab/gcp/model.rb deleted file mode 100644 index 195391f0e3c..00000000000 --- a/lib/gitlab/gcp/model.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Gitlab - module Gcp - module Model - def table_name_prefix - "gcp_" - end - - def model_name - @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last) - end - end - end -end diff --git a/spec/factories/gcp/cluster.rb b/spec/factories/gcp/cluster.rb index 630e40da888..5c062737ffc 100644 --- a/spec/factories/gcp/cluster.rb +++ b/spec/factories/gcp/cluster.rb @@ -1,38 +1,38 @@ -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' +# 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 :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 :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 :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 +# trait :errored do +# status_event :make_errored +# status_reason 'general error' +# end +# end +# end -- cgit v1.2.1 From 478e59fe8d82b99800a2613aa4d153bf692fbd6b Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Mon, 30 Oct 2017 03:48:45 +0900 Subject: specs for models. Improved details. --- app/models/clusters/cluster.rb | 26 +- app/models/clusters/platforms/kubernetes.rb | 50 ++-- app/models/clusters/project.rb | 4 +- app/models/clusters/providers/gcp.rb | 3 +- app/validators/cluster_name_validator.rb | 2 +- app/views/projects/clusters/_form.html.haml | 2 +- spec/factories/clusters/cluster.rb | 39 +++ spec/factories/clusters/platforms/gcp.rb | 28 ++ spec/factories/clusters/providers/kubernetes.rb | 18 ++ spec/factories/gcp/cluster.rb | 38 --- spec/fixtures/clusters/sample_cert.pem | 33 +++ spec/models/clusters/cluster_spec.rb | 180 +++++++++++++ spec/models/clusters/platforms/kubernetes_spec.rb | 304 ++++++++++++++++++++++ spec/models/clusters/project_spec.rb | 6 + spec/models/clusters/providers/gcp_spec.rb | 183 +++++++++++++ spec/models/gcp/cluster_spec.rb | 264 ------------------- 16 files changed, 838 insertions(+), 342 deletions(-) create mode 100644 spec/factories/clusters/cluster.rb create mode 100644 spec/factories/clusters/platforms/gcp.rb create mode 100644 spec/factories/clusters/providers/kubernetes.rb delete mode 100644 spec/factories/gcp/cluster.rb create mode 100644 spec/fixtures/clusters/sample_cert.pem create mode 100644 spec/models/clusters/cluster_spec.rb create mode 100644 spec/models/clusters/platforms/kubernetes_spec.rb create mode 100644 spec/models/clusters/project_spec.rb create mode 100644 spec/models/clusters/providers/gcp_spec.rb delete mode 100644 spec/models/gcp/cluster_spec.rb diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index f1eedad8795..4260fadb46d 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -6,15 +6,6 @@ module Clusters belongs_to :user - enum platform_type: { - kubernetes: 1 - } - - enum provider_type: { - user: 0, - gcp: 1 - } - has_many :cluster_projects, class_name: 'Clusters::Project' has_many :projects, through: :cluster_projects, class_name: '::Project' @@ -32,6 +23,18 @@ module Clusters delegate :status_name, to: :provider, allow_nil: true delegate :on_creation?, to: :provider, 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 @@ -40,15 +43,12 @@ module Clusters return platform_kubernetes if kubernetes? end - def project - first_project - end - def first_project return @first_project if defined?(@first_project) @first_project = projects.first end + alias_method :project, :first_project private diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index d9f8927f7cc..b20b00ff51b 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -1,13 +1,11 @@ module Clusters module Platforms class Kubernetes < ActiveRecord::Base + include Gitlab::CurrentSettings include Gitlab::Kubernetes include ReactiveCaching self.table_name = 'cluster_platforms_kubernetes' - - TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze - self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.cluster_id] } belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster' @@ -22,6 +20,8 @@ module Clusters 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, @@ -34,8 +34,19 @@ module Clusters validates :token, presence: true, on: :update after_save :clear_reactive_cache! - - before_validation :enforce_namespace_to_lower_case + + alias_attribute :ca_pem, :ca_cert + + delegate :project, to: :cluster, allow_nil: true + delegate :enabled?, to: :cluster, allow_nil: true + + alias_method :active?, :enabled? + + class << self + def namespace_for_project(project) + "#{project.path}-#{project.id}" + end + end def actual_namespace if namespace.present? @@ -45,6 +56,10 @@ module Clusters end end + def default_namespace + self.class.namespace_for_project(project) if project + end + def predefined_variables config = YAML.dump(kubeconfig) @@ -55,9 +70,9 @@ module Clusters { key: 'KUBECONFIG', value: config, public: false, file: true } ] - if ca_cert.present? - variables << { key: 'KUBE_CA_PEM', value: ca_cert, public: true } - variables << { key: 'KUBE_CA_PEM_FILE', value: ca_cert, public: true, file: true } + if ca_pem.present? + variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } + variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } end variables @@ -78,7 +93,7 @@ module Clusters # Caches resources in the namespace so other calls don't need to block on # network access def calculate_reactive_cache - return unless active? && cluster.project && !cluster.project.pending_delete? + return unless active? && project && !project.pending_delete? # We may want to cache extra things in the future { pods: read_pods } @@ -89,16 +104,7 @@ module Clusters url: api_url, namespace: actual_namespace, token: token, - ca_pem: ca_cert) - end - - def namespace_placeholder - default_namespace || TEMPLATE_PLACEHOLDER - end - - def default_namespace(project = nil) - project ||= cluster&.project - "#{project.path}-#{project.id}" if project + ca_pem: ca_pem) end def read_secrets @@ -123,9 +129,9 @@ module Clusters def kubeclient_ssl_options opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - if ca_cert.present? + if ca_pem.present? opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_cert)) + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) end opts @@ -166,7 +172,7 @@ module Clusters def terminal_auth { token: token, - ca_pem: ca_cert, + ca_pem: ca_pem, max_session_time: current_application_settings.terminal_max_session_time } end diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb index 69088100420..eeb734b20b8 100644 --- a/app/models/clusters/project.rb +++ b/app/models/clusters/project.rb @@ -2,7 +2,7 @@ module Clusters class Project < ActiveRecord::Base self.table_name = 'cluster_projects' - belongs_to :cluster, inverse_of: :projects, class_name: 'Clusters::Cluster' - belongs_to :project, inverse_of: :project, class_name: 'Project' + belongs_to :cluster, class_name: 'Clusters::Cluster' + belongs_to :project, class_name: '::Project' end end diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index e4f109d2794..7700ba86f1a 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -55,7 +55,8 @@ module Clusters before_transition any => [:creating] do |provider, transition| operation_id = transition.args.first - provider.operation_id = operation_id if operation_id + raise 'operation_id is required' unless operation_id + provider.operation_id = operation_id end before_transition any => [:errored] do |provider, transition| diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb index 6c9850af30f..13ec342f399 100644 --- a/app/validators/cluster_name_validator.rb +++ b/app/validators/cluster_name_validator.rb @@ -8,7 +8,7 @@ class ClusterNameValidator < ActiveModel::EachValidator record.errors.add(attribute, " has to be present") end elsif record.gcp? - if record.persisted? && record.name != value + if record.persisted? && record.name_changed? record.errors.add(attribute, " can not be changed because it's synchronized with provider") end diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml index b3020513abf..6b9f63b7515 100644 --- a/app/views/projects/clusters/_form.html.haml +++ b/app/views/projects/clusters/_form.html.haml @@ -35,7 +35,7 @@ = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') - = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: @cluster.platform_kubernetes.default_namespace(@project) + = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: Clusters::Platforms::Kubernetes.namespace_for_project(@project) .form-group = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save' diff --git a/spec/factories/clusters/cluster.rb b/spec/factories/clusters/cluster.rb new file mode 100644 index 00000000000..8ba1eda8cc9 --- /dev/null +++ b/spec/factories/clusters/cluster.rb @@ -0,0 +1,39 @@ +FactoryGirl.define do + factory :cluster, class: Clusters::Cluster do + user + name 'test-cluster' + provider_type :user + platform_type :kubernetes + + 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 + end + + trait :provided_by_gcp do + provider_type :gcp + platform_type :kubernetes + platform_kubernetes + + provider_gcp do + create(:provider_gcp, :created) + end + end + + trait :providing_by_gcp do + provider_type :gcp + platform_type :kubernetes + + provider_gcp do + create(:provider_gcp, :creating) + end + end + end +end diff --git a/spec/factories/clusters/platforms/gcp.rb b/spec/factories/clusters/platforms/gcp.rb new file mode 100644 index 00000000000..c135bbb20a4 --- /dev/null +++ b/spec/factories/clusters/platforms/gcp.rb @@ -0,0 +1,28 @@ +FactoryGirl.define do + factory :provider_gcp, class: Clusters::Providers::Gcp do + cluster + gcp_project_id 'test-gcp-project' + + trait :creating do + access_token 'access_token_123' + + after(:build) do |gcp, evaluator| + gcp.make_creating('operation-123') + end + end + + trait :created do + endpoint '111.111.111.111' + + after(:build) do |gcp, evaluator| + gcp.make_created + end + end + + trait :errored do + after(:build) do |gcp, evaluator| + gcp.make_errored('Something wrong') + end + end + end +end diff --git a/spec/factories/clusters/providers/kubernetes.rb b/spec/factories/clusters/providers/kubernetes.rb new file mode 100644 index 00000000000..b4d413d32c1 --- /dev/null +++ b/spec/factories/clusters/providers/kubernetes.rb @@ -0,0 +1,18 @@ +FactoryGirl.define do + factory :platform_kubernetes, class: Clusters::Platforms::Kubernetes do + cluster + api_url 'https://kubernetes.example.com' + ca_cert nil + token 'a' * 40 + username 'xxxxxx' + password 'xxxxxx' + namespace nil + + trait :ca_cert do + after(:create) do |platform_kubernetes, evaluator| + pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) + platform_kubernetes.ca_cert = File.read(pem_file) + end + end + end +end diff --git a/spec/factories/gcp/cluster.rb b/spec/factories/gcp/cluster.rb deleted file mode 100644 index 5c062737ffc..00000000000 --- a/spec/factories/gcp/cluster.rb +++ /dev/null @@ -1,38 +0,0 @@ -# FactoryGirl.define do -# factory :gcp_cluster, class: Gcp::Cluster do -# project -# user -# enabled true -# gcp_project_id 'gcp-project-12345' -# gcp_cluster_name 'test-cluster' -# gcp_cluster_zone 'us-central1-a' -# gcp_cluster_size 1 -# gcp_machine_type 'n1-standard-4' - -# trait :with_kubernetes_service do -# after(:create) do |cluster, evaluator| -# create(:kubernetes_service, project: cluster.project).tap do |service| -# cluster.update(service: service) -# end -# end -# end - -# trait :custom_project_namespace do -# project_namespace 'sample-app' -# end - -# trait :created_on_gke do -# status_event :make_created -# endpoint '111.111.111.111' -# ca_cert 'xxxxxx' -# kubernetes_token 'xxxxxx' -# username 'xxxxxx' -# password 'xxxxxx' -# end - -# trait :errored do -# status_event :make_errored -# status_reason 'general error' -# end -# end -# end diff --git a/spec/fixtures/clusters/sample_cert.pem b/spec/fixtures/clusters/sample_cert.pem new file mode 100644 index 00000000000..e39a2b34416 --- /dev/null +++ b/spec/fixtures/clusters/sample_cert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAOutg3Kf2y5dMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTcxMDI5MTgxOTU3WhcNMTgxMDI5MTgxOTU3WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAvQysroM3TLxaavadSPnFIltrYnxCnU4PvCR8971HMWXsq7Z4ShU4BbbE +8yp7oUFjulSwW6DhdIvnQb8ihLKictLmrA0isQqrD/iNpKZ6/lI4DGWw4QzrvMnW +V4yy2QZNpg9tzQHd4+xkeeIoG23RijDU/sPd5dqxF+rPHBfCVInmYvSzLvMhneNj +Bt6gV02gU9e9hsnMatsDvEbvWKp7wcbPot0nWrfZulx2QAWyXy+zG9mJQUds6yc0 +4agAeT9JEb/xtRgR/kS0aUHSGnfSnhZiEn17s0PhTmbu7qSHgzgB+7oJrC9jPoUh +S2Wo3n0xykAjHrA8wC/Ddw3L38S41VQ58GEfNchistPswyMmXo/Oenv9P3s/kCOI +fndiksFNdqVo51y9Vjngj589hpOseFDyKmWPIEQZ9kxW/crjP6RZWWLHgz26KtxZ +uJaoYL8VBbYfrk/bucw0Ma2GEOp8rTsBE7SvgejXZa78q+381Kzc/utW6VwSXqzY +xeIitft0rXi17SZ+XoiTkIXtHn0ZwMtOXNDBADTpFmKa6wVACQilvcpOYD8gUHyH +pB+EDRdST3M4Fiq1MBAVhk8Lj3tHSJ/1ymeF1PWSu57AnJlzerzq2fcfPotNNd37 +ZPNkPh0kxPLwxbAyrHflzx9qVVdI1irY9055mNSnhzlec4qJ9cECAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUnVa5dYPoIG/3+qXml0bX8+N16GwwdQYDVR0jBG4wbIAUnVa5 +dYPoIG/3+qXml0bX8+N16GyhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDr +rYNyn9suXTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQAUg4cyxXi1 +VR8ejTpaAruRyJ1pEG9Kc3kiIRXODy60z3hJXnx9LkScPkWGiuL5XacfZ2rMd4bw +oVXIyi8U1UHWfAH8EZdrFKkU92jCiL5soHUONxLAvQEJ/FTR/qijrpzLCxXBdVQE +xFEDWUu6rxLFyjEwzwnRTLgpjR606fdb7qXHkuAMvZ/ezJj8j97hok3Odpn4lr2H +6hMTpK7HmDBX+kmdJJ+yBrm9hG1Pzpl7QU0dkxZ+qJNFjYMLnziiTwkv0c5ZaA9E +NykZUcOv3Sjb6spu1A/E2BSq4WTjkIjrogFlfimE1vmUmObTRJOqUB0Vky1kHEwN +pg7QqIJQmof1EAIaSM/YpUWXyumBwGLDUEud1JUz05In9Q4IZjEwZSJwbQW4fUia +A93m9rk3Lw3xsFcaUdPMFIXk0rPoF1IgmV/oqb0gK95lOWRLbN+AV8qpKPpcKXOc +TkIdFE47ZisEDhIdF6wC1izEMLeMEsPAO7/Y6MY4nRxsinSe95lRaw+yQpzx+mvJ +Q7n1kiHI9Pd5M3+CiQda0d/GO1o5ORJnUGJRvr9HKuNmE7Lif0As/N0AlywjzE7A +6Z8AEiWyRV1ffshu1k2UKmzvZuZeGGKRtrIjbJIRAtpRVtVZZGzhq5/sojCLoJ+u +texqFBUo/4mFRZa4pDItUdyOlDy2/LO/ag== +-----END CERTIFICATE----- diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb new file mode 100644 index 00000000000..e53ce8497f5 --- /dev/null +++ b/spec/models/clusters/cluster_spec.rb @@ -0,0 +1,180 @@ +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 respond_to :project } + + describe '.enabled' do + subject { described_class.enabled } + + let!(:cluster) { create(:cluster, enabled: true) } + + before do + create(:cluster, enabled: false) + end + + it { is_expected.to contain_exactly(cluster) } + end + + describe '.disabled' do + subject { described_class.disabled } + + let!(:cluster) { create(:cluster, enabled: false) } + + before do + create(:cluster, enabled: true) + end + + it { is_expected.to contain_exactly(cluster) } + end + + describe 'validation' do + subject { cluster.valid? } + + context 'when validates name' do + context 'when provided by user' do + let!(:cluster) { build(:cluster, :provided_by_user, name: name) } + + context 'when name is empty' do + let(:name) { '' } + + it { is_expected.to be_falsey } + end + + context 'when name is nil' do + let(:name) { nil } + + it { is_expected.to be_falsey } + end + + context 'when name is present' do + let(:name) { 'cluster-name-1' } + + it { is_expected.to be_truthy } + end + end + + context 'when provided by gcp' do + let!(:cluster) { build(:cluster, :provided_by_gcp, name: name) } + + context 'when name is shorter than 1' do + let(:name) { '' } + + it { is_expected.to be_falsey } + end + + context 'when name is longer than 63' do + let(:name) { 'a' * 64 } + + it { is_expected.to be_falsey } + end + + context 'when name includes invalid character' do + let(:name) { '!!!!!!' } + + it { is_expected.to be_falsey } + end + + context 'when name is present' do + let(:name) { 'cluster-name-1' } + + it { is_expected.to be_truthy } + end + + context 'when record is persisted' do + let(:name) { 'cluster-name-1' } + + before do + cluster.save! + end + + context 'when name is changed' do + before do + cluster.name = 'new-cluster-name' + end + + it { is_expected.to be_falsey } + end + + context 'when name is same' do + before do + cluster.name = name + end + + it { is_expected.to be_truthy } + end + end + end + end + + context 'when validates restrict_modification' do + context 'when creation is on going' do + let!(:cluster) { create(:cluster, :providing_by_gcp) } + + it { expect(cluster.update(enabled: false)).to be_falsey } + end + + context 'when creation is done' do + let!(:cluster) { create(:cluster, :provided_by_gcp) } + + it { expect(cluster.update(enabled: false)).to be_truthy } + end + end + end + + describe '#provider' do + subject { cluster.provider } + + context 'when provider is gcp' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + it 'returns a provider' do + is_expected.to eq(cluster.provider_gcp) + expect(subject.class.name.deconstantize).to eq(Clusters::Providers.to_s) + end + end + + context 'when provider is user' do + let(:cluster) { create(:cluster, :provided_by_user) } + + it { is_expected.to be_nil } + end + end + + describe '#platform' do + subject { cluster.platform } + + context 'when platform is kubernetes' do + let(:cluster) { create(:cluster, :provided_by_user) } + + it 'returns a platform' do + is_expected.to eq(cluster.platform_kubernetes) + expect(subject.class.name.deconstantize).to eq(Clusters::Platforms.to_s) + end + end + end + + describe '#first_project' do + subject { cluster.first_project } + + context 'when cluster belongs to a project' do + let(:cluster) { create(:cluster, :project) } + let(:project) { Clusters::Project.find_by_cluster_id(cluster.id).project } + + it { is_expected.to eq(project) } + end + + context 'when cluster does not belong to projects' do + let(:cluster) { create(:cluster) } + + it { is_expected.to be_nil } + end + end +end diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb new file mode 100644 index 00000000000..ec6ecee6ff2 --- /dev/null +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -0,0 +1,304 @@ +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 be_kind_of(Gitlab::Kubernetes) } + it { is_expected.to be_kind_of(ReactiveCaching) } + it { is_expected.to respond_to :ca_pem } + + describe 'before_validation' do + context 'when namespace includes upper case' do + let(:kubernetes) { create(:platform_kubernetes, 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(:platform_kubernetes, 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 + context 'when updates a record' do + let(:kubernetes) { create(:platform_kubernetes) } + + 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 creates a record' do + let(:kubernetes) { build(:platform_kubernetes) } + + before do + kubernetes.api_url = api_url + end + + context 'when api_url is nil' do + let(:api_url) { nil } + + it { expect(kubernetes.save).to be_truthy } + end + end + end + + context 'when validates token' do + context 'when updates a record' do + let(:kubernetes) { create(:platform_kubernetes) } + + before do + kubernetes.token = token + end + + context 'when token is nil' do + let(:token) { nil } + + it { expect(kubernetes.save).to be_falsey } + end + end + + context 'when creates a record' do + let(:kubernetes) { build(:platform_kubernetes) } + + before do + kubernetes.token = token + end + + context 'when token is nil' do + let(:token) { nil } + + it { expect(kubernetes.save).to be_truthy } + end + end + end + end + + describe '#actual_namespace' do + subject { kubernetes.actual_namespace } + + let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } + let(:kubernetes) { create(:platform_kubernetes, 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("#{cluster.project.path}-#{cluster.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(:platform_kubernetes) } + + context 'when cluster belongs to a project' do + let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } + + it { is_expected.to eq("#{cluster.project.path}-#{cluster.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 + + describe '#predefined_variables' do + let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } + let(:kubernetes) { create(:platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) } + let(:api_url) { 'https://kube.domain.com' } + let(:ca_pem) { 'CA PEM DATA' } + let(:token) { 'token' } + + let(:kubeconfig) do + config_file = expand_fixture_path('config/kubeconfig.yml') + config = YAML.load(File.read(config_file)) + config.dig('users', 0, 'user')['token'] = token + config.dig('contexts', 0, 'context')['namespace'] = namespace + config.dig('clusters', 0, 'cluster')['certificate-authority-data'] = + Base64.strict_encode64(ca_pem) + + YAML.dump(config) + end + + shared_examples 'setting variables' do + it 'sets the variables' do + expect(kubernetes.predefined_variables).to include( + { key: 'KUBE_URL', value: api_url, public: true }, + { key: 'KUBE_TOKEN', value: token, public: false }, + { key: 'KUBE_NAMESPACE', value: namespace, public: true }, + { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true }, + { key: 'KUBE_CA_PEM', value: ca_pem, public: true }, + { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } + ) + end + end + + context 'namespace is provided' do + let(:namespace) { 'my-project' } + + before do + kubernetes.namespace = namespace + end + + it_behaves_like 'setting variables' + end + + context 'no namespace provided' do + let(:namespace) { kubernetes.actual_namespace } + + it_behaves_like 'setting variables' + + it 'sets the KUBE_NAMESPACE' do + kube_namespace = kubernetes.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' } + + expect(kube_namespace).not_to be_nil + expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/) + end + end + end + + describe '#terminals' do + subject { service.terminals(environment) } + + let!(:cluster) { create(:cluster, :project, platform_kubernetes: service) } + let(:project) { cluster.project } + let(:service) { create(:platform_kubernetes) } + let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } + + context 'with invalid pods' do + it 'returns no terminals' do + stub_reactive_cache(service, pods: [{ "bad" => "pod" }]) + + is_expected.to be_empty + end + end + + context 'with valid pods' do + let(:pod) { kube_pod(app: environment.slug) } + let(:terminals) { kube_terminals(service, pod) } + + before do + stub_reactive_cache( + service, + pods: [pod, pod, kube_pod(app: "should-be-filtered-out")] + ) + end + + it 'returns terminals' do + is_expected.to eq(terminals + terminals) + end + + it 'uses max session time from settings' do + stub_application_setting(terminal_max_session_time: 600) + + times = subject.map { |terminal| terminal[:max_session_time] } + expect(times).to eq [600, 600, 600, 600] + end + end + end + + describe '#calculate_reactive_cache' do + subject { service.calculate_reactive_cache } + + let!(:cluster) { create(:cluster, :project, enabled: enabled, platform_kubernetes: service) } + let(:service) { create(:platform_kubernetes, :ca_cert) } + let(:enabled) { true } + + context 'when cluster is disabled' do + let(:enabled) { false } + + it { is_expected.to be_nil } + end + + context 'when kubernetes responds with valid pods' do + before do + stub_kubeclient_pods + end + + it { is_expected.to eq(pods: [kube_pod]) } + end + + context 'when kubernetes responds with 500s' do + before do + stub_kubeclient_pods(status: 500) + end + + it { expect { subject }.to raise_error(KubeException) } + end + + context 'when kubernetes responds with 404s' do + before do + stub_kubeclient_pods(status: 404) + end + + it { is_expected.to eq(pods: []) } + end + end +end diff --git a/spec/models/clusters/project_spec.rb b/spec/models/clusters/project_spec.rb new file mode 100644 index 00000000000..7d75d6ab345 --- /dev/null +++ b/spec/models/clusters/project_spec.rb @@ -0,0 +1,6 @@ +require 'spec_helper' + +describe Clusters::Project do + it { is_expected.to belong_to(:cluster) } + it { is_expected.to belong_to(:project) } +end diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb new file mode 100644 index 00000000000..99eb8c46e9a --- /dev/null +++ b/spec/models/clusters/providers/gcp_spec.rb @@ -0,0 +1,183 @@ +require 'spec_helper' + +describe Clusters::Providers::Gcp do + it { is_expected.to belong_to(:cluster) } + it { is_expected.to validate_presence_of(:zone) } + + describe 'default_value_for' do + let(:gcp) { build(: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(: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(: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(: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(: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(: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(: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(:provider_gcp, :creating) } + + it { is_expected.to be_truthy } + end + + context 'when status is created' do + let(:gcp) { create(: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(: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(:provider_gcp, :created) } + + it { is_expected.to be_nil } + end + + context 'when status is errored' do + let(:gcp) { build(:provider_gcp, :errored) } + + it { is_expected.to be_nil } + end + end +end diff --git a/spec/models/gcp/cluster_spec.rb b/spec/models/gcp/cluster_spec.rb deleted file mode 100644 index 8f39fff6394..00000000000 --- a/spec/models/gcp/cluster_spec.rb +++ /dev/null @@ -1,264 +0,0 @@ -require 'spec_helper' - -describe Gcp::Cluster do - it { is_expected.to belong_to(:project) } - it { is_expected.to belong_to(:user) } - it { is_expected.to belong_to(:service) } - - it { is_expected.to validate_presence_of(:gcp_cluster_zone) } - - describe '.enabled' do - subject { described_class.enabled } - - let!(:cluster) { create(:gcp_cluster, enabled: true) } - - before do - create(:gcp_cluster, enabled: false) - end - - it { is_expected.to contain_exactly(cluster) } - end - - describe '.disabled' do - subject { described_class.disabled } - - let!(:cluster) { create(:gcp_cluster, enabled: false) } - - before do - create(:gcp_cluster, enabled: true) - end - - it { is_expected.to contain_exactly(cluster) } - end - - describe '#default_value_for' do - let(:cluster) { described_class.new } - - it { expect(cluster.gcp_cluster_zone).to eq('us-central1-a') } - it { expect(cluster.gcp_cluster_size).to eq(3) } - it { expect(cluster.gcp_machine_type).to eq('n1-standard-4') } - end - - describe '#validates' do - subject { cluster.valid? } - - context 'when validates gcp_project_id' do - let(:cluster) { build(:gcp_cluster, gcp_project_id: gcp_project_id) } - - context 'when valid' do - let(:gcp_project_id) { 'gcp-project-12345' } - - it { is_expected.to be_truthy } - end - - context 'when empty' do - let(:gcp_project_id) { '' } - - it { is_expected.to be_falsey } - end - - context 'when too long' do - let(:gcp_project_id) { 'A' * 64 } - - it { is_expected.to be_falsey } - end - - context 'when includes abnormal character' do - let(:gcp_project_id) { '!!!!!!' } - - it { is_expected.to be_falsey } - end - end - - context 'when validates gcp_cluster_name' do - let(:cluster) { build(:gcp_cluster, gcp_cluster_name: gcp_cluster_name) } - - context 'when valid' do - let(:gcp_cluster_name) { 'test-cluster' } - - it { is_expected.to be_truthy } - end - - context 'when empty' do - let(:gcp_cluster_name) { '' } - - it { is_expected.to be_falsey } - end - - context 'when too long' do - let(:gcp_cluster_name) { 'A' * 64 } - - it { is_expected.to be_falsey } - end - - context 'when includes abnormal character' do - let(:gcp_cluster_name) { '!!!!!!' } - - it { is_expected.to be_falsey } - end - end - - context 'when validates gcp_cluster_size' do - let(:cluster) { build(:gcp_cluster, gcp_cluster_size: gcp_cluster_size) } - - context 'when valid' do - let(:gcp_cluster_size) { 1 } - - it { is_expected.to be_truthy } - end - - context 'when zero' do - let(:gcp_cluster_size) { 0 } - - it { is_expected.to be_falsey } - end - end - - context 'when validates project_namespace' do - let(:cluster) { build(:gcp_cluster, project_namespace: project_namespace) } - - context 'when valid' do - let(:project_namespace) { 'default-namespace' } - - it { is_expected.to be_truthy } - end - - context 'when empty' do - let(:project_namespace) { '' } - - it { is_expected.to be_truthy } - end - - context 'when too long' do - let(:project_namespace) { 'A' * 64 } - - it { is_expected.to be_falsey } - end - - context 'when includes abnormal character' do - let(:project_namespace) { '!!!!!!' } - - it { is_expected.to be_falsey } - end - end - - context 'when validates restrict_modification' do - let(:cluster) { create(:gcp_cluster) } - - before do - cluster.make_creating! - end - - context 'when created' do - before do - cluster.make_created! - end - - it { is_expected.to be_truthy } - end - - context 'when creating' do - it { is_expected.to be_falsey } - end - end - end - - describe '#state_machine' do - let(:cluster) { build(:gcp_cluster) } - - context 'when transits to created state' do - before do - cluster.gcp_token = 'tmp' - cluster.gcp_operation_id = 'tmp' - cluster.make_created! - end - - it 'nullify gcp_token and gcp_operation_id' do - expect(cluster.gcp_token).to be_nil - expect(cluster.gcp_operation_id).to be_nil - expect(cluster).to be_created - end - end - - context 'when transits to errored state' do - let(:reason) { 'something wrong' } - - before do - cluster.make_errored!(reason) - end - - it 'sets status_reason' do - expect(cluster.status_reason).to eq(reason) - expect(cluster).to be_errored - end - end - end - - describe '#project_namespace_placeholder' do - subject { cluster.project_namespace_placeholder } - - let(:cluster) { create(:gcp_cluster) } - - it 'returns a placeholder' do - is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}") - end - end - - describe '#on_creation?' do - subject { cluster.on_creation? } - - let(:cluster) { create(:gcp_cluster) } - - context 'when status is creating' do - before do - cluster.make_creating! - end - - it { is_expected.to be_truthy } - end - - context 'when status is created' do - before do - cluster.make_created! - end - - it { is_expected.to be_falsey } - end - end - - describe '#api_url' do - subject { cluster.api_url } - - let(:cluster) { create(:gcp_cluster, :created_on_gke) } - let(:api_url) { 'https://' + cluster.endpoint } - - it { is_expected.to eq(api_url) } - end - - describe '#restrict_modification' do - subject { cluster.restrict_modification } - - let(:cluster) { create(:gcp_cluster) } - - context 'when status is created' do - before do - cluster.make_created! - end - - it { is_expected.to be_truthy } - end - - context 'when status is creating' do - before do - cluster.make_creating! - end - - it { is_expected.to be_falsey } - - it 'sets error' do - is_expected.to be_falsey - expect(cluster.errors).not_to be_empty - end - end - end -end -- cgit v1.2.1 From d6744d98384192799c9b3a97ad0eaf69cb4d25ee Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Mon, 30 Oct 2017 21:55:18 +0900 Subject: specs for services. Improved details. --- app/models/clusters/cluster.rb | 2 +- app/models/clusters/platforms/kubernetes.rb | 3 - .../clusters/gcp/finalize_creation_service.rb | 8 +- spec/factories/clusters/cluster.rb | 14 ++- spec/factories/clusters/platforms/gcp.rb | 28 ----- spec/factories/clusters/platforms/kubernetes.rb | 21 ++++ spec/factories/clusters/providers/gcp.rb | 32 +++++ spec/factories/clusters/providers/kubernetes.rb | 18 --- spec/services/ci/create_cluster_service_spec.rb | 47 ------- .../ci/fetch_gcp_operation_service_spec.rb | 36 ------ .../ci/fetch_kubernetes_token_service_spec.rb | 64 ---------- .../ci/finalize_cluster_creation_service_spec.rb | 61 --------- spec/services/ci/integrate_cluster_service_spec.rb | 42 ------- spec/services/ci/provision_cluster_service_spec.rb | 85 ------------- spec/services/ci/update_cluster_service_spec.rb | 37 ------ spec/services/clusters/create_service_spec.rb | 62 +++++++++ .../clusters/gcp/fetch_operation_service_spec.rb | 43 +++++++ .../clusters/gcp/finalize_creation_service_spec.rb | 110 ++++++++++++++++ .../clusters/gcp/provision_service_spec.rb | 69 ++++++++++ .../gcp/verify_provision_status_service_spec.rb | 107 ++++++++++++++++ spec/services/clusters/update_service_spec.rb | 59 +++++++++ spec/support/google_api_helpers.rb | 140 +++++++++++++++++++++ spec/support/kubernetes_helpers.rb | 35 +++++- 23 files changed, 694 insertions(+), 429 deletions(-) delete mode 100644 spec/factories/clusters/platforms/gcp.rb create mode 100644 spec/factories/clusters/platforms/kubernetes.rb create mode 100644 spec/factories/clusters/providers/gcp.rb delete mode 100644 spec/factories/clusters/providers/kubernetes.rb delete mode 100644 spec/services/ci/create_cluster_service_spec.rb delete mode 100644 spec/services/ci/fetch_gcp_operation_service_spec.rb delete mode 100644 spec/services/ci/fetch_kubernetes_token_service_spec.rb delete mode 100644 spec/services/ci/finalize_cluster_creation_service_spec.rb delete mode 100644 spec/services/ci/integrate_cluster_service_spec.rb delete mode 100644 spec/services/ci/provision_cluster_service_spec.rb delete mode 100644 spec/services/ci/update_cluster_service_spec.rb create mode 100644 spec/services/clusters/create_service_spec.rb create mode 100644 spec/services/clusters/gcp/fetch_operation_service_spec.rb create mode 100644 spec/services/clusters/gcp/finalize_creation_service_spec.rb create mode 100644 spec/services/clusters/gcp/provision_service_spec.rb create mode 100644 spec/services/clusters/gcp/verify_provision_status_service_spec.rb create mode 100644 spec/services/clusters/update_service_spec.rb create mode 100644 spec/support/google_api_helpers.rb diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 4260fadb46d..7af56adb613 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -13,7 +13,7 @@ module Clusters has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes' accepts_nested_attributes_for :provider_gcp - accepts_nested_attributes_for :platform_kubernetes + accepts_nested_attributes_for :platform_kubernetes, update_only: true validates :name, cluster_name: true validate :restrict_modification, on: :update diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index b20b00ff51b..1a4e293be65 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -111,9 +111,6 @@ module Clusters kubeclient = build_kubeclient! kubeclient.get_secrets.as_json - rescue KubeException => err - raise err unless err.error_code == 404 - [] end # Returns a hash of all pods in the namespace diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 9f257bd0e9a..d379870924a 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -14,9 +14,11 @@ module Clusters provider.make_created! end rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") + provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") + rescue KubeException => e + provider.make_errored!("Failed to request to Kubernetes; #{e.message}") rescue ActiveRecord::RecordInvalid => e - cluster.make_errored!("Failed to configure GKE Cluster: #{e.message}") + provider.make_errored!("Failed to configure GKE Cluster: #{e.message}") end private @@ -41,6 +43,8 @@ module Clusters return Base64.decode64(token_base64) if token_base64 end end + + nil end def gke_cluster diff --git a/spec/factories/clusters/cluster.rb b/spec/factories/clusters/cluster.rb index 8ba1eda8cc9..ad116ecfa16 100644 --- a/spec/factories/clusters/cluster.rb +++ b/spec/factories/clusters/cluster.rb @@ -14,13 +14,19 @@ FactoryGirl.define do trait :provided_by_user do provider_type :user platform_type :kubernetes - platform_kubernetes + + platform_kubernetes do + create(:platform_kubernetes, :configured) + end end trait :provided_by_gcp do provider_type :gcp platform_type :kubernetes - platform_kubernetes + + platform_kubernetes do + create(:platform_kubernetes, :configured) + end provider_gcp do create(:provider_gcp, :created) @@ -34,6 +40,10 @@ FactoryGirl.define do provider_gcp do create(:provider_gcp, :creating) end + + after(:create) do |cluster, evaluator| + create(:platform_kubernetes, cluster: cluster) + end end end end diff --git a/spec/factories/clusters/platforms/gcp.rb b/spec/factories/clusters/platforms/gcp.rb deleted file mode 100644 index c135bbb20a4..00000000000 --- a/spec/factories/clusters/platforms/gcp.rb +++ /dev/null @@ -1,28 +0,0 @@ -FactoryGirl.define do - factory :provider_gcp, class: Clusters::Providers::Gcp do - cluster - gcp_project_id 'test-gcp-project' - - trait :creating do - access_token 'access_token_123' - - after(:build) do |gcp, evaluator| - gcp.make_creating('operation-123') - end - end - - trait :created do - endpoint '111.111.111.111' - - after(:build) do |gcp, evaluator| - gcp.make_created - end - end - - trait :errored do - after(:build) do |gcp, evaluator| - gcp.make_errored('Something wrong') - end - end - end -end diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb new file mode 100644 index 00000000000..69c2f30f859 --- /dev/null +++ b/spec/factories/clusters/platforms/kubernetes.rb @@ -0,0 +1,21 @@ +FactoryGirl.define do + factory :platform_kubernetes, class: Clusters::Platforms::Kubernetes do + cluster + namespace nil + + trait :ca_cert do + 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 + + trait :configured do + api_url 'https://kubernetes.example.com' + ca_cert nil + token 'a' * 40 + username 'xxxxxx' + password 'xxxxxx' + end + end +end diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb new file mode 100644 index 00000000000..13bf50d7b7f --- /dev/null +++ b/spec/factories/clusters/providers/gcp.rb @@ -0,0 +1,32 @@ +FactoryGirl.define do + factory :provider_gcp, class: Clusters::Providers::Gcp do + cluster + gcp_project_id 'test-gcp-project' + + trait :scheduled do + access_token 'access_token_123' + end + + trait :creating do + access_token 'access_token_123' + + after(:build) do |gcp, evaluator| + gcp.make_creating('operation-123') + end + end + + trait :created do + endpoint '111.111.111.111' + + after(:build) do |gcp, evaluator| + gcp.make_created + end + end + + trait :errored do + after(:build) do |gcp, evaluator| + gcp.make_errored('Something wrong') + end + end + end +end diff --git a/spec/factories/clusters/providers/kubernetes.rb b/spec/factories/clusters/providers/kubernetes.rb deleted file mode 100644 index b4d413d32c1..00000000000 --- a/spec/factories/clusters/providers/kubernetes.rb +++ /dev/null @@ -1,18 +0,0 @@ -FactoryGirl.define do - factory :platform_kubernetes, class: Clusters::Platforms::Kubernetes do - cluster - api_url 'https://kubernetes.example.com' - ca_cert nil - token 'a' * 40 - username 'xxxxxx' - password 'xxxxxx' - namespace nil - - trait :ca_cert do - after(:create) do |platform_kubernetes, evaluator| - pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) - platform_kubernetes.ca_cert = File.read(pem_file) - end - end - end -end diff --git a/spec/services/ci/create_cluster_service_spec.rb b/spec/services/ci/create_cluster_service_spec.rb deleted file mode 100644 index 6e7398fbffa..00000000000 --- a/spec/services/ci/create_cluster_service_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'spec_helper' - -describe Ci::CreateClusterService do - describe '#execute' do - let(:access_token) { 'xxx' } - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:result) { described_class.new(project, user, params).execute(access_token) } - - context 'when correct params' do - let(:params) do - { - gcp_project_id: 'gcp-project', - gcp_cluster_name: 'test-cluster', - gcp_cluster_zone: 'us-central1-a', - gcp_cluster_size: 1 - } - end - - it 'creates a cluster object' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { result }.to change { Gcp::Cluster.count }.by(1) - expect(result.gcp_project_id).to eq('gcp-project') - expect(result.gcp_cluster_name).to eq('test-cluster') - expect(result.gcp_cluster_zone).to eq('us-central1-a') - expect(result.gcp_cluster_size).to eq(1) - expect(result.gcp_token).to eq(access_token) - end - end - - context 'when invalid params' do - let(:params) do - { - gcp_project_id: 'gcp-project', - gcp_cluster_name: 'test-cluster', - gcp_cluster_zone: 'us-central1-a', - gcp_cluster_size: 'ABC' - } - end - - it 'returns an error' do - expect(ClusterProvisionWorker).not_to receive(:perform_async) - expect { result }.to change { Gcp::Cluster.count }.by(0) - end - end - end -end diff --git a/spec/services/ci/fetch_gcp_operation_service_spec.rb b/spec/services/ci/fetch_gcp_operation_service_spec.rb deleted file mode 100644 index 7792979c5cb..00000000000 --- a/spec/services/ci/fetch_gcp_operation_service_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'spec_helper' -require 'google/apis' - -describe Ci::FetchGcpOperationService do - describe '#execute' do - let(:cluster) { create(:gcp_cluster) } - let(:operation) { double } - - context 'when suceeded' do - before do - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:projects_zones_operations).and_return(operation) - end - - it 'fetch the gcp operaion' do - expect { |b| described_class.new.execute(cluster, &b) } - .to yield_with_args(operation) - end - end - - context 'when raises an error' do - let(:error) { Google::Apis::ServerError.new('a') } - - before do - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:projects_zones_operations).and_raise(error) - end - - it 'sets an error to cluster object' do - expect { |b| described_class.new.execute(cluster, &b) } - .not_to yield_with_args - expect(cluster.reload).to be_errored - end - end - end -end diff --git a/spec/services/ci/fetch_kubernetes_token_service_spec.rb b/spec/services/ci/fetch_kubernetes_token_service_spec.rb deleted file mode 100644 index 1d05c9671a9..00000000000 --- a/spec/services/ci/fetch_kubernetes_token_service_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'spec_helper' - -describe Ci::FetchKubernetesTokenService do - describe '#execute' do - subject { described_class.new(api_url, ca_pem, username, password).execute } - - let(:api_url) { 'http://111.111.111.111' } - let(:ca_pem) { '' } - let(:username) { 'admin' } - let(:password) { 'xxx' } - - context 'when params correct' do - let(:token) { 'xxx.token.xxx' } - - let(:secrets_json) do - [ - { - 'metadata': { - name: metadata_name - }, - 'data': { - 'token': Base64.encode64(token) - } - } - ] - end - - before do - allow_any_instance_of(Kubeclient::Client) - .to receive(:get_secrets).and_return(secrets_json) - end - - context 'when default-token exists' do - let(:metadata_name) { 'default-token-123' } - - it { is_expected.to eq(token) } - end - - context 'when default-token does not exist' do - let(:metadata_name) { 'another-token-123' } - - it { is_expected.to be_nil } - end - end - - context 'when api_url is nil' do - let(:api_url) { nil } - - it { expect { subject }.to raise_error("Incomplete settings") } - end - - context 'when username is nil' do - let(:username) { nil } - - it { expect { subject }.to raise_error("Incomplete settings") } - end - - context 'when password is nil' do - let(:password) { nil } - - it { expect { subject }.to raise_error("Incomplete settings") } - end - end -end diff --git a/spec/services/ci/finalize_cluster_creation_service_spec.rb b/spec/services/ci/finalize_cluster_creation_service_spec.rb deleted file mode 100644 index def3709fdb4..00000000000 --- a/spec/services/ci/finalize_cluster_creation_service_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'spec_helper' - -describe Ci::FinalizeClusterCreationService do - describe '#execute' do - let(:cluster) { create(:gcp_cluster) } - let(:result) { described_class.new.execute(cluster) } - - context 'when suceeded to get cluster from api' do - let(:gke_cluster) { double } - - before do - allow(gke_cluster).to receive(:endpoint).and_return('111.111.111.111') - allow(gke_cluster).to receive(:master_auth).and_return(spy) - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:projects_zones_clusters_get).and_return(gke_cluster) - end - - context 'when suceeded to get kubernetes token' do - let(:kubernetes_token) { 'abc' } - - before do - allow_any_instance_of(Ci::FetchKubernetesTokenService) - .to receive(:execute).and_return(kubernetes_token) - end - - it 'executes integration cluster' do - expect_any_instance_of(Ci::IntegrateClusterService).to receive(:execute) - described_class.new.execute(cluster) - end - end - - context 'when failed to get kubernetes token' do - before do - allow_any_instance_of(Ci::FetchKubernetesTokenService) - .to receive(:execute).and_return(nil) - end - - it 'sets an error to cluster object' do - described_class.new.execute(cluster) - - expect(cluster.reload).to be_errored - end - end - end - - context 'when failed to get cluster from api' do - let(:error) { Google::Apis::ServerError.new('a') } - - before do - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:projects_zones_clusters_get).and_raise(error) - end - - it 'sets an error to cluster object' do - described_class.new.execute(cluster) - - expect(cluster.reload).to be_errored - end - end - end -end diff --git a/spec/services/ci/integrate_cluster_service_spec.rb b/spec/services/ci/integrate_cluster_service_spec.rb deleted file mode 100644 index 3a79c205bd1..00000000000 --- a/spec/services/ci/integrate_cluster_service_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'spec_helper' - -describe Ci::IntegrateClusterService do - describe '#execute' do - let(:cluster) { create(:gcp_cluster, :custom_project_namespace) } - let(:endpoint) { '123.123.123.123' } - let(:ca_cert) { 'ca_cert_xxx' } - let(:token) { 'token_xxx' } - let(:username) { 'username_xxx' } - let(:password) { 'password_xxx' } - - before do - described_class - .new.execute(cluster, endpoint, ca_cert, token, username, password) - - cluster.reload - end - - context 'when correct params' do - it 'creates a cluster object' do - expect(cluster.endpoint).to eq(endpoint) - expect(cluster.ca_cert).to eq(ca_cert) - expect(cluster.kubernetes_token).to eq(token) - expect(cluster.username).to eq(username) - expect(cluster.password).to eq(password) - expect(cluster.service.active).to be_truthy - expect(cluster.service.api_url).to eq(cluster.api_url) - expect(cluster.service.ca_pem).to eq(ca_cert) - expect(cluster.service.namespace).to eq(cluster.project_namespace) - expect(cluster.service.token).to eq(token) - end - end - - context 'when invalid params' do - let(:endpoint) { nil } - - it 'sets an error to cluster object' do - expect(cluster).to be_errored - end - end - end -end diff --git a/spec/services/ci/provision_cluster_service_spec.rb b/spec/services/ci/provision_cluster_service_spec.rb deleted file mode 100644 index 5ce5c788314..00000000000 --- a/spec/services/ci/provision_cluster_service_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -require 'spec_helper' - -describe Ci::ProvisionClusterService do - describe '#execute' do - let(:cluster) { create(:gcp_cluster) } - let(:operation) { spy } - - shared_examples 'error' do - it 'sets an error to cluster object' do - described_class.new.execute(cluster) - - expect(cluster.reload).to be_errored - end - end - - context 'when suceeded to request provision' do - before do - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:projects_zones_clusters_create).and_return(operation) - end - - context 'when operation status is RUNNING' do - before do - allow(operation).to receive(:status).and_return('RUNNING') - end - - context 'when suceeded to parse gcp operation id' do - before do - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:parse_operation_id).and_return('operation-123') - end - - context 'when cluster status is scheduled' do - before do - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:parse_operation_id).and_return('operation-123') - end - - it 'schedules a worker for status minitoring' do - expect(WaitForClusterCreationWorker).to receive(:perform_in) - - described_class.new.execute(cluster) - end - end - - context 'when cluster status is creating' do - before do - cluster.make_creating! - end - - it_behaves_like 'error' - end - end - - context 'when failed to parse gcp operation id' do - before do - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:parse_operation_id).and_return(nil) - end - - it_behaves_like 'error' - end - end - - context 'when operation status is others' do - before do - allow(operation).to receive(:status).and_return('others') - end - - it_behaves_like 'error' - end - end - - context 'when failed to request provision' do - let(:error) { Google::Apis::ServerError.new('a') } - - before do - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:projects_zones_clusters_create).and_raise(error) - end - - it_behaves_like 'error' - end - end -end diff --git a/spec/services/ci/update_cluster_service_spec.rb b/spec/services/ci/update_cluster_service_spec.rb deleted file mode 100644 index a289385b88f..00000000000 --- a/spec/services/ci/update_cluster_service_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe Ci::UpdateClusterService do - describe '#execute' do - let(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service) } - - before do - described_class.new(cluster.project, cluster.user, params).execute(cluster) - - cluster.reload - end - - context 'when correct params' do - context 'when enabled is true' do - let(:params) { { 'enabled' => 'true' } } - - it 'enables cluster and overwrite kubernetes service' do - expect(cluster.enabled).to be_truthy - expect(cluster.service.active).to be_truthy - expect(cluster.service.api_url).to eq(cluster.api_url) - expect(cluster.service.ca_pem).to eq(cluster.ca_cert) - expect(cluster.service.namespace).to eq(cluster.project_namespace) - expect(cluster.service.token).to eq(cluster.kubernetes_token) - end - end - - context 'when enabled is false' do - let(:params) { { 'enabled' => 'false' } } - - it 'disables cluster and kubernetes service' do - expect(cluster.enabled).to be_falsy - expect(cluster.service.active).to be_falsy - end - end - end - end -end diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb new file mode 100644 index 00000000000..14f88d3f0f5 --- /dev/null +++ b/spec/services/clusters/create_service_spec.rb @@ -0,0 +1,62 @@ +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 correct params' do + let(:params) do + { + name: 'test-cluster', + platform_type: :kubernetes, + provider_type: :gcp, + platform_kubernetes_attributes: { + namespace: 'custom-namespace' + }, + 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) + 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.namespace).to eq('custom-namespace') + end + end + + context 'when invalid params' do + let(:params) do + { + name: 'test-cluster', + platform_type: :kubernetes, + provider_type: :user, + provider_gcp_attributes: { + gcp_project_id: 'gcp-project', + zone: 'us-central1-a', + num_nodes: 'ABC' + } + } + 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.num_nodes"]).to be_present + end + end +end diff --git a/spec/services/clusters/gcp/fetch_operation_service_spec.rb b/spec/services/clusters/gcp/fetch_operation_service_spec.rb new file mode 100644 index 00000000000..20d46608033 --- /dev/null +++ b/spec/services/clusters/gcp/fetch_operation_service_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Clusters::Gcp::FetchOperationService do + include GoogleApi::CloudPlatformHelpers + + describe '#execute' do + let(:provider) { create(:provider_gcp, :creating) } + let(:gcp_project_id) { provider.gcp_project_id } + let(:zone) { provider.zone } + let(:operation_id) { provider.operation_id } + + shared_examples 'success' do + it 'yields' do + expect { |b| described_class.new.execute(provider, &b) } + .to yield_with_args + end + end + + shared_examples 'error' do + it 'sets an error to provider object' do + expect { |b| described_class.new.execute(provider, &b) } + .not_to yield_with_args + expect(provider.reload).to be_errored + end + end + + context 'when suceeded to fetch operation' do + before do + stub_cloud_platform_get_zone_operation(gcp_project_id, zone, operation_id) + end + + it_behaves_like 'success' + end + + context 'when Internal Server Error happened' do + before do + stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id) + end + + it_behaves_like 'error' + end + end +end diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb new file mode 100644 index 00000000000..70ea1cbca46 --- /dev/null +++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb @@ -0,0 +1,110 @@ +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) + provider.reload + platform.reload + + expect(provider.endpoint).to eq(endpoint) + expect(platform.api_url).to eq(api_url) + expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) + expect(platform.username).to eq(username) + expect(platform.password).to eq(password) + expect(platform.token).to eq(token) + end + end + + context 'when default-token is not found' do + before do + stub_kubeclient_get_secrets(api_url, metadata_name: 'aaaa') + end + + it_behaves_like 'error' + end + + context 'when token is empty' do + before do + stub_kubeclient_get_secrets(api_url, token: '') + end + + it_behaves_like 'error' + end + + context 'when failed to fetch kuberenetes token' do + before do + stub_kubeclient_get_secrets_error(api_url) + end + + it_behaves_like 'error' + end + end + + context 'when failed to fetch gke cluster info' do + before do + stub_cloud_platform_get_zone_cluster_error(gcp_project_id, zone, cluster_name) + end + + it_behaves_like 'error' + end + end +end diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb new file mode 100644 index 00000000000..f5f9d4800fd --- /dev/null +++ b/spec/services/clusters/gcp/provision_service_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Clusters::Gcp::ProvisionService do + include GoogleApi::CloudPlatformHelpers + + describe '#execute' do + let(:provider) { create(:provider_gcp, :scheduled) } + let(:gcp_project_id) { provider.gcp_project_id } + let(:zone) { provider.zone } + + shared_examples 'success' do + it 'schedules a worker for status minitoring' do + expect(WaitForClusterCreationWorker).to receive(:perform_in) + + described_class.new.execute(provider) + + expect(provider.reload).to be_creating + end + end + + shared_examples 'error' do + it 'sets an error to provider object' do + described_class.new.execute(provider) + + expect(provider.reload).to be_errored + end + end + + context 'when suceeded to request provision' do + before do + stub_cloud_platform_create_cluster(gcp_project_id, zone) + end + + it_behaves_like 'success' + end + + context 'when operation status is unexpected' do + before do + stub_cloud_platform_create_cluster( + gcp_project_id, zone, + { + "status": 'unexpected' + } ) + end + + it_behaves_like 'error' + end + + context 'when selfLink is unexpected' do + before do + stub_cloud_platform_create_cluster( + gcp_project_id, zone, + { + "selfLink": 'unexpected' + }) + end + + it_behaves_like 'error' + end + + context 'when Internal Server Error happened' do + before do + stub_cloud_platform_create_cluster_error(gcp_project_id, zone) + end + + it_behaves_like 'error' + end + end +end diff --git a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb new file mode 100644 index 00000000000..666fcf13cac --- /dev/null +++ b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +describe Clusters::Gcp::VerifyProvisionStatusService do + include GoogleApi::CloudPlatformHelpers + + describe '#execute' do + let(:provider) { create(:provider_gcp, :creating) } + let(:gcp_project_id) { provider.gcp_project_id } + let(:zone) { provider.zone } + let(:operation_id) { provider.operation_id } + + shared_examples 'continue_creation' do + it 'schedules a worker for status minitoring' do + expect(WaitForClusterCreationWorker).to receive(:perform_in) + + described_class.new.execute(provider) + end + end + + shared_examples 'finalize_creation' do + it 'schedules a worker for status minitoring' do + expect_any_instance_of(Clusters::Gcp::FinalizeCreationService).to receive(:execute) + + described_class.new.execute(provider) + end + end + + shared_examples 'error' do + it 'sets an error to provider object' do + described_class.new.execute(provider) + + expect(provider.reload).to be_errored + end + end + + context 'when operation status is RUNNING' do + before do + stub_cloud_platform_get_zone_operation( + gcp_project_id, zone, operation_id, + { + "status": 'RUNNING', + "startTime": 1.minute.ago.strftime("%FT%TZ") + } ) + end + + it_behaves_like 'continue_creation' + + context 'when cluster creation time exceeds timeout' do + before do + stub_cloud_platform_get_zone_operation( + gcp_project_id, zone, operation_id, + { + "status": 'RUNNING', + "startTime": 30.minutes.ago.strftime("%FT%TZ") + } ) + end + + it_behaves_like 'error' + end + end + + context 'when operation status is PENDING' do + before do + stub_cloud_platform_get_zone_operation( + gcp_project_id, zone, operation_id, + { + "status": 'PENDING', + "startTime": 1.minute.ago.strftime("%FT%TZ") + } ) + end + + it_behaves_like 'continue_creation' + end + + context 'when operation status is DONE' do + before do + stub_cloud_platform_get_zone_operation( + gcp_project_id, zone, operation_id, + { + "status": 'DONE' + } ) + end + + it_behaves_like 'finalize_creation' + end + + context 'when operation status is unexpected' do + before do + stub_cloud_platform_get_zone_operation( + gcp_project_id, zone, operation_id, + { + "status": 'unexpected' + } ) + end + + it_behaves_like 'error' + end + + context 'when failed to get operation status' do + before do + stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id) + end + + it_behaves_like 'error' + end + end +end diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb new file mode 100644 index 00000000000..2d91a21035d --- /dev/null +++ b/spec/services/clusters/update_service_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Clusters::UpdateService do + describe '#execute' do + subject { described_class.new(cluster.project, cluster.user, params).execute(cluster) } + + let(:cluster) { create(:cluster, :project, :provided_by_user) } + + context 'when correct params' do + context 'when enabled is true' do + let(:params) { { enabled: true } } + + it 'enables cluster' do + is_expected.to eq(true) + expect(cluster.enabled).to be_truthy + end + end + + context 'when enabled is false' do + let(:params) { { enabled: false } } + + it 'disables cluster' do + is_expected.to eq(true) + expect(cluster.enabled).to be_falsy + end + end + + context 'when namespace is specified' do + let(:params) do + { + platform_kubernetes_attributes: { + namespace: 'custom-namespace' + } + } + end + + it 'updates namespace' do + is_expected.to eq(true) + expect(cluster.platform.namespace).to eq('custom-namespace') + end + end + end + + context 'when invalid params' do + let(:params) do + { + platform_kubernetes_attributes: { + namespace: '!!!' + } + } + end + + it 'returns false' do + is_expected.to eq(false) + expect(cluster.errors[:"platform_kubernetes.namespace"]).to be_present + end + end + end +end diff --git a/spec/support/google_api_helpers.rb b/spec/support/google_api_helpers.rb new file mode 100644 index 00000000000..f032b028741 --- /dev/null +++ b/spec/support/google_api_helpers.rb @@ -0,0 +1,140 @@ +module GoogleApi + module CloudPlatformHelpers + 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 + + def cloud_platform_cluster_body(**options) + { + "name": options[:name] || 'string', + "description": options[:description] || 'string', + "initialNodeCount": options[:initialNodeCount] || 'number', + # "nodeConfig": {, + # object(NodeConfig), + # },, + "masterAuth": { + "username": options[:username] || 'string', + "password": options[:password] || 'string', + # "clientCertificateConfig": { + # object(ClientCertificateConfig) + # }, + "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', + # "addonsConfig": {, + # object(AddonsConfig), + # },, + "subnetwork": options[:subnetwork] || 'string', + # "nodePools": [, + # {, + # object(NodePool), + # }, + # ],, + # "locations": [, + # string, + # ],, + "enableKubernetesAlpha": options[:enableKubernetesAlpha] || 'boolean', + # "resourceLabels": {, + # string: string,, + # ..., + # },, + "labelFingerprint": options[:labelFingerprint] || 'string', + # "legacyAbac": {, + # object(LegacyAbac), + # }, + # "networkPolicy": {, + # object(NetworkPolicy), + # }, + # "ipAllocationPolicy": {, + # object(IPAllocationPolicy), + # }, + # "masterAuthorizedNetworksConfig": {, + # object(MasterAuthorizedNetworksConfig), + # }, + "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', + # "instanceGroupUrls": [, + # string, + # ],, + "currentNodeCount": options[:currentNodeCount] || 'number', + "expireTime": options[:expireTime] || 'string' + } + end + + def cloud_platform_operation_body(**options) + { + "name": options[:name] || 'operation-1234567891234-1234567', + "zone": options[:zone] || 'us-central1-a', + "operationType": options[:operationType] || 'CREATE_CLUSTER', + "status": options[:status] || 'PENDING', + "detail": options[:detail] || 'detail', + "statusMessage": options[:statusMessage] || '', + "selfLink": options[:selfLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/operations/operation-1234567891234-1234567', + "targetLink": options[:targetLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/clusters/test-cluster', + "startTime": options[:startTime] || '2017-09-13T16:49:13.055601589Z', + "endTime": options[:endTime] || '' + } + end + end +end diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb index c92f78b324c..1916bec05b8 100644 --- a/spec/support/kubernetes_helpers.rb +++ b/spec/support/kubernetes_helpers.rb @@ -9,8 +9,8 @@ module KubernetesHelpers kube_response(kube_pods_body) end - def stub_kubeclient_discover - WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body)) + def stub_kubeclient_discover(api_url) + WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body)) end def stub_kubeclient_pods(response = nil) @@ -20,11 +20,40 @@ module KubernetesHelpers WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response) end + def stub_kubeclient_get_secrets(api_url, **options) + WebMock.stub_request(:get, api_url + '/api/v1/secrets') + .to_return(kube_response(kube_v1_secrets_body(options))) + end + + def stub_kubeclient_get_secrets_error(api_url) + WebMock.stub_request(:get, api_url + '/api/v1/secrets') + .to_return(status: [500, "Internal Server Error"]) + end + + def kube_v1_secrets_body(**options) + { + "kind" => "SecretList", + "apiVersion": "v1", + "items" => [ + { + "metadata": { + "name": options[:metadata_name] || "default-token-1", + "namespace": "kube-system" + }, + "data": { + "token": options[:token] || Base64.encode64('token-sample-123') + } + } + ] + } + end + def kube_v1_discovery_body { "kind" => "APIResourceList", "resources" => [ - { "name" => "pods", "namespaced" => true, "kind" => "Pod" } + { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, + { "name" => "secrets", "namespaced" => true, "kind" => "Secret" } ] } end -- cgit v1.2.1 From 4934f6078b12072fd62f8065a1b25d961aa2d825 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Tue, 31 Oct 2017 00:18:15 +0900 Subject: specs for workers. --- app/workers/cluster_provision_worker.rb | 4 +- app/workers/wait_for_cluster_creation_worker.rb | 4 +- spec/workers/cluster_provision_worker_spec.rb | 19 +++++-- .../wait_for_cluster_creation_worker_spec.rb | 61 +++++----------------- 4 files changed, 33 insertions(+), 55 deletions(-) diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb index 0929fffc444..b01f9708424 100644 --- a/app/workers/cluster_provision_worker.rb +++ b/app/workers/cluster_provision_worker.rb @@ -4,8 +4,8 @@ class ClusterProvisionWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - cluster.provider_gcp.try do |provider| - Clusters::Gcp::ProvisionService.new.execute(provider) + cluster.provider.try do |provider| + Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp? end end end diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb index b2f04869636..241ed3901dc 100644 --- a/app/workers/wait_for_cluster_creation_worker.rb +++ b/app/workers/wait_for_cluster_creation_worker.rb @@ -4,8 +4,8 @@ class WaitForClusterCreationWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - cluster.provider_gcp.try do |provider| - Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) + cluster.provider.try do |provider| + Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) if cluster.gcp? end end end diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb index 11f208289db..85c7dc20ede 100644 --- a/spec/workers/cluster_provision_worker_spec.rb +++ b/spec/workers/cluster_provision_worker_spec.rb @@ -2,11 +2,22 @@ require 'spec_helper' describe ClusterProvisionWorker do describe '#perform' do - context 'when cluster exists' do - let(:cluster) { create(:gcp_cluster) } + context 'when provider type is gcp' do + let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) } + let(:provider) { create(:provider_gcp, :scheduled) } it 'provision a cluster' do - expect_any_instance_of(Ci::ProvisionClusterService).to receive(:execute) + expect_any_instance_of(Clusters::Gcp::ProvisionService).to receive(:execute) + + described_class.new.perform(cluster.id) + end + end + + context 'when provider type is user' do + let(:cluster) { create(:cluster, provider_type: :user) } + + it 'does not provision a cluster' do + expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute) described_class.new.perform(cluster.id) end @@ -14,7 +25,7 @@ describe ClusterProvisionWorker do context 'when cluster does not exist' do it 'does not provision a cluster' do - expect_any_instance_of(Ci::ProvisionClusterService).not_to receive(:execute) + expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute) described_class.new.perform(123) end diff --git a/spec/workers/wait_for_cluster_creation_worker_spec.rb b/spec/workers/wait_for_cluster_creation_worker_spec.rb index dcd4a3b9aec..29812408396 100644 --- a/spec/workers/wait_for_cluster_creation_worker_spec.rb +++ b/spec/workers/wait_for_cluster_creation_worker_spec.rb @@ -2,65 +2,32 @@ require 'spec_helper' describe WaitForClusterCreationWorker do describe '#perform' do - context 'when cluster exists' do - let(:cluster) { create(:gcp_cluster) } - let(:operation) { double } + context 'when provider type is gcp' do + let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) } + let(:provider) { create(:provider_gcp, :creating) } - before do - allow(operation).to receive(:status).and_return(status) - allow(operation).to receive(:start_time).and_return(1.minute.ago) - allow(operation).to receive(:status_message).and_return('error') - allow_any_instance_of(Ci::FetchGcpOperationService).to receive(:execute).and_yield(operation) - end - - context 'when operation status is RUNNING' do - let(:status) { 'RUNNING' } - - it 'reschedules worker' do - expect(described_class).to receive(:perform_in) - - described_class.new.perform(cluster.id) - end - - context 'when operation timeout' do - before do - allow(operation).to receive(:start_time).and_return(30.minutes.ago.utc) - end - - it 'sets an error message on cluster' do - described_class.new.perform(cluster.id) + it 'provision a cluster' do + expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).to receive(:execute) - expect(cluster.reload).to be_errored - end - end - end - - context 'when operation status is DONE' do - let(:status) { 'DONE' } - - it 'finalizes cluster creation' do - expect_any_instance_of(Ci::FinalizeClusterCreationService).to receive(:execute) - - described_class.new.perform(cluster.id) - end + described_class.new.perform(cluster.id) end + end - context 'when operation status is others' do - let(:status) { 'others' } + context 'when provider type is user' do + let(:cluster) { create(:cluster, provider_type: :user) } - it 'sets an error message on cluster' do - described_class.new.perform(cluster.id) + it 'does not provision a cluster' do + expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute) - expect(cluster.reload).to be_errored - end + described_class.new.perform(cluster.id) end end context 'when cluster does not exist' do it 'does not provision a cluster' do - expect_any_instance_of(Ci::FetchGcpOperationService).not_to receive(:execute) + expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute) - described_class.new.perform(1234) + described_class.new.perform(123) end end end -- cgit v1.2.1 From 709b8b6186b06372bf9bfebe9d36a0fea9226a1b Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Tue, 31 Oct 2017 00:29:48 +0900 Subject: specs for policies. --- spec/policies/clusters/cluster_policy_spec.rb | 28 +++++++++++++++++++++++++++ spec/policies/gcp/cluster_policy_spec.rb | 28 --------------------------- 2 files changed, 28 insertions(+), 28 deletions(-) create mode 100644 spec/policies/clusters/cluster_policy_spec.rb delete mode 100644 spec/policies/gcp/cluster_policy_spec.rb diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb new file mode 100644 index 00000000000..4207f42b07f --- /dev/null +++ b/spec/policies/clusters/cluster_policy_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Clusters::ClusterPolicy, :models do + let(:cluster) { create(:cluster, :project) } + let(:project) { cluster.project } + let(:user) { create(:user) } + let(:policy) { described_class.new(user, cluster) } + + describe 'rules' do + context 'when developer' do + before do + project.add_developer(user) + end + + it { expect(policy).to be_disallowed :update_cluster } + it { expect(policy).to be_disallowed :admin_cluster } + end + + context 'when master' do + before do + project.add_master(user) + end + + it { expect(policy).to be_allowed :update_cluster } + it { expect(policy).to be_allowed :admin_cluster } + end + end +end diff --git a/spec/policies/gcp/cluster_policy_spec.rb b/spec/policies/gcp/cluster_policy_spec.rb deleted file mode 100644 index e213aa3d557..00000000000 --- a/spec/policies/gcp/cluster_policy_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe Gcp::ClusterPolicy, :models do - set(:project) { create(:project) } - set(:cluster) { create(:gcp_cluster, project: project) } - let(:user) { create(:user) } - let(:policy) { described_class.new(user, cluster) } - - describe 'rules' do - context 'when developer' do - before do - project.add_developer(user) - end - - it { expect(policy).to be_disallowed :update_cluster } - it { expect(policy).to be_disallowed :admin_cluster } - end - - context 'when master' do - before do - project.add_master(user) - end - - it { expect(policy).to be_allowed :update_cluster } - it { expect(policy).to be_allowed :admin_cluster } - end - end -end -- cgit v1.2.1 From c43a82cdfa8c5c1d045e2628330953227dcc9767 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Tue, 31 Oct 2017 01:03:58 +0900 Subject: specs for serializers. --- spec/serializers/cluster_entity_spec.rb | 38 ++++++++++++++++++++--------- spec/serializers/cluster_serializer_spec.rb | 19 +++++++++------ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb index 2c7f49974f1..7b132a1b84d 100644 --- a/spec/serializers/cluster_entity_spec.rb +++ b/spec/serializers/cluster_entity_spec.rb @@ -1,22 +1,38 @@ require 'spec_helper' describe ClusterEntity do - set(:cluster) { create(:gcp_cluster, :errored) } - let(:request) { double('request') } + describe '#as_json' do + subject { described_class.new(cluster).as_json } - let(:entity) do - described_class.new(cluster) - end + context 'when provider type is gcp' do + let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) } - describe '#as_json' do - subject { entity.as_json } + context 'when status is creating' do + let(:provider) { create(:provider_gcp, :creating) } - it 'contains status' do - expect(subject[:status]).to eq(:errored) + it 'has corresponded data' do + expect(subject[:status]).to eq(:creating) + expect(subject[:status_reason]).to be_nil + end + end + + context 'when status is errored' do + let(:provider) { create(:provider_gcp, :errored) } + + it 'has corresponded data' do + expect(subject[:status]).to eq(:errored) + expect(subject[:status_reason]).to eq(provider.status_reason) + end + end end - it 'contains status reason' do - expect(subject[:status_reason]).to eq('general error') + context 'when provider type is user' do + let(:cluster) { create(:cluster, provider_type: :user) } + + it 'has nil' do + expect(subject[:status]).to be_nil + expect(subject[:status_reason]).to be_nil + end end end end diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb index 1ac6784d28f..e5da92a451e 100644 --- a/spec/serializers/cluster_serializer_spec.rb +++ b/spec/serializers/cluster_serializer_spec.rb @@ -1,15 +1,20 @@ require 'spec_helper' describe ClusterSerializer do - let(:serializer) do - described_class.new - end - describe '#represent_status' do - subject { serializer.represent_status(resource) } + subject { described_class.new.represent_status(cluster) } + + context 'when provider type is gcp' do + let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) } + let(:provider) { create(:provider_gcp, :errored) } + + it 'serializes only status' do + expect(subject.keys).to contain_exactly(:status, :status_reason) + end + end - context 'when represents only status' do - let(:resource) { create(:gcp_cluster, :errored) } + context 'when provider type is user' do + let(:cluster) { create(:cluster, provider_type: :user) } it 'serializes only status' do expect(subject.keys).to contain_exactly(:status, :status_reason) -- cgit v1.2.1 From 944149ec1602e72215283a77ab0b72dabd7ce8b8 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Tue, 31 Oct 2017 01:09:39 +0900 Subject: spec for spec/presenters --- spec/presenters/clusters/cluster_presenter_spec.rb | 34 +++++++++++++++++++++ spec/presenters/gcp/cluster_presenter_spec.rb | 35 ---------------------- 2 files changed, 34 insertions(+), 35 deletions(-) create mode 100644 spec/presenters/clusters/cluster_presenter_spec.rb delete mode 100644 spec/presenters/gcp/cluster_presenter_spec.rb diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb new file mode 100644 index 00000000000..48d4f3671c5 --- /dev/null +++ b/spec/presenters/clusters/cluster_presenter_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Clusters::ClusterPresenter do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + subject(:presenter) do + described_class.new(cluster) + end + + it 'inherits from Gitlab::View::Presenter::Delegated' do + expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated) + end + + describe '#initialize' do + it 'takes a cluster and optional params' do + expect { presenter }.not_to raise_error + end + + it 'exposes cluster' do + expect(presenter.cluster).to eq(cluster) + end + + it 'forwards missing methods to cluster' do + expect(presenter.status).to eq(cluster.status) + end + end + + describe '#gke_cluster_url' do + subject { described_class.new(cluster).gke_cluster_url } + + it { is_expected.to include(cluster.provider.zone) } + it { is_expected.to include(cluster.name) } + end +end diff --git a/spec/presenters/gcp/cluster_presenter_spec.rb b/spec/presenters/gcp/cluster_presenter_spec.rb deleted file mode 100644 index 8d86dc31582..00000000000 --- a/spec/presenters/gcp/cluster_presenter_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' - -describe Gcp::ClusterPresenter do - let(:project) { create(:project) } - let(:cluster) { create(:gcp_cluster, project: project) } - - subject(:presenter) do - described_class.new(cluster) - end - - it 'inherits from Gitlab::View::Presenter::Delegated' do - expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated) - end - - describe '#initialize' do - it 'takes a cluster and optional params' do - expect { presenter }.not_to raise_error - end - - it 'exposes cluster' do - expect(presenter.cluster).to eq(cluster) - end - - it 'forwards missing methods to cluster' do - expect(presenter.gcp_cluster_zone).to eq(cluster.gcp_cluster_zone) - end - end - - describe '#gke_cluster_url' do - subject { described_class.new(cluster).gke_cluster_url } - - it { is_expected.to include(cluster.gcp_cluster_zone) } - it { is_expected.to include(cluster.gcp_cluster_name) } - end -end -- cgit v1.2.1 From 6a65e2f5f94781a69f3f7fb329483ead6bc81fd9 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Tue, 31 Oct 2017 17:47:48 +0900 Subject: specs for controller. Improved validation --- app/controllers/projects/clusters_controller.rb | 15 +- app/models/clusters/cluster.rb | 6 +- app/models/clusters/platforms/kubernetes.rb | 4 +- app/services/clusters/create_service.rb | 8 +- .../projects/clusters_controller_spec.rb | 543 ++++++++++++++------- spec/factories/clusters/cluster.rb | 4 +- spec/factories/clusters/platforms/kubernetes.rb | 13 +- spec/models/clusters/cluster_spec.rb | 2 + spec/models/clusters/platforms/kubernetes_spec.rb | 90 ++-- spec/support/google_api/cloud_platform_helpers.rb | 150 ++++++ spec/support/google_api_helpers.rb | 140 ------ spec/support/kubernetes_helpers.rb | 2 +- 12 files changed, 586 insertions(+), 391 deletions(-) create mode 100644 spec/support/google_api/cloud_platform_helpers.rb delete mode 100644 spec/support/google_api_helpers.rb diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index c07d955f148..b204bd17eec 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -27,13 +27,14 @@ class Projects::ClustersController < Projects::ApplicationController end def new - @cluster = Clusters::Cluster.new( - platform_type: :kubernetes, - provider_type: :gcp).tap do |cluster| - cluster.build_provider_gcp - cluster.build_platform_kubernetes - cluster.projects << project - end + # @cluster = Clusters::Cluster.new( + # platform_type: :kubernetes, + # provider_type: :gcp).tap do |cluster| + # cluster.build_provider_gcp + # cluster.build_platform_kubernetes + # cluster.projects << project + # end + @cluster = Clusters::Cluster.new end def create diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 7af56adb613..091c91e3fb9 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -10,11 +10,13 @@ module Clusters has_many :projects, through: :cluster_projects, class_name: '::Project' has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp' - has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes' + has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', validate: { if: :update } - accepts_nested_attributes_for :provider_gcp + accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true + validates :provider_type, presence: true + validates :platform_type, presence: true validates :name, cluster_name: true validate :restrict_modification, on: :update diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 1a4e293be65..52022509d49 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -30,8 +30,8 @@ module Clusters message: Gitlab::Regex.kubernetes_namespace_regex_message } - validates :api_url, url: true, presence: true, on: :update - validates :token, presence: true, on: :update + validates :api_url, url: true, presence: true + validates :token, presence: true after_save :clear_reactive_cache! diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index 3f458e25c14..94b889895ba 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -28,10 +28,10 @@ module Clusters def cluster_params return @cluster_params if defined?(@cluster_params) - params[:provider_gcp_attributes][:machine_type] ||= - GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE - - params[:provider_gcp_attributes][:access_token] ||= access_token + params[:provider_gcp_attributes].try do |h| + h[:machine_type] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE + h[:access_token] ||= access_token + end @cluster_params = params.merge(user: current_user) end diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 7985028d73b..fd1a68ba7e1 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -1,68 +1,108 @@ require 'spec_helper' describe Projects::ClustersController do - set(:user) { create(:user) } - set(:project) { create(:project) } - let(:role) { :master } + include AccessMatchersForController + include GoogleApi::CloudPlatformHelpers - before do - project.team << [user, role] + describe 'GET index' do + describe 'functionality' do + let(:user) { create(:user) } - sign_in(user) - end + before do + project.add_master(user) + sign_in(user) + end - describe 'GET index' do - subject do - get :index, namespace_id: project.namespace, - project_id: project - end + context 'when project has a cluster' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } - context 'when cluster is already created' do - let!(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) } + it { expect(go).to redirect_to(project_cluster_path(project, project.cluster)) } + end - it 'redirects to show a cluster' do - subject + context 'when project does not have a cluster' do + let(:project) { create(:project) } - expect(response).to redirect_to(project_cluster_path(project, cluster)) + it { expect(go).to redirect_to(new_project_cluster_path(project)) } end end - context 'when we do not have cluster' do - it 'redirects to create a cluster' do - subject + describe 'security' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end - expect(response).to redirect_to(new_project_cluster_path(project)) - end + def go + get :index, namespace_id: project.namespace.to_param, project_id: project end end describe 'GET login' do - render_views + let(:project) { create(:project) } - subject do - get :login, namespace_id: project.namespace, - project_id: project - end + describe 'functionality' do + let(:user) { create(:user) } - context 'when we do have omniauth configured' do - it 'shows login button' do - subject - - expect(response.body).to include('auth_buttons/signin_with_google') + before do + project.add_master(user) + sign_in(user) end - end - context 'when we do not have omniauth configured' do - before do - stub_omniauth_setting(providers: []) + context 'when omniauth has been configured' do + let(:key) { 'secere-key' } + + let(:session_key_for_redirect_uri) do + GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key) + end + + before do + allow(SecureRandom).to receive(:hex).and_return(key) + end + + it 'has authorize_url' do + go + + expect(assigns(:authorize_url)).to include(key) + expect(session[session_key_for_redirect_uri]).to eq(namespace_project_clusters_url(project.namespace, project)) + end end - it 'shows notice message' do - subject + context 'when omniauth has not configured' do + before do + stub_omniauth_setting(providers: []) + end - expect(response.body).to include('Ask your GitLab administrator if you want to use this service.') + it 'does not have authorize_url' do + go + + expect(assigns(:authorize_url)).to be_nil + end end end + + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + + def go + get :login, namespace_id: project.namespace, project_id: project + end end shared_examples 'requires to login' do @@ -74,235 +114,406 @@ describe Projects::ClustersController do end describe 'GET new' do - render_views + let(:project) { create(:project) } - subject do - get :new, namespace_id: project.namespace, - project_id: project - end + describe 'functionality' do + let(:user) { create(:user) } - context 'when logged' do before do - make_logged_in + project.add_master(user) + sign_in(user) + end + + context 'when access token is valid' do + before do + stub_google_api_validate_token + end + + it 'has new object' do + go + + expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster) + end end - it 'shows a creation form' do - subject + context 'when access token is expired' do + before do + stub_google_api_expired_token + end + + it { expect(go).to redirect_to(login_project_clusters_path(project)) } + end - expect(response.body).to include('Create cluster') + context 'when access token is not stored in session' do + it { expect(go).to redirect_to(login_project_clusters_path(project)) } end end - context 'when not logged' do - it_behaves_like 'requires to login' + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + + def go + get :new, namespace_id: project.namespace, project_id: project end end describe 'POST create' do - subject do - post :create, params.merge(namespace_id: project.namespace, - project_id: project) + let(:project) { create(:project) } + + let(:params) do + { + cluster: { + name: 'new-cluster', + platform_type: :kubernetes, + provider_type: :gcp, + provider_gcp_attributes: { + gcp_project_id: '111', + } + } + } end - context 'when not logged' do - let(:params) { {} } - - it_behaves_like 'requires to login' - end + describe 'functionality' do + let(:user) { create(:user) } - context 'when logged in' do before do - make_logged_in + project.add_master(user) + sign_in(user) end - context 'when all required parameters are set' do - let(:params) do - { - cluster: { - gcp_cluster_name: 'new-cluster', - gcp_project_id: '111' - } - } + context 'when access token is valid' do + before do + stub_google_api_validate_token end - before do - expect(ClusterProvisionWorker).to receive(:perform_async) { } + context 'when creates a cluster on gke' do + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } + expect(response).to redirect_to(project_cluster_path(project, project.cluster)) + end end - it 'creates a new cluster' do - expect { subject }.to change { Gcp::Cluster.count } + context 'when adds a cluster manually' do + let(:params) do + { + cluster: { + name: 'new-cluster', + platform_type: :kubernetes, + provider_type: :user + } + } + end - expect(response).to redirect_to(project_cluster_path(project, project.cluster)) + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } + expect(response).to redirect_to(project_cluster_path(project, project.cluster)) + end end - end - - context 'when not all required parameters are set' do - render_views - let(:params) do - { - cluster: { - project_namespace: 'some namespace' + context 'when not all required parameters are set' do + let(:params) do + { + cluster: { + name: 'new-cluster' + } } - } + end + + it 'shows an error message' do + expect { go }.not_to change { Clusters::Cluster.count } + expect(assigns(:cluster).errors).not_to be_empty + expect(response).to render_template(:new) + end + end + end + + context 'when access token is expired' do + before do + stub_google_api_expired_token end - it 'shows an error message' do - expect { subject }.not_to change { Gcp::Cluster.count } + it 'redirects to login page' do + expect(go).to redirect_to(login_project_clusters_path(project)) + end + end - expect(response).to render_template(:new) + context 'when access token is not stored in session' do + it 'redirects to login page' do + expect(go).to redirect_to(login_project_clusters_path(project)) end end end + + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + + def go + post :create, params.merge(namespace_id: project.namespace, project_id: project) + end end describe 'GET status' do - let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) } + let(:cluster) { create(:cluster, :project, :providing_by_gcp) } + let(:project) { cluster.project } + + describe 'functionality' do + let(:user) { create(:user) } - subject do + before do + project.add_master(user) + sign_in(user) + end + + it "responds with matching schema" do + go + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('cluster_status') + end + end + + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + + def go get :status, namespace_id: project.namespace, project_id: project, id: cluster, format: :json end - - it "responds with matching schema" do - subject - - expect(response).to have_http_status(:ok) - expect(response).to match_response_schema('cluster_status') - end end describe 'GET show' do - render_views - - let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) } + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } - subject do - get :show, namespace_id: project.namespace, - project_id: project, - id: cluster - end + describe 'functionality' do + let(:user) { create(:user) } - context 'when logged as master' do - it "allows to update cluster" do - subject - - expect(response).to have_http_status(:ok) - expect(response.body).to include("Save") + before do + project.add_master(user) + sign_in(user) end - it "allows remove integration" do - subject + it "renders view" do + go expect(response).to have_http_status(:ok) - expect(response.body).to include("Remove integration") + expect(assigns(:cluster)).to eq(cluster) end end - context 'when logged as developer' do - let(:role) { :developer } - - it "does not allow to access page" do - subject + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end - expect(response).to have_http_status(:not_found) - end + def go + get :show, namespace_id: project.namespace, + project_id: project, + id: cluster end end describe 'PUT update' do - render_views + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } - let(:service) { project.build_kubernetes_service } - let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project, service: service) } - let(:params) { {} } + describe 'functionality' do + let(:user) { create(:user) } - subject do - put :update, params.merge(namespace_id: project.namespace, - project_id: project, - id: cluster) - end + before do + project.add_master(user) + sign_in(user) + end - context 'when logged as master' do - context 'when valid params are used' do + context 'when update enabled' do let(:params) do { cluster: { enabled: false } } end - it "redirects back to show page" do - subject + it "updates and redirects back to show page" do + go + cluster.reload expect(response).to redirect_to(project_cluster_path(project, project.cluster)) expect(flash[:notice]).to eq('Cluster was successfully updated.') + expect(cluster.enabled).to be_falsey + end + + context 'when cluster is being created' do + let(:cluster) { create(:cluster, :project, :providing_by_gcp) } + + it "rejects changes" do + go + + expect(response).to have_http_status(:ok) + expect(response).to render_template(:show) + expect(cluster.enabled).to be_truthy + end end end - context 'when invalid params are used' do + context 'when update namespace' do + let(:namespace) { 'namespace-123' } + let(:params) do { - cluster: { project_namespace: 'my Namespace 321321321 #' } + cluster: { + platform_kubernetes_attributes: { + namespace: namespace + } + } } end - it "rejects changes" do - subject + it "updates and redirects back to show page" do + go + + cluster.reload + expect(response).to redirect_to(project_cluster_path(project, project.cluster)) + expect(flash[:notice]).to eq('Cluster was successfully updated.') + expect(cluster.platform.namespace).to eq(namespace) + end + + context 'when namespace is invalid' do + let(:namespace) { 'my Namespace 321321321 #' } + + it "rejects changes" do + go - expect(response).to have_http_status(:ok) - expect(response).to render_template(:show) + expect(response).to have_http_status(:ok) + expect(response).to render_template(:show) + expect(cluster.platform.namespace).not_to eq(namespace) + end end end end - context 'when logged as developer' do - let(:role) { :developer } + describe 'security' do + let(:params) do + { + cluster: { enabled: false } + } + end - it "does not allow to update cluster" do - subject + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end - expect(response).to have_http_status(:not_found) - end + def go + put :update, params.merge(namespace_id: project.namespace, + project_id: project, + id: cluster) end end describe 'delete update' do - let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) } + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } - subject do - delete :destroy, namespace_id: project.namespace, - project_id: project, - id: cluster - end + describe 'functionality' do + let(:user) { create(:user) } - context 'when logged as master' do - it "redirects back to clusters list" do - subject + before do + project.add_master(user) + sign_in(user) + end + + it "destroys and redirects back to clusters list" do + expect { go } + .to change { Clusters::Cluster.count }.by(-1) + .and change { Clusters::Platforms::Kubernetes.count }.by(-1) + .and change { Clusters::Providers::Gcp.count }.by(-1) expect(response).to redirect_to(project_clusters_path(project)) expect(flash[:notice]).to eq('Cluster integration was successfully removed.') end - end - context 'when logged as developer' do - let(:role) { :developer } + context 'when cluster is being created' do + let(:cluster) { create(:cluster, :project, :providing_by_gcp) } + + it "destroys and redirects back to clusters list" do + expect { go } + .to change { Clusters::Cluster.count }.by(-1) + .and change { Clusters::Platforms::Kubernetes.count }.by(-1) + .and change { Clusters::Providers::Gcp.count }.by(-1) + + expect(response).to redirect_to(project_clusters_path(project)) + expect(flash[:notice]).to eq('Cluster integration was successfully removed.') + end + end + + context 'when provider is user' do + let(:cluster) { create(:cluster, :project, :provided_by_user) } - it "does not allow to destroy cluster" do - subject + it "destroys and redirects back to clusters list" do + expect { go } + .to change { Clusters::Cluster.count }.by(-1) + .and change { Clusters::Platforms::Kubernetes.count }.by(-1) + .and change { Clusters::Providers::Gcp.count }.by(0) - expect(response).to have_http_status(:not_found) + expect(response).to redirect_to(project_clusters_path(project)) + expect(flash[:notice]).to eq('Cluster integration was successfully removed.') + end end end - end - def make_logged_in - session[GoogleApi::CloudPlatform::Client.session_key_for_token] = '1234' - session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = in_hour.to_i.to_s - end + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end - def in_hour - Time.now + 1.hour + def go + delete :destroy, namespace_id: project.namespace, + project_id: project, + id: cluster + end end end diff --git a/spec/factories/clusters/cluster.rb b/spec/factories/clusters/cluster.rb index ad116ecfa16..ef09cfbf5e3 100644 --- a/spec/factories/clusters/cluster.rb +++ b/spec/factories/clusters/cluster.rb @@ -42,7 +42,9 @@ FactoryGirl.define do end after(:create) do |cluster, evaluator| - create(:platform_kubernetes, cluster: cluster) + build(:platform_kubernetes, cluster: cluster).tap do |platform| + platform.save!(validate: false) + end end end end diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb index 69c2f30f859..86adaddfcb9 100644 --- a/spec/factories/clusters/platforms/kubernetes.rb +++ b/spec/factories/clusters/platforms/kubernetes.rb @@ -3,19 +3,16 @@ FactoryGirl.define do cluster namespace nil - trait :ca_cert do - 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 - trait :configured do api_url 'https://kubernetes.example.com' - ca_cert nil token 'a' * 40 username 'xxxxxx' password 'xxxxxx' + + after(:create) do |platform_kubernetes, evaluator| + pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) + platform_kubernetes.ca_cert = File.read(pem_file) + end end end end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index e53ce8497f5..997ed865545 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -10,6 +10,8 @@ describe Clusters::Cluster do it { is_expected.to delegate_method(:status_name).to(:provider) } it { is_expected.to delegate_method(:on_creation?).to(:provider) } it { is_expected.to respond_to :project } + it { is_expected.to validate_presence_of(:provider_type) } + it { is_expected.to validate_presence_of(:platform_type) } describe '.enabled' do subject { described_class.enabled } diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index ec6ecee6ff2..d11ce690601 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -11,7 +11,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching describe 'before_validation' do context 'when namespace includes upper case' do - let(:kubernetes) { create(:platform_kubernetes, namespace: namespace) } + let(:kubernetes) { create(:platform_kubernetes, :configured, namespace: namespace) } let(:namespace) { 'ABC' } it 'converts to lower case' do @@ -24,7 +24,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching subject { kubernetes.valid? } context 'when validates namespace' do - let(:kubernetes) { build(:platform_kubernetes, namespace: namespace) } + let(:kubernetes) { build(:platform_kubernetes, :configured, namespace: namespace) } context 'when namespace is blank' do let(:namespace) { '' } @@ -52,74 +52,42 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end context 'when validates api_url' do - context 'when updates a record' do - let(:kubernetes) { create(:platform_kubernetes) } + let(:kubernetes) { build(: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 + before do + kubernetes.api_url = api_url + end - context 'when api_url is valid url' do - let(:api_url) { 'https://111.111.111.111' } + context 'when api_url is invalid url' do + let(:api_url) { '!!!!!!' } - it { expect(kubernetes.save).to be_truthy } - end + it { expect(kubernetes.save).to be_falsey } end - context 'when creates a record' do - let(:kubernetes) { build(:platform_kubernetes) } + context 'when api_url is nil' do + let(:api_url) { nil } - before do - kubernetes.api_url = api_url - end + it { expect(kubernetes.save).to be_falsey } + end - context 'when api_url is nil' do - let(:api_url) { nil } + context 'when api_url is valid url' do + let(:api_url) { 'https://111.111.111.111' } - it { expect(kubernetes.save).to be_truthy } - end + it { expect(kubernetes.save).to be_truthy } end end context 'when validates token' do - context 'when updates a record' do - let(:kubernetes) { create(:platform_kubernetes) } + let(:kubernetes) { build(: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 + before do + kubernetes.token = token end - context 'when creates a record' do - let(:kubernetes) { build(:platform_kubernetes) } + context 'when token is nil' do + let(:token) { nil } - before do - kubernetes.token = token - end - - context 'when token is nil' do - let(:token) { nil } - - it { expect(kubernetes.save).to be_truthy } - end + it { expect(kubernetes.save).to be_falsey } end end end @@ -128,7 +96,8 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching subject { kubernetes.actual_namespace } let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } - let(:kubernetes) { create(:platform_kubernetes, namespace: namespace) } + let(:project) { cluster.project } + let(:kubernetes) { create(:platform_kubernetes, :configured, namespace: namespace) } context 'when namespace is present' do let(:namespace) { 'namespace-123' } @@ -139,7 +108,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching context 'when namespace is not present' do let(:namespace) { nil } - it { is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}") } + it { is_expected.to eq("#{project.path}-#{project.id}") } end end @@ -154,12 +123,13 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching describe '#default_namespace' do subject { kubernetes.default_namespace } - let(:kubernetes) { create(:platform_kubernetes) } + let(:kubernetes) { create(: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("#{cluster.project.path}-#{cluster.project.id}") } + it { is_expected.to eq("#{project.path}-#{project.id}") } end context 'when cluster belongs to nothing' do @@ -229,7 +199,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching let!(:cluster) { create(:cluster, :project, platform_kubernetes: service) } let(:project) { cluster.project } - let(:service) { create(:platform_kubernetes) } + let(:service) { create(:platform_kubernetes, :configured) } let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } context 'with invalid pods' do @@ -268,7 +238,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching subject { service.calculate_reactive_cache } let!(:cluster) { create(:cluster, :project, enabled: enabled, platform_kubernetes: service) } - let(:service) { create(:platform_kubernetes, :ca_cert) } + let(:service) { create(:platform_kubernetes, :configured) } let(:enabled) { true } context 'when cluster is disabled' do diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb new file mode 100644 index 00000000000..ef73add3991 --- /dev/null +++ b/spec/support/google_api/cloud_platform_helpers.rb @@ -0,0 +1,150 @@ +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 + + def cloud_platform_cluster_body(**options) + { + "name": options[:name] || 'string', + "description": options[:description] || 'string', + "initialNodeCount": options[:initialNodeCount] || 'number', + # "nodeConfig": {, + # object(NodeConfig), + # },, + "masterAuth": { + "username": options[:username] || 'string', + "password": options[:password] || 'string', + # "clientCertificateConfig": { + # object(ClientCertificateConfig) + # }, + "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', + # "addonsConfig": {, + # object(AddonsConfig), + # },, + "subnetwork": options[:subnetwork] || 'string', + # "nodePools": [, + # {, + # object(NodePool), + # }, + # ],, + # "locations": [, + # string, + # ],, + "enableKubernetesAlpha": options[:enableKubernetesAlpha] || 'boolean', + # "resourceLabels": {, + # string: string,, + # ..., + # },, + "labelFingerprint": options[:labelFingerprint] || 'string', + # "legacyAbac": {, + # object(LegacyAbac), + # }, + # "networkPolicy": {, + # object(NetworkPolicy), + # }, + # "ipAllocationPolicy": {, + # object(IPAllocationPolicy), + # }, + # "masterAuthorizedNetworksConfig": {, + # object(MasterAuthorizedNetworksConfig), + # }, + "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', + # "instanceGroupUrls": [, + # string, + # ],, + "currentNodeCount": options[:currentNodeCount] || 'number', + "expireTime": options[:expireTime] || 'string' + } + end + + def cloud_platform_operation_body(**options) + { + "name": options[:name] || 'operation-1234567891234-1234567', + "zone": options[:zone] || 'us-central1-a', + "operationType": options[:operationType] || 'CREATE_CLUSTER', + "status": options[:status] || 'PENDING', + "detail": options[:detail] || 'detail', + "statusMessage": options[:statusMessage] || '', + "selfLink": options[:selfLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/operations/operation-1234567891234-1234567', + "targetLink": options[:targetLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/clusters/test-cluster', + "startTime": options[:startTime] || '2017-09-13T16:49:13.055601589Z', + "endTime": options[:endTime] || '' + } + end + end +end diff --git a/spec/support/google_api_helpers.rb b/spec/support/google_api_helpers.rb deleted file mode 100644 index f032b028741..00000000000 --- a/spec/support/google_api_helpers.rb +++ /dev/null @@ -1,140 +0,0 @@ -module GoogleApi - module CloudPlatformHelpers - 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 - - def cloud_platform_cluster_body(**options) - { - "name": options[:name] || 'string', - "description": options[:description] || 'string', - "initialNodeCount": options[:initialNodeCount] || 'number', - # "nodeConfig": {, - # object(NodeConfig), - # },, - "masterAuth": { - "username": options[:username] || 'string', - "password": options[:password] || 'string', - # "clientCertificateConfig": { - # object(ClientCertificateConfig) - # }, - "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', - # "addonsConfig": {, - # object(AddonsConfig), - # },, - "subnetwork": options[:subnetwork] || 'string', - # "nodePools": [, - # {, - # object(NodePool), - # }, - # ],, - # "locations": [, - # string, - # ],, - "enableKubernetesAlpha": options[:enableKubernetesAlpha] || 'boolean', - # "resourceLabels": {, - # string: string,, - # ..., - # },, - "labelFingerprint": options[:labelFingerprint] || 'string', - # "legacyAbac": {, - # object(LegacyAbac), - # }, - # "networkPolicy": {, - # object(NetworkPolicy), - # }, - # "ipAllocationPolicy": {, - # object(IPAllocationPolicy), - # }, - # "masterAuthorizedNetworksConfig": {, - # object(MasterAuthorizedNetworksConfig), - # }, - "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', - # "instanceGroupUrls": [, - # string, - # ],, - "currentNodeCount": options[:currentNodeCount] || 'number', - "expireTime": options[:expireTime] || 'string' - } - end - - def cloud_platform_operation_body(**options) - { - "name": options[:name] || 'operation-1234567891234-1234567', - "zone": options[:zone] || 'us-central1-a', - "operationType": options[:operationType] || 'CREATE_CLUSTER', - "status": options[:status] || 'PENDING', - "detail": options[:detail] || 'detail', - "statusMessage": options[:statusMessage] || '', - "selfLink": options[:selfLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/operations/operation-1234567891234-1234567', - "targetLink": options[:targetLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/clusters/test-cluster', - "startTime": options[:startTime] || '2017-09-13T16:49:13.055601589Z', - "endTime": options[:endTime] || '' - } - end - end -end diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb index 1916bec05b8..3ae325637f6 100644 --- a/spec/support/kubernetes_helpers.rb +++ b/spec/support/kubernetes_helpers.rb @@ -14,7 +14,7 @@ module KubernetesHelpers end def stub_kubeclient_pods(response = nil) - stub_kubeclient_discover + stub_kubeclient_discover(service.api_url) pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods" WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response) -- cgit v1.2.1 From 253bf69dda460869741bc6c9d864c789055b8013 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Wed, 1 Nov 2017 03:59:40 +0900 Subject: specs for feature --- app/controllers/projects/clusters_controller.rb | 12 +- app/models/clusters/cluster.rb | 2 +- app/models/clusters/platforms/kubernetes.rb | 6 +- app/services/clusters/create_service.rb | 3 +- lib/google_api/cloud_platform/client.rb | 1 - .../projects/clusters_controller_spec.rb | 7 +- spec/features/projects/clusters_spec.rb | 19 ++- spec/models/clusters/platforms/kubernetes_spec.rb | 56 ++++---- .../project_services/kubernetes_service_spec.rb | 4 +- spec/services/clusters/create_service_spec.rb | 149 +++++++++++++++------ 10 files changed, 165 insertions(+), 94 deletions(-) diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index b204bd17eec..0f35b4f9c21 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -27,14 +27,10 @@ class Projects::ClustersController < Projects::ApplicationController end def new - # @cluster = Clusters::Cluster.new( - # platform_type: :kubernetes, - # provider_type: :gcp).tap do |cluster| - # cluster.build_provider_gcp - # cluster.build_platform_kubernetes - # cluster.projects << project - # end - @cluster = Clusters::Cluster.new + @cluster = Clusters::Cluster.new.tap do |cluster| + cluster.build_provider_gcp + cluster.build_platform_kubernetes + end end def create diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 091c91e3fb9..177403dcf00 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -10,7 +10,7 @@ module Clusters has_many :projects, through: :cluster_projects, class_name: '::Project' has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp' - has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', validate: { if: :update } + has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes' accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 52022509d49..e30ab805f1e 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -30,8 +30,10 @@ module Clusters message: Gitlab::Regex.kubernetes_namespace_regex_message } - validates :api_url, url: true, presence: true - validates :token, presence: true + # TODO: when cluster.gcp? skip validation when create a record + # TODO: when cluster.user? validates always + # validates :api_url, url: true, presence: true + # validates :token, presence: true after_save :clear_reactive_cache! diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index 94b889895ba..8c30e247fdc 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -29,8 +29,7 @@ module Clusters return @cluster_params if defined?(@cluster_params) params[:provider_gcp_attributes].try do |h| - h[:machine_type] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE - h[:access_token] ||= access_token + h[:access_token] = access_token end @cluster_params = params.merge(user: current_user) diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb index a440a3e3562..9242cbe840c 100644 --- a/lib/google_api/cloud_platform/client.rb +++ b/lib/google_api/cloud_platform/client.rb @@ -3,7 +3,6 @@ require 'google/apis/container_v1' module GoogleApi module CloudPlatform class Client < GoogleApi::Auth - DEFAULT_MACHINE_TYPE = 'n1-standard-1'.freeze SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze LEAST_TOKEN_LIFE_TIME = 10.minutes diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index fd1a68ba7e1..f29e119ee35 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -208,7 +208,12 @@ describe Projects::ClustersController do cluster: { name: 'new-cluster', platform_type: :kubernetes, - provider_type: :user + provider_type: :user, + platform_kubernetes_attributes: { + namespace: 'custom-namespace', + api_url: 'https://111.111.111.111', + token: 'token' + } } } end diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index 810f2c39b43..27c1f5062f5 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Clusters', :js do + include GoogleApi::CloudPlatformHelpers + let!(:project) { create(:project, :repository) } let!(:user) { create(:user) } @@ -11,8 +13,10 @@ feature 'Clusters', :js do context 'when user has signed in Google' do before do - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:validate_token).and_return(true) + allow_any_instance_of(Projects::ClustersController) + .to receive(:token_in_session).and_return('token') + allow_any_instance_of(Projects::ClustersController) + .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) end context 'when user does not have a cluster and visits cluster index page' do @@ -36,15 +40,15 @@ feature 'Clusters', :js do allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) - fill_in 'cluster_gcp_project_id', with: 'gcp-project-123' - fill_in 'cluster_gcp_cluster_name', with: 'dev-cluster' + fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' + fill_in 'cluster_name', with: 'dev-cluster' click_button 'Create cluster' end it 'user sees a cluster details page and creation status' do expect(page).to have_content('Cluster is being created on Google Container Engine...') - Gcp::Cluster.last.make_created! + Clusters::Cluster.last.provider.make_created! expect(page).to have_content('Cluster was successfully created on Google Container Engine') end @@ -62,7 +66,8 @@ feature 'Clusters', :js do end context 'when user has a cluster and visits cluster index page' do - let!(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service, project: project) } + let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } before do visit project_clusters_path(project) @@ -70,7 +75,7 @@ feature 'Clusters', :js do it 'user sees an cluster details page' do expect(page).to have_button('Save') - expect(page.find(:css, '.cluster-name').value).to eq(cluster.gcp_cluster_name) + expect(page.find(:css, '.cluster-name').value).to eq(cluster.name) end context 'when user disables the cluster' do diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index d11ce690601..df68720ffd8 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -51,45 +51,45 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end end - context 'when validates api_url' do - let(:kubernetes) { build(:platform_kubernetes, :configured) } + # context 'when validates api_url' do + # let(:kubernetes) { build(:platform_kubernetes, :configured) } - before do - kubernetes.api_url = api_url - end + # before do + # kubernetes.api_url = api_url + # end - context 'when api_url is invalid url' do - let(:api_url) { '!!!!!!' } + # context 'when api_url is invalid url' do + # let(:api_url) { '!!!!!!' } - it { expect(kubernetes.save).to be_falsey } - end + # it { expect(kubernetes.save).to be_falsey } + # end - context 'when api_url is nil' do - let(:api_url) { nil } + # context 'when api_url is nil' do + # let(:api_url) { nil } - it { expect(kubernetes.save).to be_falsey } - end + # it { expect(kubernetes.save).to be_falsey } + # end - context 'when api_url is valid url' do - let(:api_url) { 'https://111.111.111.111' } + # 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 + # it { expect(kubernetes.save).to be_truthy } + # end + # end - context 'when validates token' do - let(:kubernetes) { build(:platform_kubernetes, :configured) } + # context 'when validates token' do + # let(:kubernetes) { build(:platform_kubernetes, :configured) } - before do - kubernetes.token = token - end + # before do + # kubernetes.token = token + # end - context 'when token is nil' do - let(:token) { nil } + # context 'when token is nil' do + # let(:token) { nil } - it { expect(kubernetes.save).to be_falsey } - end - end + # it { expect(kubernetes.save).to be_falsey } + # end + # end end describe '#actual_namespace' do diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 2298dcab55f..0840a867aed 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -156,7 +156,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do let(:discovery_url) { 'https://kubernetes.example.com/api/v1' } before do - stub_kubeclient_discover + stub_kubeclient_discover(service.api_url) end context 'with path prefix in api_url' do @@ -164,7 +164,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do it 'tests with the prefix' do service.api_url = 'https://kubernetes.example.com/prefix' - stub_kubeclient_discover + stub_kubeclient_discover(service.api_url) expect(service.test[:success]).to be_truthy expect(WebMock).to have_requested(:get, discovery_url).once diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb index 14f88d3f0f5..b33578d6acd 100644 --- a/spec/services/clusters/create_service_spec.rb +++ b/spec/services/clusters/create_service_spec.rb @@ -6,57 +6,122 @@ describe Clusters::CreateService do let(:user) { create(:user) } let(:result) { described_class.new(project, user, params).execute(access_token) } - context 'when correct params' do - let(:params) do - { - name: 'test-cluster', - platform_type: :kubernetes, - provider_type: :gcp, - platform_kubernetes_attributes: { - namespace: 'custom-namespace' - }, - provider_gcp_attributes: { - gcp_project_id: 'gcp-project', - zone: 'us-central1-a', - num_nodes: 1, - machine_type: 'machine_type-a' + context 'when provider is gcp' do + context 'when correct params' do + let(:params) do + { + name: 'test-cluster', + platform_type: :kubernetes, + provider_type: :gcp, + platform_kubernetes_attributes: { + namespace: 'custom-namespace' + }, + 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::Platforms::Kubernetes.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.namespace).to eq('custom-namespace') + expect(result.platform.valid?).to be_falsey + end end - it 'creates a cluster object and performs a worker' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { result }.to change { Clusters::Cluster.count }.by(1) - 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.namespace).to eq('custom-namespace') + context 'when invalid params' do + let(:params) do + { + name: 'test-cluster', + platform_type: :kubernetes, + provider_type: :gcp, + platform_kubernetes_attributes: { + namespace: 'custom-namespace' + }, + 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 - context 'when invalid params' do - let(:params) do - { - name: 'test-cluster', - platform_type: :kubernetes, - provider_type: :user, - provider_gcp_attributes: { - gcp_project_id: 'gcp-project', - zone: 'us-central1-a', - num_nodes: 'ABC' + context 'when provider is user' do + context 'when correct params' do + let(:params) do + { + name: 'test-cluster', + platform_type: :kubernetes, + provider_type: :user, + platform_kubernetes_attributes: { + namespace: 'custom-namespace', + api_url: 'https://111.111.111.111', + token: 'token' + } } - } + 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::Platforms::Kubernetes.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).to be_nil + expect(result.platform.namespace).to eq('custom-namespace') + expect(result.platform.valid?).to be_truthy + end 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.num_nodes"]).to be_present + context 'when invalid params' do + let(:params) do + { + name: 'test-cluster', + platform_type: :kubernetes, + provider_type: :user, + platform_kubernetes_attributes: { + namespace: 'custom-namespace', + api_url: '!!!!!', + token: 'token' + } + } + 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[:"platform_kubernetes.api_url"]).to be_present + end end end end -- cgit v1.2.1 From af13ffbcedd8952433d4d3f6693aac3450c5fa6f Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Wed, 1 Nov 2017 01:16:29 +0000 Subject: Dont emit toggle-markdown event if current target is already active and add specs --- .../vue_shared/components/markdown/header.vue | 16 +++++++--- .../vue_shared/components/markdown/field_spec.js | 36 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 5bf2a90cc3b..ef1c2dcf638 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -16,12 +16,18 @@ toolbarButton, }, methods: { + isMarkdownForm(form) { + return form && !form.find('.js-vue-markdown-field').length; + }, + + isActiveTarget(target) { + return target.closest('li').classList.contains('active'); + }, + toggleMarkdownPreview(e, form) { - if (form && !form.find('.js-vue-markdown-field').length) { - return; - } else if (e.target.blur) { - e.target.blur(); - } + if (e.target.blur) e.target.blur(); + + if (this.isMarkdownForm(form) || this.isActiveTarget(e.target)) return; this.$emit('toggle-markdown'); }, diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 60a5c2ae74e..734266d0ebb 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -39,6 +39,7 @@ describe('Markdown field component', () => { describe('markdown preview', () => { let previewLink; + let writeLink; beforeEach(() => { spyOn(Vue.http, 'post').and.callFake(() => new Promise((resolve) => { @@ -52,6 +53,7 @@ describe('Markdown field component', () => { })); previewLink = vm.$el.querySelector('.nav-links li:nth-child(2) a'); + writeLink = vm.$el.querySelector('.nav-links li:nth-child(1) a'); }); it('sets preview link as active', (done) => { @@ -103,6 +105,40 @@ describe('Markdown field component', () => { done(); }, 0); }); + + function assertLinks(isWrite) { + expect(writeLink.parentNode.classList.contains('active')).toEqual(isWrite); + expect(previewLink.parentNode.classList.contains('active')).toEqual(!isWrite); + expect(vm.$el.querySelector('.md-preview').style.display).toEqual(isWrite ? 'none' : ''); + } + + it('clicking already active write or preview link does nothing', (done) => { + writeLink.click(); + + setTimeout(() => { + assertLinks(true); + + writeLink.click(); + + setTimeout(() => { + assertLinks(true); + + previewLink.click(); + + setTimeout(() => { + assertLinks(false); + + previewLink.click(); + + setTimeout(() => { + assertLinks(false); + + done(); + }); + }); + }); + }); + }); }); describe('markdown buttons', () => { -- cgit v1.2.1 From 6571efb6c3afd568c019e7bb46aba84328a4e821 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Wed, 1 Nov 2017 16:12:44 +0900 Subject: Fix spec. Fix usage ping. Fix warnings by adding new models and attributes. --- app/models/clusters/platforms/kubernetes.rb | 6 +-- app/services/clusters/create_service.rb | 12 ++++- ...1013094327_create_new_clusters_architectures.rb | 2 +- lib/gitlab/import_export/relation_factory.rb | 4 +- lib/gitlab/usage_data.rb | 6 +-- .../projects/clusters_controller_spec.rb | 4 +- .../gitlab/import_export/safe_model_attributes.yml | 43 +++++++++++++++++ spec/lib/gitlab/usage_data_spec.rb | 6 +-- spec/models/clusters/platforms/kubernetes_spec.rb | 56 +++++++++++----------- spec/services/clusters/create_service_spec.rb | 4 +- .../clusters/gcp/finalize_creation_service_spec.rb | 2 +- spec/support/google_api/cloud_platform_helpers.rb | 5 ++ 12 files changed, 102 insertions(+), 48 deletions(-) diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index e30ab805f1e..52022509d49 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -30,10 +30,8 @@ module Clusters message: Gitlab::Regex.kubernetes_namespace_regex_message } - # TODO: when cluster.gcp? skip validation when create a record - # TODO: when cluster.user? validates always - # validates :api_url, url: true, presence: true - # validates :token, presence: true + validates :api_url, url: true, presence: true + validates :token, presence: true after_save :clear_reactive_cache! diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index 8c30e247fdc..503118fa6b6 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -2,6 +2,9 @@ module Clusters class CreateService < BaseService attr_reader :access_token + TEMPOLARY_API_URL = 'http://tempolary_api_url'.freeze + TEMPOLARY_TOKEN = 'tempolary_token'.freeze + def execute(access_token) @access_token = access_token @@ -28,8 +31,13 @@ module Clusters def cluster_params return @cluster_params if defined?(@cluster_params) - params[:provider_gcp_attributes].try do |h| - h[:access_token] = access_token + params[:provider_gcp_attributes].try do |provider| + provider[:access_token] = access_token + + params[:platform_kubernetes_attributes].try do |platform| + platform[:api_url] = TEMPOLARY_API_URL + platform[:token] = TEMPOLARY_TOKEN + end end @cluster_params = params.merge(user: current_user) diff --git a/db/migrate/20171013094327_create_new_clusters_architectures.rb b/db/migrate/20171013094327_create_new_clusters_architectures.rb index 35df8cb4a60..2eae72595c6 100644 --- a/db/migrate/20171013094327_create_new_clusters_architectures.rb +++ b/db/migrate/20171013094327_create_new_clusters_architectures.rb @@ -22,7 +22,7 @@ class CreateNewClustersArchitectures < ActiveRecord::Migration 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 } diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 469b230377d..a790dcfe8a6 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -8,8 +8,8 @@ module Gitlab triggers: 'Ci::Trigger', pipeline_schedules: 'Ci::PipelineSchedule', builds: 'Ci::Build', - cluster: 'Gcp::Cluster', - clusters: 'Gcp::Cluster', + cluster: 'Clusters::Cluster', + clusters: 'Clusters::Cluster', hooks: 'ProjectHook', merge_access_levels: 'ProtectedBranch::MergeAccessLevel', push_access_levels: 'ProtectedBranch::PushAccessLevel', diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 70a403652e7..112d4939582 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -48,9 +48,9 @@ module Gitlab deploy_keys: DeployKey.count, deployments: Deployment.count, environments: ::Environment.count, - gcp_clusters: ::Gcp::Cluster.count, - gcp_clusters_enabled: ::Gcp::Cluster.enabled.count, - gcp_clusters_disabled: ::Gcp::Cluster.disabled.count, + clusters: ::Clusters::Cluster.count, + clusters_enabled: ::Clusters::Cluster.enabled.count, + clusters_disabled: ::Clusters::Cluster.disabled.count, in_review_folder: ::Environment.in_review_folder.count, groups: Group.count, issues: Issue.count, diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 4661f959693..c7d3c945430 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -72,7 +72,7 @@ describe Projects::ClustersController do go expect(assigns(:authorize_url)).to include(key) - expect(session[session_key_for_redirect_uri]).to eq(namespace_project_clusters_url(project.namespace, project)) + expect(session[session_key_for_redirect_uri]).to eq(project_clusters_url(project)) end end @@ -175,7 +175,7 @@ describe Projects::ClustersController do platform_type: :kubernetes, provider_type: :gcp, provider_gcp_attributes: { - gcp_project_id: '111', + gcp_project_id: '111' } } } diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 89d30407077..d8dc3672d40 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -313,6 +313,49 @@ Ci::PipelineSchedule: - deleted_at - created_at - updated_at +Clusters::Cluster: +- id +- user_id +- enabled +- name +- provider_type +- platform_type +- created_at +- updated_at +Clusters::Project: +- id +- project_id +- cluster_id +- created_at +- updated_at +Clusters::Providers::Gcp: +- id +- cluster_id +- status +- status_reason +- gcp_project_id +- zone +- num_nodes +- machine_type +- operation_id +- endpoint +- encrypted_access_token +- encrypted_access_token_iv +- created_at +- updated_at +Clusters::Platforms::Kubernetes: +- id +- cluster_id +- api_url +- ca_cert +- namespace +- username +- encrypted_password +- encrypted_password_iv +- encrypted_token +- encrypted_token_iv +- created_at +- updated_at Gcp::Cluster: - id - project_id diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index a7b65e94706..a4c1113ae37 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -60,9 +60,9 @@ describe Gitlab::UsageData do deploy_keys deployments environments - gcp_clusters - gcp_clusters_enabled - gcp_clusters_disabled + clusters + clusters_enabled + clusters_disabled in_review_folder groups issues diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index df68720ffd8..d11ce690601 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -51,45 +51,45 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end end - # context 'when validates api_url' do - # let(:kubernetes) { build(:platform_kubernetes, :configured) } + context 'when validates api_url' do + let(:kubernetes) { build(:platform_kubernetes, :configured) } - # before do - # kubernetes.api_url = api_url - # end + before do + kubernetes.api_url = api_url + end - # context 'when api_url is invalid url' do - # let(:api_url) { '!!!!!!' } + context 'when api_url is invalid url' do + let(:api_url) { '!!!!!!' } - # it { expect(kubernetes.save).to be_falsey } - # end + it { expect(kubernetes.save).to be_falsey } + end - # context 'when api_url is nil' do - # let(:api_url) { nil } + context 'when api_url is nil' do + let(:api_url) { nil } - # it { expect(kubernetes.save).to be_falsey } - # end + it { expect(kubernetes.save).to be_falsey } + end - # context 'when api_url is valid url' do - # let(:api_url) { 'https://111.111.111.111' } + 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 + it { expect(kubernetes.save).to be_truthy } + end + end - # context 'when validates token' do - # let(:kubernetes) { build(:platform_kubernetes, :configured) } + context 'when validates token' do + let(:kubernetes) { build(:platform_kubernetes, :configured) } - # before do - # kubernetes.token = token - # end + before do + kubernetes.token = token + end - # context 'when token is nil' do - # let(:token) { nil } + context 'when token is nil' do + let(:token) { nil } - # it { expect(kubernetes.save).to be_falsey } - # end - # end + it { expect(kubernetes.save).to be_falsey } + end + end end describe '#actual_namespace' do diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb index b33578d6acd..9ed6ae8c2fc 100644 --- a/spec/services/clusters/create_service_spec.rb +++ b/spec/services/clusters/create_service_spec.rb @@ -42,7 +42,8 @@ describe Clusters::CreateService do expect(result.provider.machine_type).to eq('machine_type-a') expect(result.provider.access_token).to eq(access_token) expect(result.platform.namespace).to eq('custom-namespace') - expect(result.platform.valid?).to be_falsey + expect(result.platform.api_url).to eq(Clusters::CreateService::TEMPOLARY_API_URL) + expect(result.platform.token).to eq(Clusters::CreateService::TEMPOLARY_TOKEN) end end @@ -99,7 +100,6 @@ describe Clusters::CreateService do expect(result.project).to eq(project) expect(result.provider).to be_nil expect(result.platform.namespace).to eq('custom-namespace') - expect(result.platform.valid?).to be_truthy end end diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb index 70ea1cbca46..ca7741f641b 100644 --- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb +++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb @@ -40,7 +40,7 @@ describe Clusters::Gcp::FinalizeCreationService do { endpoint: endpoint, username: username, - password: password, + password: password } ) diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb index ef73add3991..4b785611ab5 100644 --- a/spec/support/google_api/cloud_platform_helpers.rb +++ b/spec/support/google_api/cloud_platform_helpers.rb @@ -61,6 +61,11 @@ module GoogleApi 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', -- cgit v1.2.1 From 0c417ef0435ca49dba451a7270235f775d1d9a75 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Wed, 1 Nov 2017 21:28:47 +0900 Subject: Improve migration file. Add migration spec. Reorder columns of the table. --- ...1013094327_create_new_clusters_architectures.rb | 26 ++++--- ...e_gcp_clusters_to_new_clusters_architectures.rb | 86 +++++++++++++++++++++ ...cp_clusters_to_new_clusters_architectures.rb.rb | 84 --------------------- db/schema.rb | 21 +++--- ..._clusters_to_new_clusters_architectures_spec.rb | 88 ++++++++++++++++++++++ 5 files changed, 200 insertions(+), 105 deletions(-) create mode 100644 db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb delete mode 100644 db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb.rb create mode 100644 spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb diff --git a/db/migrate/20171013094327_create_new_clusters_architectures.rb b/db/migrate/20171013094327_create_new_clusters_architectures.rb index 2eae72595c6..a212288f859 100644 --- a/db/migrate/20171013094327_create_new_clusters_architectures.rb +++ b/db/migrate/20171013094327_create_new_clusters_architectures.rb @@ -3,16 +3,17 @@ class CreateNewClustersArchitectures < ActiveRecord::Migration def change create_table :clusters do |t| - t.references :user, foreign_key: { on_delete: :nullify } - - t.boolean :enabled, default: true - t.string :name, null: false # If manual, read-write. If gcp, read-only. + t.references :user, null: false, index: true, foreign_key: { on_delete: :nullify } t.integer :provider_type, null: false t.integer :platform_type, null: false 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| @@ -26,7 +27,10 @@ class CreateNewClustersArchitectures < ActiveRecord::Migration create_table :cluster_platforms_kubernetes do |t| t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } - t.string :api_url + 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 @@ -37,20 +41,21 @@ class CreateNewClustersArchitectures < ActiveRecord::Migration t.text :encrypted_token t.string :encrypted_token_iv - - t.datetime_with_timezone :created_at, null: false - t.datetime_with_timezone :updated_at, null: false 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.integer :num_nodes, null: false t.string :machine_type t.string :operation_id @@ -58,9 +63,6 @@ class CreateNewClustersArchitectures < ActiveRecord::Migration t.text :encrypted_access_token t.string :encrypted_access_token_iv - - t.datetime_with_timezone :created_at, null: false - t.datetime_with_timezone :updated_at, null: false end end end diff --git a/db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb b/db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb new file mode 100644 index 00000000000..6ff98899bcb --- /dev/null +++ b/db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb @@ -0,0 +1,86 @@ +class MigrateGcpClustersToNewClustersArchitectures < ActiveRecord::Migration + DOWNTIME = false + + def up + gcp_clusters = ActiveRecord::Base.connection.select_all('SELECT * from gcp_clusters;') + + rows_for_clusters = Array.new + rows_for_cluster_projects = Array.new + rows_for_cluster_providers_gcp = Array.new + rows_for_cluster_platforms_kubernetes = Array.new + + gcp_clusters.each do |gcp_cluster| + rows_for_clusters << params_for_clusters(gcp_cluster) + rows_for_cluster_projects << params_for_cluster_projects(gcp_cluster) + rows_for_cluster_providers_gcp << params_for_cluster_providers_gcp(gcp_cluster) + rows_for_cluster_platforms_kubernetes << params_for_cluster_platforms_kubernetes(gcp_cluster) + end + + Gitlab::Database.bulk_insert('clusters', rows_for_clusters) + Gitlab::Database.bulk_insert('cluster_projects', rows_for_cluster_projects) + Gitlab::Database.bulk_insert('cluster_providers_gcp', rows_for_cluster_providers_gcp) + Gitlab::Database.bulk_insert('cluster_platforms_kubernetes', rows_for_cluster_platforms_kubernetes) + end + + def down + execute('DELETE FROM clusters') + end + + private + + def params_for_clusters(gcp_cluster) + { + id: gcp_cluster['id'], + user_id: gcp_cluster['user_id'], + enabled: gcp_cluster['enabled'], + name: gcp_cluster['gcp_cluster_name'], + provider_type: Clusters::Cluster.provider_types[:gcp], + platform_type: Clusters::Cluster.platform_types[:kubernetes], + created_at: gcp_cluster['created_at'], + updated_at: gcp_cluster['updated_at'] + } + end + + def params_for_cluster_projects(gcp_cluster) + { + cluster_id: gcp_cluster['id'], + project_id: gcp_cluster['project_id'], + created_at: gcp_cluster['created_at'], + updated_at: gcp_cluster['updated_at'] + } + end + + def params_for_cluster_providers_gcp(gcp_cluster) + { + cluster_id: gcp_cluster['id'], + 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'], + created_at: gcp_cluster['created_at'], + updated_at: gcp_cluster['updated_at'] + } + end + + def params_for_cluster_platforms_kubernetes(gcp_cluster) + { + cluster_id: gcp_cluster['id'], + api_url: 'https://' + 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'], + created_at: gcp_cluster['created_at'], + updated_at: gcp_cluster['updated_at'] + } + end +end diff --git a/db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb.rb b/db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb.rb deleted file mode 100644 index 5510b036d24..00000000000 --- a/db/migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb.rb +++ /dev/null @@ -1,84 +0,0 @@ -class MigrateGcpClustersToNewClustersArchitectures < ActiveRecord::Migration - DOWNTIME = false - - def up - # TODO: Chnage to something reaistic - ActiveRecord::Base.connection.select_rows('SELECT * from gcp_clusters;').each do |old_cluster| - id = old_cluster[0] - project_id = old_cluster[1] - user_id = old_cluster[2] - service_id = old_cluster[3] - status = old_cluster[4] - gcp_cluster_size = old_cluster[5] - created_at = old_cluster[6] - updated_at = old_cluster[7] - enabled = old_cluster[8] - status_reason = old_cluster[9] - project_namespace = old_cluster[10] - endpoint = old_cluster[11] - ca_cert = old_cluster[12] - encrypted_kubernetes_token = old_cluster[13] - encrypted_kubernetes_token_iv = old_cluster[14] - username = old_cluster[15] - encrypted_password = old_cluster[16] - encrypted_password_iv = old_cluster[17] - gcp_project_id = old_cluster[18] - gcp_cluster_zone = old_cluster[19] - gcp_cluster_name = old_cluster[20] - gcp_machine_type = old_cluster[21] - gcp_operation_id = old_cluster[22] - encrypted_gcp_token = old_cluster[23] - encrypted_gcp_token_iv = old_cluster[24] - - cluster = Clusters::Cluster.create!( - user_id: user_id, - enabled: enabled, - name: gcp_cluster_name, - provider_type: :gcp, - platform_type: :kubernetes, - created_at: created_at, - updated_at: updated_at) - - Clusters::Project.create!( - cluster: cluster, - project_id: project_id, - created_at: created_at, - updated_at: updated_at) - - Clusters::Platforms::Kubernetes.create!( - cluster: cluster, - api_url: 'https://' + endpoint, - ca_cert: ca_cert, - namespace: project_namespace, - username: username, - encrypted_password: encrypted_password, - encrypted_password_iv: encrypted_password_iv, - encrypted_token: encrypted_kubernetes_token, - encrypted_token_iv: encrypted_kubernetes_token_iv, - created_at: created_at, - updated_at: updated_at) - - Clusters::Providers::Gcp.create!( - cluster: cluster, - status: status, - status_reason: status_reason, - gcp_project_id: gcp_project_id, - zone: gcp_cluster_zone, - num_nodes: gcp_cluster_size, - machine_type: gcp_machine_type, - operation_id: gcp_operation_id, - endpoint: endpoint, - encrypted_access_token: encrypted_gcp_token, - encrypted_access_token_iv: encrypted_gcp_token_iv, - created_at: created_at, - updated_at: updated_at) - end - end - - def down - Clusters::Cluster.delete_all - Clusters::Project.delete_all - Clusters::Providers::Gcp.delete_all - Clusters::Platforms::Kubernetes.delete_all - end -end diff --git a/db/schema.rb b/db/schema.rb index 611435af082..adf8b9594fb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -464,7 +464,9 @@ ActiveRecord::Schema.define(version: 20171017145932) do create_table "cluster_platforms_kubernetes", force: :cascade do |t| t.integer "cluster_id", null: false - t.string "api_url" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "api_url" t.text "ca_cert" t.string "namespace" t.string "username" @@ -472,8 +474,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do t.string "encrypted_password_iv" t.text "encrypted_token" t.string "encrypted_token_iv" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false end add_index "cluster_platforms_kubernetes", ["cluster_id"], name: "index_cluster_platforms_kubernetes_on_cluster_id", unique: true, using: :btree @@ -491,31 +491,34 @@ ActiveRecord::Schema.define(version: 20171017145932) do 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 "created_at", null: false + t.datetime "updated_at", null: false t.text "status_reason" t.string "gcp_project_id", null: false t.string "zone", null: false - t.integer "num_nodes", null: false t.string "machine_type" t.string "operation_id" t.string "endpoint" t.text "encrypted_access_token" t.string "encrypted_access_token_iv" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false 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.boolean "enabled", default: true - t.string "name", null: false + t.integer "user_id", null: false t.integer "provider_type", null: false t.integer "platform_type", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.boolean "enabled", default: true + t.string "name", null: false end + add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree + add_index "clusters", ["user_id"], name: "index_clusters_on_user_id", using: :btree + create_table "container_repositories", force: :cascade do |t| t.integer "project_id", null: false t.string "name", null: false diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb new file mode 100644 index 00000000000..de9a42b46d8 --- /dev/null +++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' +require Rails.root.join('db', '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) } + + let(:project_id) { project.id } + let(:user_id) { user.id } + let(:service_id) { service.id } + let(:status) { 3 } + let(:gcp_cluster_size) { 1 } + let(:created_at) { '2017-10-17 20:24:02.219679' } + let(:updated_at) { '2017-10-17 20:28:44.738998' } + 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 eq(enabled) + expect(cluster.name).to eq(gcp_cluster_name) + expect(cluster.provider_type).to eq('gcp') + expect(cluster.platform_type).to eq('kubernetes') + expect(cluster.created_at).to eq(created_at) + expect(cluster.updated_at).to eq(updated_at) + + expect(cluster.project).to eq(project) + + expect(cluster.provider_gcp.cluster).to eq(cluster) + expect(cluster.provider_gcp.status).to eq(status) + expect(cluster.provider_gcp.status_reason).to eq(status_reason) + expect(cluster.provider_gcp.gcp_project_id).to eq(gcp_project_id) + expect(cluster.provider_gcp.zone).to eq(gcp_cluster_zone) + expect(cluster.provider_gcp.num_nodes).to eq(gcp_cluster_size) + expect(cluster.provider_gcp.machine_type).to eq(gcp_machine_type) + expect(cluster.provider_gcp.operation_id).to eq(gcp_operation_id) + expect(cluster.provider_gcp.endpoint).to eq(endpoint) + expect(cluster.provider_gcp.encrypted_access_token).to eq(encrypted_gcp_token) + expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(encrypted_gcp_token_iv) + expect(cluster.provider_gcp.created_at).to eq(created_at) + expect(cluster.provider_gcp.updated_at).to eq(updated_at) + + expect(cluster.platform_kubernetes.cluster).to eq(cluster) + expect(cluster.platform_kubernetes.api_url).to eq('https://' + endpoint) + expect(cluster.platform_kubernetes.ca_cert).to eq(ca_cert) + expect(cluster.platform_kubernetes.namespace).to eq(project_namespace) + expect(cluster.platform_kubernetes.username).to eq(username) + expect(cluster.platform_kubernetes.encrypted_password).to eq(encrypted_password) + expect(cluster.platform_kubernetes.encrypted_password_iv).to eq(encrypted_password_iv) + expect(cluster.platform_kubernetes.encrypted_token).to eq(encrypted_kubernetes_token) + expect(cluster.platform_kubernetes.encrypted_token_iv).to eq(encrypted_kubernetes_token_iv) + expect(cluster.platform_kubernetes.created_at).to eq(created_at) + expect(cluster.platform_kubernetes.updated_at).to eq(updated_at) + end +end -- cgit v1.2.1 From ce22977fb6a70e75ada07b9a670cfc079bb4fd26 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Wed, 1 Nov 2017 12:46:05 +0000 Subject: Use settimeoutpromise in field_spec --- .../vue_shared/components/markdown/field_spec.js | 37 ++++++++-------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 734266d0ebb..40cfafdf792 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import fieldComponent from '~/vue_shared/components/markdown/field.vue'; +import setTimeoutPromise from '../../../helpers/set_timeout_promise_helper'; describe('Markdown field component', () => { let vm; @@ -115,29 +116,19 @@ describe('Markdown field component', () => { it('clicking already active write or preview link does nothing', (done) => { writeLink.click(); - setTimeout(() => { - assertLinks(true); - - writeLink.click(); - - setTimeout(() => { - assertLinks(true); - - previewLink.click(); - - setTimeout(() => { - assertLinks(false); - - previewLink.click(); - - setTimeout(() => { - assertLinks(false); - - done(); - }); - }); - }); - }); + setTimeoutPromise() + .then(() => assertLinks(true)) + .then(() => writeLink.click()) + .then(() => setTimeoutPromise()) + .then(() => assertLinks(true)) + .then(() => previewLink.click()) + .then(() => setTimeoutPromise()) + .then(() => assertLinks(false)) + .then(() => previewLink.click()) + .then(() => setTimeoutPromise()) + .then(() => assertLinks(false)) + .then(done) + .catch(done.fail); }); }); -- cgit v1.2.1 From ccf09824f6d3ef41db4be3b40aa99b6dfd0dc9ac Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 1 Nov 2017 12:57:05 +0100 Subject: Slim down Platforms::Kubernetes, and instead make it instrument KubernetesService --- app/models/clusters/cluster.rb | 7 +- app/models/clusters/platforms/kubernetes.rb | 141 +++++++--------------------- app/services/clusters/create_service.rb | 19 +--- db/schema.rb | 4 +- 4 files changed, 43 insertions(+), 128 deletions(-) diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 177403dcf00..a3f6d20ba43 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -9,8 +9,11 @@ module Clusters has_many :cluster_projects, class_name: 'Clusters::Project' has_many :projects, through: :cluster_projects, class_name: '::Project' - has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp' - has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes' + # 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 diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 52022509d49..4c3e270892e 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -2,11 +2,8 @@ module Clusters module Platforms class Kubernetes < ActiveRecord::Base include Gitlab::CurrentSettings - include Gitlab::Kubernetes - include ReactiveCaching self.table_name = 'cluster_platforms_kubernetes' - self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.cluster_id] } belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster' @@ -30,17 +27,24 @@ module Clusters message: Gitlab::Regex.kubernetes_namespace_regex_message } - validates :api_url, url: true, presence: true - validates :token, presence: true + # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned) + with_options presence: true, if: :active? do + validates :api_url, url: true, presence: true + validates :token, presence: true + end - after_save :clear_reactive_cache! + # TODO: Glue code till we migrate Kubernetes Integration into Platforms::Kubernetes + after_save :update_kubernetes_integration! + 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 - alias_method :active?, :enabled? + def active? + enabled? && api_url.present? + end class << self def namespace_for_project(project) @@ -60,122 +64,43 @@ module Clusters self.class.namespace_for_project(project) if project end - def predefined_variables - config = YAML.dump(kubeconfig) - - variables = [ - { key: 'KUBE_URL', value: api_url, public: true }, - { key: 'KUBE_TOKEN', value: token, public: false }, - { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true }, - { key: 'KUBECONFIG', value: config, public: false, file: true } - ] - - if ca_pem.present? - variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } - variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } - end - - variables - end - - # Constructs a list of terminals from the reactive cache - # - # Returns nil if the cache is empty, in which case you should try again a - # short time later - def terminals(environment) - with_reactive_cache do |data| - pods = filter_by_label(data[:pods], app: environment.slug) - terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) } - terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) } - end - end - - # Caches resources in the namespace so other calls don't need to block on - # network access - def calculate_reactive_cache - return unless active? && project && !project.pending_delete? - - # We may want to cache extra things in the future - { pods: read_pods } - end - - def kubeconfig - to_kubeconfig( - url: api_url, - namespace: actual_namespace, - token: token, - ca_pem: ca_pem) - end - - def read_secrets - kubeclient = build_kubeclient! + private - kubeclient.get_secrets.as_json + def enforce_namespace_to_lower_case + self.namespace = self.namespace&.downcase end - # Returns a hash of all pods in the namespace - def read_pods - kubeclient = build_kubeclient! + # TODO: glue code till we migrate Kubernetes Service into Platforms::Kubernetes class + def manages_kubernetes_service? + return true unless kubernetes_service&.active? - kubeclient.get_pods(namespace: actual_namespace).as_json - rescue KubeException => err - raise err unless err.error_code == 404 - [] + kubernetes_service.api_url == api_url end - def kubeclient_ssl_options - opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - - if ca_pem.present? - opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) - end + def destroy_kubernetes_integration! + return unless manages_kubernetes_service? - opts + kubernetes_service.destroy! end - private - - def build_kubeclient!(api_path: 'api', api_version: 'v1') - raise "Incomplete settings" unless api_url && actual_namespace + def update_kubernetes_integration! + return raise 'Kubernetes service already configured' unless manages_kubernetes_service? - unless (username && password) || token - raise "Either username/password or token is required to access API" - end - - ::Kubeclient::Client.new( - join_api_url(api_path), - api_version, - auth_options: kubeclient_auth_options, - ssl_options: kubeclient_ssl_options, - http_proxy_uri: ENV['http_proxy'] + ensure_kubernetes_service.update!( + active: active?, + api_url: api_url, + namespace: namespace, + token: token, + ca_pem: ca_cert, ) end - def kubeclient_auth_options - return { username: username, password: password } if username && password - return { bearer_token: token } if token - end - - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s + def kubernetes_service + @kubernetes_service ||= project.kubernetes_service || project.build_kubernetes_service end - def terminal_auth - { - token: token, - ca_pem: ca_pem, - max_session_time: current_application_settings.terminal_max_session_time - } - end - - def enforce_namespace_to_lower_case - self.namespace = self.namespace&.downcase + def ensure_kubernetes_service + @kubernetes_service ||= kubernetes_service || project.build_kubernetes_service end end end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index 503118fa6b6..a1c74566d7a 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -2,9 +2,6 @@ module Clusters class CreateService < BaseService attr_reader :access_token - TEMPOLARY_API_URL = 'http://tempolary_api_url'.freeze - TEMPOLARY_TOKEN = 'tempolary_token'.freeze - def execute(access_token) @access_token = access_token @@ -16,14 +13,9 @@ module Clusters private def create_cluster - cluster = nil - - ActiveRecord::Base.transaction do - cluster = Clusters::Cluster.create!(cluster_params) - cluster.projects << project - end - - cluster + Clusters::Cluster.create!( + cluster_params.merge( + projects: [project])) rescue ActiveRecord::RecordInvalid => e e.record end @@ -33,11 +25,6 @@ module Clusters params[:provider_gcp_attributes].try do |provider| provider[:access_token] = access_token - - params[:platform_kubernetes_attributes].try do |platform| - platform[:api_url] = TEMPOLARY_API_URL - platform[:token] = TEMPOLARY_TOKEN - end end @cluster_params = params.merge(user: current_user) diff --git a/db/schema.rb b/db/schema.rb index adf8b9594fb..24f2d4b439c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -481,8 +481,8 @@ ActiveRecord::Schema.define(version: 20171017145932) do create_table "cluster_projects", force: :cascade do |t| t.integer "project_id", null: false t.integer "cluster_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", 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 -- cgit v1.2.1 From 1427bdcadf5f4026d141a5c4e93db8b1b00fe40a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 1 Nov 2017 13:56:27 +0100 Subject: Revert back FetchKubernetesTokenService --- app/controllers/projects/clusters_controller.rb | 1 - app/models/clusters/cluster.rb | 2 - app/models/clusters/platforms/kubernetes.rb | 8 +-- app/services/ci/fetch_kubernetes_token_service.rb | 72 ++++++++++++++++++++++ .../clusters/gcp/finalize_creation_service.rb | 35 ++++------- app/views/projects/clusters/_form.html.haml | 8 +-- ...1013094327_create_new_clusters_architectures.rb | 4 +- db/schema.rb | 4 +- .../ci/fetch_kubernetes_token_service_spec.rb | 64 +++++++++++++++++++ 9 files changed, 156 insertions(+), 42 deletions(-) create mode 100644 app/services/ci/fetch_kubernetes_token_service.rb create mode 100644 spec/services/ci/fetch_kubernetes_token_service_spec.rb diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 0f35b4f9c21..7570da3e0b1 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -29,7 +29,6 @@ class Projects::ClustersController < Projects::ApplicationController def new @cluster = Clusters::Cluster.new.tap do |cluster| cluster.build_provider_gcp - cluster.build_platform_kubernetes end end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index a3f6d20ba43..ca09b939f34 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -18,8 +18,6 @@ module Clusters accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true - validates :provider_type, presence: true - validates :platform_type, presence: true validates :name, cluster_name: true validate :restrict_modification, on: :update diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 4c3e270892e..3ad2ffe531d 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -28,7 +28,7 @@ module Clusters } # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned) - with_options presence: true, if: :active? do + with_options presence: true, if: :enabled? do validates :api_url, url: true, presence: true validates :token, presence: true end @@ -42,10 +42,6 @@ module Clusters delegate :project, to: :cluster, allow_nil: true delegate :enabled?, to: :cluster, allow_nil: true - def active? - enabled? && api_url.present? - end - class << self def namespace_for_project(project) "#{project.path}-#{project.id}" @@ -87,7 +83,7 @@ module Clusters return raise 'Kubernetes service already configured' unless manages_kubernetes_service? ensure_kubernetes_service.update!( - active: active?, + active: enabled?, api_url: api_url, namespace: namespace, token: token, diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb new file mode 100644 index 00000000000..44da87cb00c --- /dev/null +++ b/app/services/ci/fetch_kubernetes_token_service.rb @@ -0,0 +1,72 @@ +## +# TODO: +# Almost components in this class were copied from app/models/project_services/kubernetes_service.rb +# We should dry up those classes not to repeat the same code. +# Maybe we should have a special facility (e.g. lib/kubernetes_api) to maintain all Kubernetes API caller. +module Ci + class FetchKubernetesTokenService + attr_reader :api_url, :ca_pem, :username, :password + + def initialize(api_url, ca_pem, username, password) + @api_url = api_url + @ca_pem = ca_pem + @username = username + @password = password + end + + def execute + read_secrets.each do |secret| + name = secret.dig('metadata', 'name') + if /default-token/ =~ name + token_base64 = secret.dig('data', 'token') + return Base64.decode64(token_base64) if token_base64 + end + end + + nil + end + + private + + def read_secrets + kubeclient = build_kubeclient! + + kubeclient.get_secrets.as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + + def build_kubeclient!(api_path: 'api', api_version: 'v1') + raise "Incomplete settings" unless api_url && username && password + + ::Kubeclient::Client.new( + join_api_url(api_path), + api_version, + auth_options: { username: username, password: password }, + ssl_options: kubeclient_ssl_options, + http_proxy_uri: ENV['http_proxy'] + ) + end + + def join_api_url(api_path) + url = URI.parse(api_url) + prefix = url.path.sub(%r{/+\z}, '') + + url.path = [prefix, api_path].join("/") + + url.to_s + end + + def kubeclient_ssl_options + opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } + + if ca_pem.present? + opts[:cert_store] = OpenSSL::X509::Store.new + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + end + + opts + end + end +end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index d379870924a..53b13518771 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -9,10 +9,7 @@ module Clusters configure_provider configure_kubernetes - ActiveRecord::Base.transaction do - kubernetes.save! - provider.make_created! - end + provider.make_created! rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") rescue KubeException => e @@ -28,23 +25,21 @@ module Clusters end def configure_kubernetes - kubernetes.api_url = 'https://' + gke_cluster.endpoint - kubernetes.ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) - kubernetes.username = gke_cluster.master_auth.username - kubernetes.password = gke_cluster.master_auth.password - kubernetes.token = request_kuberenetes_token + 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 - kubernetes.read_secrets.each do |secret| - name = secret.dig('metadata', 'name') - if /default-token/ =~ name - token_base64 = secret.dig('data', 'token') - return Base64.decode64(token_base64) if token_base64 - end - end - - nil + 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) end def gke_cluster @@ -57,10 +52,6 @@ module Clusters def cluster @cluster ||= provider.cluster end - - def kubernetes - @kubernetes ||= cluster.platform_kubernetes - end end end end diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml index 6b9f63b7515..d8e5b55bb88 100644 --- a/app/views/projects/clusters/_form.html.haml +++ b/app/views/projects/clusters/_form.html.haml @@ -5,8 +5,7 @@ = s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page} = form_for @cluster, url: namespace_project_clusters_path(@project.namespace, @project, @cluster), as: :cluster do |field| - = field.hidden_field :platform_type, :value => 'kubernetes' - = field.hidden_field :provider_type, :value => 'gcp' + = field.hidden_field :provider_type, value: :gcp = form_errors(@cluster) .form-group = field.label :name, s_('ClusterIntegration|Cluster name') @@ -32,10 +31,5 @@ = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer') = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4' - = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| - .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') - = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: Clusters::Platforms::Kubernetes.namespace_for_project(@project) - .form-group = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save' diff --git a/db/migrate/20171013094327_create_new_clusters_architectures.rb b/db/migrate/20171013094327_create_new_clusters_architectures.rb index a212288f859..b196aa1949c 100644 --- a/db/migrate/20171013094327_create_new_clusters_architectures.rb +++ b/db/migrate/20171013094327_create_new_clusters_architectures.rb @@ -5,8 +5,8 @@ class CreateNewClustersArchitectures < ActiveRecord::Migration create_table :clusters do |t| t.references :user, null: false, index: true, foreign_key: { on_delete: :nullify } - t.integer :provider_type, null: false - t.integer :platform_type, null: false + t.integer :provider_type + t.integer :platform_type t.datetime_with_timezone :created_at, null: false t.datetime_with_timezone :updated_at, null: false diff --git a/db/schema.rb b/db/schema.rb index 24f2d4b439c..d76977d45f2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -508,8 +508,8 @@ ActiveRecord::Schema.define(version: 20171017145932) do create_table "clusters", force: :cascade do |t| t.integer "user_id", null: false - t.integer "provider_type", null: false - t.integer "platform_type", null: false + t.integer "provider_type" + t.integer "platform_type" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "enabled", default: true diff --git a/spec/services/ci/fetch_kubernetes_token_service_spec.rb b/spec/services/ci/fetch_kubernetes_token_service_spec.rb new file mode 100644 index 00000000000..1d05c9671a9 --- /dev/null +++ b/spec/services/ci/fetch_kubernetes_token_service_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Ci::FetchKubernetesTokenService do + describe '#execute' do + subject { described_class.new(api_url, ca_pem, username, password).execute } + + let(:api_url) { 'http://111.111.111.111' } + let(:ca_pem) { '' } + let(:username) { 'admin' } + let(:password) { 'xxx' } + + context 'when params correct' do + let(:token) { 'xxx.token.xxx' } + + let(:secrets_json) do + [ + { + 'metadata': { + name: metadata_name + }, + 'data': { + 'token': Base64.encode64(token) + } + } + ] + end + + before do + allow_any_instance_of(Kubeclient::Client) + .to receive(:get_secrets).and_return(secrets_json) + end + + context 'when default-token exists' do + let(:metadata_name) { 'default-token-123' } + + it { is_expected.to eq(token) } + end + + context 'when default-token does not exist' do + let(:metadata_name) { 'another-token-123' } + + it { is_expected.to be_nil } + end + end + + context 'when api_url is nil' do + let(:api_url) { nil } + + it { expect { subject }.to raise_error("Incomplete settings") } + end + + context 'when username is nil' do + let(:username) { nil } + + it { expect { subject }.to raise_error("Incomplete settings") } + end + + context 'when password is nil' do + let(:password) { nil } + + it { expect { subject }.to raise_error("Incomplete settings") } + end + end +end -- cgit v1.2.1 From 3fc05267cac2d4a2e83a482d80afab958b1d2474 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Wed, 1 Nov 2017 13:17:06 +0000 Subject: Let field.vue handle the changing of preview --- .../javascripts/vue_shared/components/markdown/field.vue | 6 ++++-- .../vue_shared/components/markdown/header.vue | 16 +++++++--------- .../vue_shared/components/markdown/field_spec.js | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 8c0d9b9cda8..62c4e014e73 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -45,8 +45,10 @@ }, }, methods: { - toggleMarkdownPreview() { - this.previewMarkdown = !this.previewMarkdown; + toggleMarkdownPreview(isPreview) { + if (isPreview === this.previewMarkdown) return; + + this.previewMarkdown = isPreview; /* Can't use `$refs` as the component is technically in the parent component diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index ef1c2dcf638..d4a3f532a09 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -20,16 +20,12 @@ return form && !form.find('.js-vue-markdown-field').length; }, - isActiveTarget(target) { - return target.closest('li').classList.contains('active'); - }, - - toggleMarkdownPreview(e, form) { + toggleMarkdownPreview(e, isPreview, form) { if (e.target.blur) e.target.blur(); - if (this.isMarkdownForm(form) || this.isActiveTarget(e.target)) return; + if (this.isMarkdownForm(form)) return; - this.$emit('toggle-markdown'); + this.$emit('toggle-markdown', isPreview); }, }, mounted() { @@ -48,17 +44,19 @@