GitLab ist jetzt auf dem neuesten Stand! Die jüngsten Probleme wurden behoben. Wir entschuldigen uns für die dadurch verursachten Unannehmlichkeiten.

Commit f8234d9a authored by João Cunha's avatar João Cunha Committed by jerasmus

Creates Clusterss::ApplciationsController update endpoint

- Creates new route
- Creates new controller action
- Creates call stack:
  Clusterss::ApplciationsController calls -->
  Clusters::Applications::UpdateService calls -->
  Clusters::Applications::ScheduleUpdateService calls -->
  ClusterUpdateAppWorker calls -->
  Clusters::Applications::PatchService -->
  ClusterWaitForAppInstallationWorker

DRY req params

Adds gcp_cluster:cluster_update_app queue

Schedule_update_service is uneeded

Extract common logic to a parent class (UpdateService will need it)

Introduce new UpdateService

Fix rescue class namespace

Fix RuboCop offenses

Adds BaseService for create and update services

Remove request_handler code duplication

Fixes update command

Move update_command to ApplicationCore so all apps can use it

Adds tests for Knative update_command

Adds specs for PatchService

Raise error if update receives an unistalled app

Adds update_service spec

Fix RuboCop offense

Use subject in favor of go

Adds update endpoint specs for project namespace

Adds update endpoint specs for group namespace
parent cf1b85dd
......@@ -3,26 +3,41 @@
class Clusters::ApplicationsController < Clusters::BaseController
before_action :cluster
before_action :authorize_create_cluster!, only: [:create]
before_action :authorize_update_cluster!, only: [:update]
def create
Clusters::Applications::CreateService
.new(@cluster, current_user, create_cluster_application_params)
.execute(request)
request_handler do
Clusters::Applications::CreateService
.new(@cluster, current_user, cluster_application_params)
.execute(request)
end
end
def update
request_handler do
Clusters::Applications::UpdateService
.new(@cluster, current_user, cluster_application_params)
.execute(request)
end
end
private
def request_handler
yield
head :no_content
rescue Clusters::Applications::CreateService::InvalidApplicationError
rescue Clusters::Applications::BaseService::InvalidApplicationError
render_404
rescue StandardError
head :bad_request
end
private
def cluster
@cluster ||= clusterable.clusters.find(params[:id]) || render_404
end
def create_cluster_application_params
def cluster_application_params
params.permit(:application, :hostname, :email)
end
end
......@@ -30,6 +30,12 @@ module Clusters
# Override if you need extra data synchronized
# from K8s after installation
end
def update_command
command = install_command
command.version = version
command
end
end
end
end
......
......@@ -46,6 +46,10 @@ module Clusters
@install_command ||= app.install_command
end
def update_command
@update_command ||= app.update_command
end
def upgrade_command(new_values = "")
app.upgrade_command(new_values)
end
......
# frozen_string_literal: true
module Clusters
module Applications
class BaseService
InvalidApplicationError = Class.new(StandardError)
attr_reader :cluster, :current_user, :params
def initialize(cluster, user, params = {})
@cluster = cluster
@current_user = user
@params = params.dup
end
def execute(request)
instantiate_application.tap do |application|
if application.has_attribute?(:hostname)
application.hostname = params[:hostname]
end
if application.has_attribute?(:email)
application.email = params[:email]
end
if application.respond_to?(:oauth_application)
application.oauth_application = create_oauth_application(application, request)
end
worker = worker_class(application)
application.make_scheduled!
worker.perform_async(application.name, application.id)
end
end
protected
def worker_class(application)
raise NotImplementedError
end
def builders
raise NotImplementedError
end
def project_builders
raise NotImplementedError
end
def instantiate_application
builder.call(@cluster) || raise(InvalidApplicationError, "invalid application: #{application_name}")
end
def builder
builders[application_name] || raise(InvalidApplicationError, "invalid application: #{application_name}")
end
def application_name
params[:application]
end
def create_oauth_application(application, request)
oauth_application_params = {
name: params[:application],
redirect_uri: application.callback_url,
scopes: 'api read_user openid',
owner: current_user
}
::Applications::CreateService.new(current_user, oauth_application_params).execute(request)
end
end
end
end
......@@ -2,47 +2,11 @@
module Clusters
module Applications
class CreateService
InvalidApplicationError = Class.new(StandardError)
attr_reader :cluster, :current_user, :params
def initialize(cluster, user, params = {})
@cluster = cluster
@current_user = user
@params = params.dup
end
def execute(request)
create_application.tap do |application|
if application.has_attribute?(:hostname)
application.hostname = params[:hostname]
end
if application.has_attribute?(:email)
application.email = params[:email]
end
if application.respond_to?(:oauth_application)
application.oauth_application = create_oauth_application(application, request)
end
worker = application.updateable? ? ClusterUpgradeAppWorker : ClusterInstallAppWorker
application.make_scheduled!
worker.perform_async(application.name, application.id)
end
end
class CreateService < Clusters::Applications::BaseService
private
def create_application
builder.call(@cluster)
end
def builder
builders[application_name] || raise(InvalidApplicationError, "invalid application: #{application_name}")
def worker_class(application)
application.updateable? ? ClusterUpgradeAppWorker : ClusterInstallAppWorker
end
def builders
......@@ -65,21 +29,6 @@ module Clusters
"knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative }
}
end
def application_name
params[:application]
end
def create_oauth_application(application, request)
oauth_application_params = {
name: params[:application],
redirect_uri: application.callback_url,
scopes: 'api read_user openid',
owner: current_user
}
::Applications::CreateService.new(current_user, oauth_application_params).execute(request)
end
end
end
end
# frozen_string_literal: true
module Clusters
module Applications
class PatchService < BaseHelmService
def execute
return unless app.scheduled?
begin
app.make_updating!
helm_api.update(update_command)
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e
log_error(e)
app.make_update_errored!("Kubernetes error: #{e.error_code}")
rescue StandardError => e
log_error(e)
app.make_update_errored!("Can't start update process.")
end
end
end
end
end
# frozen_string_literal: true
module Clusters
module Applications
class UpdateService < Clusters::Applications::BaseService
private
def worker_class(application)
ClusterUpdateAppWorker
end
def builders
{
"helm" => -> (cluster) { cluster.application_helm },
"ingress" => -> (cluster) { cluster.application_ingress },
"cert_manager" => -> (cluster) { cluster.application_cert_manager }
}.tap do |hash|
hash.merge!(project_builders) if cluster.project_type?
end
end
# These applications will need extra configuration to enable them to work
# with groups of projects
def project_builders
{
"prometheus" => -> (cluster) { cluster.application_prometheus },
"runner" => -> (cluster) { cluster.application_runner },
"jupyter" => -> (cluster) { cluster.application_jupyter },
"knative" => -> (cluster) { cluster.application_knative }
}
end
end
end
end
......@@ -23,6 +23,7 @@
- cronjob:prune_web_hook_logs
- gcp_cluster:cluster_install_app
- gcp_cluster:cluster_update_app
- gcp_cluster:cluster_upgrade_app
- gcp_cluster:cluster_provision
- gcp_cluster:cluster_wait_for_app_installation
......
# frozen_string_literal: true
class ClusterUpdateAppWorker
include ApplicationWorker
include ClusterQueue
include ClusterApplications
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::PatchService.new(app).execute
end
end
end
......@@ -101,6 +101,7 @@ Rails.application.routes.draw do
member do
scope :applications do
post '/:application', to: 'clusters/applications#create', as: :install_applications
patch '/:application', to: 'clusters/applications#update', as: :update_applications
end
get :cluster_status, format: :json
......
......@@ -7,7 +7,8 @@ module Gitlab
include BaseCommand
include ClientCommand
attr_reader :name, :files, :chart, :version, :repository, :preinstall, :postinstall
attr_reader :name, :files, :chart, :repository, :preinstall, :postinstall
attr_accessor :version
def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil, preinstall: nil, postinstall: nil)
@name = name
......
......@@ -9,9 +9,25 @@ describe Groups::Clusters::ApplicationsController do
Clusters::Cluster::APPLICATIONS[application]
end
shared_examples 'a secure endpoint' do
it { expect { subject }.to be_allowed_for(:admin) }
it { expect { subject }.to be_allowed_for(:owner).of(group) }
it { expect { subject }.to be_allowed_for(:maintainer).of(group) }
it { expect { subject }.to be_denied_for(:developer).of(group) }
it { expect { subject }.to be_denied_for(:reporter).of(group) }
it { expect { subject }.to be_denied_for(:guest).of(group) }
it { expect { subject }.to be_denied_for(:user) }
it { expect { subject }.to be_denied_for(:external) }
end
let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
let(:group) { cluster.group }
describe 'POST create' do
let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
let(:group) { cluster.group }
subject do
post :create, params: params.merge(group_id: group)
end
let(:application) { 'helm' }
let(:params) { { application: application, id: cluster.id } }
......@@ -26,7 +42,7 @@ describe Groups::Clusters::ApplicationsController do
it 'schedule an application installation' do
expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
expect { go }.to change { current_application.count }
expect { subject }.to change { current_application.count }
expect(response).to have_http_status(:no_content)
expect(cluster.application_helm).to be_scheduled
end
......@@ -37,7 +53,7 @@ describe Groups::Clusters::ApplicationsController do
end
it 'return 404' do
expect { go }.not_to change { current_application.count }
expect { subject }.not_to change { current_application.count }
expect(response).to have_http_status(:not_found)
end
end
......@@ -46,9 +62,7 @@ describe Groups::Clusters::ApplicationsController do
let(:application) { 'unkwnown-app' }
it 'return 404' do
go
expect(response).to have_http_status(:not_found)
is_expected.to have_http_status(:not_found)
end
end
......@@ -58,9 +72,7 @@ describe Groups::Clusters::ApplicationsController do
end
it 'returns 400' do
go
expect(response).to have_http_status(:bad_request)
is_expected.to have_http_status(:bad_request)
end
end
end
......@@ -70,18 +82,66 @@ describe Groups::Clusters::ApplicationsController do
allow(ClusterInstallAppWorker).to receive(:perform_async)
end
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
it_behaves_like 'a secure endpoint'
end
end
def go
post :create, params: params.merge(group_id: group)
describe 'PATCH update' do
subject do
patch :update, params: params.merge(group_id: group)
end
let!(:application) { create(:clusters_applications_cert_managers, :installed, cluster: cluster) }
let(:application_name) { application.name }
let(:params) { { application: application_name, id: cluster.id, email: "new-email@example.com" } }
describe 'functionality' do
let(:user) { create(:user) }
before do
group.add_maintainer(user)
sign_in(user)
end
context "when cluster and app exists" do
it "schedules an application update" do
expect(ClusterUpdateAppWorker).to receive(:perform_async).with(application.name, anything).once
is_expected.to have_http_status(:no_content)
expect(cluster.application_cert_manager).to be_scheduled
end
end
context 'when cluster do not exists' do
before do
cluster.destroy!
end
it { is_expected.to have_http_status(:not_found) }
end
context 'when application is unknown' do
let(:application_name) { 'unkwnown-app' }
it { is_expected.to have_http_status(:not_found) }
end
context 'when application is already scheduled' do
before do
application.make_scheduled!
end
it { is_expected.to have_http_status(:bad_request) }
end
end
describe 'security' do
before do
allow(ClusterUpdateAppWorker).to receive(:perform_async)
end
it_behaves_like 'a secure endpoint'
end
end
end
......@@ -9,7 +9,22 @@ describe Projects::Clusters::ApplicationsController do
Clusters::Cluster::APPLICATIONS[application]
end
shared_examples 'a secure endpoint' do
it { expect { subject }.to be_allowed_for(:admin) }
it { expect { subject }.to be_allowed_for(:owner).of(project) }
it { expect { subject }.to be_allowed_for(:maintainer).of(project) }
it { expect { subject }.to be_denied_for(:developer).of(project) }
it { expect { subject }.to be_denied_for(:reporter).of(project) }
it { expect { subject }.to be_denied_for(:guest).of(project) }
it { expect { subject }.to be_denied_for(:user) }
it { expect { subject }.to be_denied_for(:external) }
end
describe 'POST create' do
subject do
post :create, params: params.merge(namespace_id: project.namespace, project_id: project)
end
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
let(:application) { 'helm' }
......@@ -26,7 +41,7 @@ describe Projects::Clusters::ApplicationsController do
it 'schedule an application installation' do
expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
expect { go }.to change { current_application.count }
expect { subject }.to change { current_application.count }
expect(response).to have_http_status(:no_content)
expect(cluster.application_helm).to be_scheduled
end
......@@ -37,7 +52,7 @@ describe Projects::Clusters::ApplicationsController do
end
it 'return 404' do
expect { go }.not_to change { current_application.count }
expect { subject }.not_to change { current_application.count }
expect(response).to have_http_status(:not_found)
end
end
......@@ -46,9 +61,7 @@ describe Projects::Clusters::ApplicationsController do
let(:application) { 'unkwnown-app' }
it 'return 404' do
go
expect(response).to have_http_status(:not_found)
is_expected.to have_http_status(:not_found)
end
end
......@@ -58,9 +71,7 @@ describe Projects::Clusters::ApplicationsController do
end
it 'returns 400' do
go
expect(response).to have_http_status(:bad_request)
is_expected.to have_http_status(:bad_request)
end
end
end
......@@ -70,18 +81,68 @@ describe Projects::Clusters::ApplicationsController do
allow(ClusterInstallAppWorker).to receive(:perform_async)
end
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(:maintainer).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) }
it_behaves_like 'a secure endpoint'
end
end
def go
post :create, params: params.merge(namespace_id: project.namespace, project_id: project)
describe 'PATCH update' do
subject do
patch :update, params: params.merge(namespace_id: project.namespace, project_id: project)
end
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
let!(:application) { create(:clusters_applications_knative, :installed, cluster: cluster) }
let(:application_name) { application.name }
let(:params) { { application: application_name, id: cluster.id, hostname: "new.example.com" } }
describe 'functionality' do
let(:user) { create(:user) }
before do
project.add_maintainer(user)
sign_in(user)
end
context "when cluster and app exists" do
it "schedules an application update" do
expect(ClusterUpdateAppWorker).to receive(:perform_async).with(application.name, anything).once
is_expected.to have_http_status(:no_content)
expect(cluster.application_knative).to be_scheduled
end
end
context 'when cluster do not exists' do
before do
cluster.destroy!
end
it { is_expected.to have_http_status(:not_found) }
end
context 'when application is unknown' do
let(:application_name) { 'unkwnown-app' }
it { is_expected.to have_http_status(:not_found) }
end
context 'when application is already scheduled' do
before do
application.make_scheduled!
end
it { is_expected.to have_http_status(:bad_request) }
end
end
describe 'security' do
before do
allow(ClusterUpdateAppWorker).to receive(:perform_async)
end
it_behaves_like 'a secure endpoint'
end
end
end
......@@ -66,9 +66,7 @@ describe Clusters::Applications::Knative do
end