summaryrefslogtreecommitdiff
path: root/app/models/clusters
diff options
context:
space:
mode:
authorKamil Trzcinski <ayufan@ayufan.eu>2017-10-13 19:21:23 +0200
committerShinya Maeda <shinya@gitlab.com>2017-10-23 08:57:52 +0300
commite1d12ba9b988e61afb9317f3a132d6e2caa93923 (patch)
tree2f68e95ed04d538dd0b4ddae338400b8af53379a /app/models/clusters
parentc4cbf115db1ca719b97677057b984672a0900bf8 (diff)
downloadgitlab-ce-e1d12ba9b988e61afb9317f3a132d6e2caa93923.tar.gz
Refactor Clusters to be consisted from GcpProvider and KubernetesPlatform
Diffstat (limited to 'app/models/clusters')
-rw-r--r--app/models/clusters/cluster.rb56
-rw-r--r--app/models/clusters/cluster_project.rb6
-rw-r--r--app/models/clusters/platforms/kubernetes.rb172
-rw-r--r--app/models/clusters/providers/gcp.rb79
4 files changed, 313 insertions, 0 deletions
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