diff options
author | Kamil Trzcinski <ayufan@ayufan.eu> | 2017-10-13 19:21:23 +0200 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2017-10-23 08:57:52 +0300 |
commit | e1d12ba9b988e61afb9317f3a132d6e2caa93923 (patch) | |
tree | 2f68e95ed04d538dd0b4ddae338400b8af53379a /app/models/clusters | |
parent | c4cbf115db1ca719b97677057b984672a0900bf8 (diff) | |
download | gitlab-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.rb | 56 | ||||
-rw-r--r-- | app/models/clusters/cluster_project.rb | 6 | ||||
-rw-r--r-- | app/models/clusters/platforms/kubernetes.rb | 172 | ||||
-rw-r--r-- | app/models/clusters/providers/gcp.rb | 79 |
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 |