diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..438e1853435f89142fe1c1aa1a2182d2a150abfa --- /dev/null +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -0,0 +1,39 @@ +class Projects::Clusters::ApplicationsController < Projects::ApplicationController + before_action :cluster + before_action :application_class, only: [:create] + before_action :authorize_read_cluster! + before_action :authorize_create_cluster!, only: [:create] + + def new + end + + def create + return render_404 if application + + new_application = application_class.create(cluster: cluster) + + respond_to do |format| + format.json do + if new_application.persisted? + head :ok + else + head :bad_request + end + end + end + end + + private + + def cluster + @cluster ||= project.clusters.find_by(cluster_id: params[:cluster_id]).present(current_user: current_user) + end + + def application_class + Clusters::Cluster::Applications.find(params[:application]) + end + + def application + application_class.find_by(cluster: cluster) + end +end diff --git a/app/models/clusters/kubernetes/helm_app.rb b/app/models/clusters/applications/helm.rb similarity index 51% rename from app/models/clusters/kubernetes/helm_app.rb rename to app/models/clusters/applications/helm.rb index 32c9e13a469a0b067d68a08c255cd45d10e86bc9..59e0076c8df5fa379079811523ff13228ece7707 100644 --- a/app/models/clusters/kubernetes/helm_app.rb +++ b/app/models/clusters/applications/helm.rb @@ -1,14 +1,15 @@ module Clusters - module Kubernetes - class HelmApp < ActiveRecord::Base + module Applications + class Helm < ActiveRecord::Base + self.table_name = 'clusters_applications_helm' + NAME = 'helm'.freeze include ::Clusters::Concerns::AppStatus - belongs_to :kubernetes_service, class_name: 'KubernetesService', foreign_key: :service_id - default_value_for :version, Gitlab::Clusters::Helm::HELM_VERSION + belongs_to :cluser, class_name: 'Clusters::Cluster', foreign_key: :cluster_id - alias_method :cluster, :kubernetes_service + default_value_for :version, Gitlab::Clusters::Helm::HELM_VERSION def name NAME diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index ca09b939f34e27c6d1483dbaf276c25cd0f8d282..c814f475adf65b673c21990e0857ce292a4cd7a7 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -4,6 +4,10 @@ module Clusters self.table_name = 'clusters' + APPLICATIONS = { + Clusters::Applications::Helm::NAME => Clusters::Applications::Helm + } + belongs_to :user has_many :cluster_projects, class_name: 'Clusters::Project' @@ -15,13 +19,14 @@ module Clusters # 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 + has_one :application_helm, class_name: 'Clusters::Applications::Helm' + accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true validates :name, cluster_name: true validate :restrict_modification, on: :update - delegate :status, to: :provider, allow_nil: true delegate :status_reason, to: :provider, allow_nil: true delegate :status_name, to: :provider, allow_nil: true delegate :on_creation?, to: :provider, allow_nil: true @@ -38,6 +43,14 @@ module Clusters scope :enabled, -> { where(enabled: true) } scope :disabled, -> { where(enabled: false) } + def status_name + if provider + provider.status_name + else + :created + end + end + def provider return provider_gcp if gcp? end @@ -53,6 +66,10 @@ module Clusters end alias_method :project, :first_project + def kubeclient + platform_kubernetes.kubeclient if kubernetes? + end + private def restrict_modification diff --git a/app/models/clusters/kubernetes.rb b/app/models/clusters/kubernetes.rb deleted file mode 100644 index b68e2ae401ef216f3709e466a70a2cbb9727495d..0000000000000000000000000000000000000000 --- a/app/models/clusters/kubernetes.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Clusters - module Kubernetes - def self.table_name_prefix - 'clusters_kubernetes_' - end - - def self.app(app_name) - case app_name - when HelmApp::NAME - HelmApp - else - raise ArgumentError, "Unknown app #{app_name}" - end - end - end -end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 3ad2ffe531d07807a1a06239396686d30d799599..1197dfaefcb69d299a70822b2b5581120d560065 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -60,6 +60,10 @@ module Clusters self.class.namespace_for_project(project) if project end + def kubeclient + @kubeclient ||= kubernetes_service.kubeclient if manages_kubernetes_service? + end + private def enforce_namespace_to_lower_case diff --git a/app/models/project.rb b/app/models/project.rb index d9d9e12c947538a14104f4b12ded9578492065f5..a42e2553935a2341a4639bb269fe9bf772989b11 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -189,6 +189,7 @@ class Project < ActiveRecord::Base has_one :cluster_project, class_name: 'Clusters::Project' has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster' + has_many :clusters, 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/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index b4654e8d1ea398438452a2a1f1a11cd75ada4727..5080acffb3c5f1998dad4a433f2bb6a47732d225 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -3,8 +3,6 @@ class KubernetesService < DeploymentService include Gitlab::Kubernetes include ReactiveCaching - has_one :helm_app, class_name: 'Clusters::Kubernetes::HelmApp', foreign_key: :service_id - self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } # Namespace defaults to the project path, but can be overridden in case that @@ -138,8 +136,8 @@ class KubernetesService < DeploymentService { pods: read_pods } end - def helm - Gitlab::Clusters::Helm.new(build_kubeclient!) + def kubeclient + @kubeclient ||= build_kubeclient! end TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb index 01cb59d0d445971b8b2e6b8c2fb9b3cbf79e3569..5e11dac3bd287f2d959485d4104eb8056af100ec 100644 --- a/app/presenters/clusters/cluster_presenter.rb +++ b/app/presenters/clusters/cluster_presenter.rb @@ -5,5 +5,11 @@ module Clusters def gke_cluster_url "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp? end + + def applications + Clusters::Cluster::APPLICATIONS.map do |key, value| + value.find_by(cluster_id: id) + end.compact + end end end diff --git a/app/serializers/cluster_app_entity.rb b/app/serializers/cluster_app_entity.rb new file mode 100644 index 0000000000000000000000000000000000000000..7da2d4921a298536099ac003c7ff435727862007 --- /dev/null +++ b/app/serializers/cluster_app_entity.rb @@ -0,0 +1,5 @@ +class ClusterAppEntity < Grape::Entity + expose :name + expose :status_name, as: :status + expose :status_reason +end diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb index 84ce34afb32f217a244bd2ec9efbe7ce162353e2..e775c68eb6be49120ef8cdc157d4744dd6cd0e69 100644 --- a/app/serializers/cluster_entity.rb +++ b/app/serializers/cluster_entity.rb @@ -3,16 +3,5 @@ class ClusterEntity < Grape::Entity expose :status_name, as: :status expose :status_reason - expose :applications do |cluster, options| - if cluster.created? - { - helm: { status: 'installed' }, - ingress: { status: 'error', status_reason: 'Missing namespace' }, - runner: { status: 'installing' }, - prometheus: { status: 'installable' } - } - else - {} - end - end + expose :applications, using: ClusterAppEntity end diff --git a/app/services/clusters/base_helm_service.rb b/app/services/clusters/base_helm_service.rb index b8ed52bf376c44eaeb383a2e72bf6e3410a65383..c7f7e2d0877b407c59d121d34389a3d602e28b23 100644 --- a/app/services/clusters/base_helm_service.rb +++ b/app/services/clusters/base_helm_service.rb @@ -8,10 +8,16 @@ module Clusters protected - def helm - return @helm if defined?(@helm) + def cluster + app.cluster + end + + def kubeclient + cluster.kubeclient + end - @helm = @app.cluster.helm + def helm_api + @helm ||= Gitlab::Clusters::Helm.new(kubeclient) end end end diff --git a/app/services/clusters/fetch_app_installation_status_service.rb b/app/services/clusters/fetch_app_installation_status_service.rb index e21aa49bb43f44ad843e152f1f658d3f987ca99a..9b281c77c493434160f0307998fba1a8835b4bd1 100644 --- a/app/services/clusters/fetch_app_installation_status_service.rb +++ b/app/services/clusters/fetch_app_installation_status_service.rb @@ -3,8 +3,8 @@ module Clusters def execute return unless app.installing? - phase = helm.installation_status(app) - log = helm.installation_log(app) if phase == 'Failed' + phase = helm_api.installation_status(app) + log = helm_api.installation_log(app) if phase == 'Failed' yield(phase, log) if block_given? rescue KubeException => ke app.make_errored!("Kubernetes error: #{ke.message}") unless app.errored? diff --git a/app/services/clusters/finalize_app_installation_service.rb b/app/services/clusters/finalize_app_installation_service.rb index c921747febc065ba1a052feb4767d1c3e7fb53c6..b9d5da063eb03cdaf05bf0a7728fc7f9e682c4c6 100644 --- a/app/services/clusters/finalize_app_installation_service.rb +++ b/app/services/clusters/finalize_app_installation_service.rb @@ -1,7 +1,7 @@ module Clusters class FinalizeAppInstallationService < BaseHelmService def execute - helm.delete_installation_pod!(app) + helm_api.delete_installation_pod!(app) app.make_errored!('Installation aborted') if aborted? end diff --git a/app/services/clusters/install_app_service.rb b/app/services/clusters/install_app_service.rb index dd8556108d417fece70ea84d125bc6cdec1b2eda..496af2495fd8810dc1cfdbad8b1ade8173d09788 100644 --- a/app/services/clusters/install_app_service.rb +++ b/app/services/clusters/install_app_service.rb @@ -4,14 +4,14 @@ module Clusters return unless app.scheduled? begin - helm.install(app) + helm_api.install(app) + if app.make_installing ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker::INITIAL_INTERVAL, app.name, app.id) else app.make_errored!("Failed to update app record; #{app.errors}") end - rescue KubeException => ke app.make_errored!("Kubernetes error: #{ke.message}") rescue StandardError => e diff --git a/app/workers/concerns/cluster_app.rb b/app/workers/concerns/cluster_app.rb index 2170f8be6f669f51925358b2b2c2f7980b4a6778..a0202901f156b8ab6f6421cae704aff4f4141665 100644 --- a/app/workers/concerns/cluster_app.rb +++ b/app/workers/concerns/cluster_app.rb @@ -3,8 +3,9 @@ module ClusterApp included do def find_app(app_name, id) - app = Clusters::Kubernetes.app(app_name).find(id) - yield(app) if block_given? + Clusters::Applications.const_get(app_name.classify).find(id).try do |app| + yield(app) if block_given? + end end end end diff --git a/config/routes/project.rb b/config/routes/project.rb index 746c0c466778e0a770a8873a0c8cf0b339a1f002..a6a7c5e74828111cbe98199a504166248258dace 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -190,6 +190,10 @@ constraints(ProjectUrlConstrainer.new) do member do get :status, format: :json + + scope '*application' do + resource :applications, only: [:create] + end end end diff --git a/db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb b/db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb index 93611bf8a12168300069b29b275a7098bbcfecd4..1035adfe2b52ecd006b245fffb9678115d6419d3 100644 --- a/db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb +++ b/db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb @@ -1,12 +1,12 @@ class CreateClustersKubernetesHelmApps < ActiveRecord::Migration def change - create_table :clusters_kubernetes_helm_apps do |t| - t.integer :status, null: false + create_table :clusters_applications_helm do |t| + t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade } t.datetime_with_timezone :created_at, null: false t.datetime_with_timezone :updated_at, null: false - - t.references :service, index: true, null: false, foreign_key: { on_delete: :cascade } + + t.integer :status, null: false t.string :version, null: false t.text :status_reason end diff --git a/db/schema.rb b/db/schema.rb index d76977d45f2ef1d617ad183e577dd2eb943494e1..02df408b45e2477ba58f983f9670db0deb7ed066 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: 20171017145932) do +ActiveRecord::Schema.define(version: 20171031100710) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -464,9 +464,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do create_table "cluster_platforms_kubernetes", force: :cascade do |t| t.integer "cluster_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "api_url" + t.string "api_url" t.text "ca_cert" t.string "namespace" t.string "username" @@ -474,6 +472,8 @@ ActiveRecord::Schema.define(version: 20171017145932) do 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 add_index "cluster_platforms_kubernetes", ["cluster_id"], name: "index_cluster_platforms_kubernetes_on_cluster_id", unique: true, using: :btree @@ -491,33 +491,39 @@ 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_with_timezone "created_at", null: false + t.datetime_with_timezone "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", null: false - t.integer "provider_type" - t.integer "platform_type" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "user_id" t.boolean "enabled", default: true t.string "name", 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 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 "clusters_applications_helm", force: :cascade do |t| + t.integer "cluster_id", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "status", null: false + t.string "version", null: false + t.text "status_reason" + end create_table "container_repositories", force: :cascade do |t| t.integer "project_id", null: false @@ -1872,6 +1878,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do 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 "clusters_applications_helm", "clusters", on_delete: :cascade 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/spec/models/clusters/kubernetes/helm_app_spec.rb b/spec/models/clusters/kubernetes/helm_app_spec.rb index 27a1561ce6cad480854ba625f89ffbdad29f5b78..6e32e2e7037f7bd0bfd3145439f02963016e3fff 100644 --- a/spec/models/clusters/kubernetes/helm_app_spec.rb +++ b/spec/models/clusters/kubernetes/helm_app_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' require_relative '../kubernetes_spec' -RSpec.describe Clusters::Kubernetes::HelmApp, type: :model do +RSpec.describe Clusters::Applications::Helm, type: :model do it_behaves_like 'a registered kubernetes app' it { is_expected.to belong_to(:kubernetes_service) } diff --git a/spec/models/clusters/kubernetes_spec.rb b/spec/models/clusters/kubernetes_spec.rb deleted file mode 100644 index 5876f08250f07381dc07b6a55654ef888fc67f34..0000000000000000000000000000000000000000 --- a/spec/models/clusters/kubernetes_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'rails_helper' - -RSpec.shared_examples 'a registered kubernetes app' do - let(:name) { described_class::NAME } - - it 'can be retrieved with Clusters::Kubernetes.app' do - expect(Clusters::Kubernetes.app(name)).to eq(described_class) - end -end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 6d9fc28bf581747031b8cc974c23a94508138ceb..1c629155e1e132270306480739665aec0b4139bf 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -9,7 +9,6 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do describe 'Associations' do it { is_expected.to belong_to :project } - it { is_expected.to have_one(:helm_app) } end describe 'Validations' do