Commit cc2c513b authored by Stan Hu's avatar Stan Hu

Merge branch 'uninstall_cluster_apps' into 'master'

Uninstall cluster applications (backend services)

Closes #60665

See merge request gitlab-org/gitlab-ce!27096
parents a2543ee2 3c52ff8c
...@@ -4,6 +4,7 @@ class Clusters::ApplicationsController < Clusters::BaseController ...@@ -4,6 +4,7 @@ class Clusters::ApplicationsController < Clusters::BaseController
before_action :cluster before_action :cluster
before_action :authorize_create_cluster!, only: [:create] before_action :authorize_create_cluster!, only: [:create]
before_action :authorize_update_cluster!, only: [:update] before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
def create def create
request_handler do request_handler do
...@@ -21,6 +22,14 @@ class Clusters::ApplicationsController < Clusters::BaseController ...@@ -21,6 +22,14 @@ class Clusters::ApplicationsController < Clusters::BaseController
end end
end end
def destroy
request_handler do
Clusters::Applications::DestroyService
.new(@cluster, current_user, cluster_application_destroy_params)
.execute(request)
end
end
private private
def request_handler def request_handler
...@@ -40,4 +49,8 @@ class Clusters::ApplicationsController < Clusters::BaseController ...@@ -40,4 +49,8 @@ class Clusters::ApplicationsController < Clusters::BaseController
def cluster_application_params def cluster_application_params
params.permit(:application, :hostname, :email) params.permit(:application, :hostname, :email)
end end
def cluster_application_destroy_params
params.permit(:application)
end
end end
...@@ -24,6 +24,12 @@ module Clusters ...@@ -24,6 +24,12 @@ module Clusters
'stable/cert-manager' 'stable/cert-manager'
end end
# We will implement this in future MRs.
# Need to reverse postinstall step
def allowed_to_uninstall?
false
end
def install_command def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new( Gitlab::Kubernetes::Helm::InstallCommand.new(
name: 'certmanager', name: 'certmanager',
......
...@@ -29,6 +29,13 @@ module Clusters ...@@ -29,6 +29,13 @@ module Clusters
self.status = 'installable' if cluster&.platform_kubernetes_active? self.status = 'installable' if cluster&.platform_kubernetes_active?
end end
# We will implement this in future MRs.
# Basically we need to check all other applications are not installed
# first.
def allowed_to_uninstall?
false
end
def install_command def install_command
Gitlab::Kubernetes::Helm::InitCommand.new( Gitlab::Kubernetes::Helm::InitCommand.new(
name: name, name: name,
......
...@@ -35,6 +35,13 @@ module Clusters ...@@ -35,6 +35,13 @@ module Clusters
'stable/nginx-ingress' 'stable/nginx-ingress'
end end
# We will implement this in future MRs.
# Basically we need to check all dependent applications are not installed
# first.
def allowed_to_uninstall?
false
end
def install_command def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new( Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name, name: name,
......
...@@ -38,6 +38,12 @@ module Clusters ...@@ -38,6 +38,12 @@ module Clusters
content_values.to_yaml content_values.to_yaml
end end
# Will be addressed in future MRs
# We need to investigate and document what will be permenantly deleted.
def allowed_to_uninstall?
false
end
def install_command def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new( Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name, name: name,
......
...@@ -51,6 +51,12 @@ module Clusters ...@@ -51,6 +51,12 @@ module Clusters
{ "domain" => hostname }.to_yaml { "domain" => hostname }.to_yaml
end end
# Handled in a new issue:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/59369
def allowed_to_uninstall?
false
end
def install_command def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new( Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name, name: name,
......
...@@ -16,10 +16,12 @@ module Clusters ...@@ -16,10 +16,12 @@ module Clusters
default_value_for :version, VERSION default_value_for :version, VERSION
after_destroy :disable_prometheus_integration
state_machine :status do state_machine :status do
after_transition any => [:installed] do |application| after_transition any => [:installed] do |application|
application.cluster.projects.each do |project| application.cluster.projects.each do |project|
project.find_or_initialize_service('prometheus').update(active: true) project.find_or_initialize_service('prometheus').update!(active: true)
end end
end end
end end
...@@ -47,6 +49,14 @@ module Clusters ...@@ -47,6 +49,14 @@ module Clusters
) )
end end
def uninstall_command
Gitlab::Kubernetes::Helm::DeleteCommand.new(
name: name,
rbac: cluster.platform_kubernetes_rbac?,
files: files
)
end
def upgrade_command(values) def upgrade_command(values)
::Gitlab::Kubernetes::Helm::InstallCommand.new( ::Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name, name: name,
...@@ -82,6 +92,12 @@ module Clusters ...@@ -82,6 +92,12 @@ module Clusters
private private
def disable_prometheus_integration
cluster.projects.each do |project|
project.prometheus_service&.update!(active: false)
end
end
def kube_client def kube_client
cluster&.kubeclient&.core_client cluster&.kubeclient&.core_client
end end
......
...@@ -29,6 +29,13 @@ module Clusters ...@@ -29,6 +29,13 @@ module Clusters
content_values.to_yaml content_values.to_yaml
end end
# Need to investigate if pipelines run by this runner will stop upon the
# executor pod stopping
# I.e.run a pipeline, and uninstall runner while pipeline is running
def allowed_to_uninstall?
false
end
def install_command def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new( Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name, name: name,
......
...@@ -18,6 +18,16 @@ module Clusters ...@@ -18,6 +18,16 @@ module Clusters
self.status = 'installable' if cluster&.application_helm_available? self.status = 'installable' if cluster&.application_helm_available?
end end
def can_uninstall?
allowed_to_uninstall?
end
# All new applications should uninstall by default
# Override if there's dependencies that needs to be uninstalled first
def allowed_to_uninstall?
true
end
def self.application_name def self.application_name
self.to_s.demodulize.underscore self.to_s.demodulize.underscore
end end
......
...@@ -25,9 +25,11 @@ module Clusters ...@@ -25,9 +25,11 @@ module Clusters
state :updating, value: 4 state :updating, value: 4
state :updated, value: 5 state :updated, value: 5
state :update_errored, value: 6 state :update_errored, value: 6
state :uninstalling, value: 7
state :uninstall_errored, value: 8
event :make_scheduled do event :make_scheduled do
transition [:installable, :errored, :installed, :updated, :update_errored] => :scheduled transition [:installable, :errored, :installed, :updated, :update_errored, :uninstall_errored] => :scheduled
end end
event :make_installing do event :make_installing do
...@@ -40,8 +42,9 @@ module Clusters ...@@ -40,8 +42,9 @@ module Clusters
end end
event :make_errored do event :make_errored do
transition any - [:updating] => :errored transition any - [:updating, :uninstalling] => :errored
transition [:updating] => :update_errored transition [:updating] => :update_errored
transition [:uninstalling] => :uninstall_errored
end end
event :make_updating do event :make_updating do
...@@ -52,6 +55,10 @@ module Clusters ...@@ -52,6 +55,10 @@ module Clusters
transition any => :update_errored transition any => :update_errored
end end
event :make_uninstalling do
transition [:scheduled] => :uninstalling
end
before_transition any => [:scheduled] do |app_status, _| before_transition any => [:scheduled] do |app_status, _|
app_status.status_reason = nil app_status.status_reason = nil
end end
...@@ -65,7 +72,7 @@ module Clusters ...@@ -65,7 +72,7 @@ module Clusters
app_status.status_reason = nil app_status.status_reason = nil
end end
before_transition any => [:update_errored] do |app_status, transition| before_transition any => [:update_errored, :uninstall_errored] do |app_status, transition|
status_reason = transition.args.first status_reason = transition.args.first
app_status.status_reason = status_reason if status_reason app_status.status_reason = status_reason if status_reason
end end
......
...@@ -10,4 +10,5 @@ class ClusterApplicationEntity < Grape::Entity ...@@ -10,4 +10,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) } expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
expose :email, if: -> (e, _) { e.respond_to?(:email) } expose :email, if: -> (e, _) { e.respond_to?(:email) }
expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) } expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) }
expose :can_uninstall?, as: :can_uninstall
end end
...@@ -37,7 +37,7 @@ module Clusters ...@@ -37,7 +37,7 @@ module Clusters
end end
def check_timeout def check_timeout
if timeouted? if timed_out?
begin begin
app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.") app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.")
end end
...@@ -51,8 +51,8 @@ module Clusters ...@@ -51,8 +51,8 @@ module Clusters
install_command.pod_name install_command.pod_name
end end
def timeouted? def timed_out?
Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT Time.now.utc - app.updated_at.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
end end
def remove_installation_pod def remove_installation_pod
......
# frozen_string_literal: true
module Clusters
module Applications
class CheckUninstallProgressService < BaseHelmService
def execute
return unless app.uninstalling?
case installation_phase
when Gitlab::Kubernetes::Pod::SUCCEEDED
on_success
when Gitlab::Kubernetes::Pod::FAILED
on_failed
else
check_timeout
end
rescue Kubeclient::HttpError => e
log_error(e)
app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
end
private
def on_success
app.destroy!
rescue StandardError => e
app.make_errored!(_('Application uninstalled but failed to destroy: %{error_message}') % { error_message: e.message })
ensure
remove_installation_pod
end
def on_failed
app.make_errored!(_('Operation failed. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name })
end
def check_timeout
if timed_out?
app.make_errored!(_('Operation timed out. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name })
else
WaitForUninstallAppWorker.perform_in(WaitForUninstallAppWorker::INTERVAL, app.name, app.id)
end
end
def pod_name
app.uninstall_command.pod_name
end
def timed_out?
Time.now.utc - app.updated_at.utc > WaitForUninstallAppWorker::TIMEOUT
end
def remove_installation_pod
helm_api.delete_pod!(pod_name)
end
def installation_phase
helm_api.status(pod_name)
end
end
end
end
...@@ -10,8 +10,8 @@ module Clusters ...@@ -10,8 +10,8 @@ module Clusters
end end
def builder def builder
cluster.method("application_#{application_name}").call || cluster.public_send(:"application_#{application_name}") || # rubocop:disable GitlabSecurity/PublicSend
cluster.method("build_application_#{application_name}").call cluster.public_send(:"build_application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend
end end
end end
end end
......
# frozen_string_literal: true
module Clusters
module Applications
class DestroyService < ::Clusters::Applications::BaseService
def execute(_request)
instantiate_application.tap do |application|
break unless application.can_uninstall?
application.make_scheduled!
Clusters::Applications::UninstallWorker.perform_async(application.name, application.id)
end
end
private
def builder
cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
# frozen_string_literal: true
module Clusters
module Applications
class UninstallService < BaseHelmService
def execute
return unless app.scheduled?
app.make_uninstalling!
uninstall
end
private
def uninstall
helm_api.uninstall(app.uninstall_command)
Clusters::Applications::WaitForUninstallAppWorker.perform_in(
Clusters::Applications::WaitForUninstallAppWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e
log_error(e)
app.make_errored!("Kubernetes error: #{e.error_code}")
rescue StandardError => e
log_error(e)
app.make_errored!('Failed to uninstall.')
end
end
end
end
...@@ -10,7 +10,7 @@ module Clusters ...@@ -10,7 +10,7 @@ module Clusters
end end
def builder def builder
cluster.method("application_#{application_name}").call cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend
end end
end end
end end
......
...@@ -32,6 +32,8 @@ ...@@ -32,6 +32,8 @@
- gcp_cluster:cluster_wait_for_ingress_ip_address - gcp_cluster:cluster_wait_for_ingress_ip_address
- gcp_cluster:cluster_configure - gcp_cluster:cluster_configure
- gcp_cluster:cluster_project_configure - gcp_cluster:cluster_project_configure
- gcp_cluster:clusters_applications_wait_for_uninstall_app
- gcp_cluster:clusters_applications_uninstall
- github_import_advance_stage - github_import_advance_stage
- github_importer:github_import_import_diff_note - github_importer:github_import_import_diff_note
......
# frozen_string_literal: true
module Clusters
module Applications
class UninstallWorker
include ApplicationWorker
include ClusterQueue
include ClusterApplications
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::UninstallService.new(app).execute
end
end
end
end
end
# frozen_string_literal: true
module Clusters
module Applications
class WaitForUninstallAppWorker
include ApplicationWorker
include ClusterQueue
include ClusterApplications
INTERVAL = 10.seconds
TIMEOUT = 20.minutes
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::CheckUninstallProgressService.new(app).execute
end
end
end
end
end
...@@ -103,6 +103,7 @@ Rails.application.routes.draw do ...@@ -103,6 +103,7 @@ Rails.application.routes.draw do
scope :applications do scope :applications do
post '/:application', to: 'clusters/applications#create', as: :install_applications post '/:application', to: 'clusters/applications#create', as: :install_applications
patch '/:application', to: 'clusters/applications#update', as: :update_applications patch '/:application', to: 'clusters/applications#update', as: :update_applications
delete '/:application', to: 'clusters/applications#destroy', as: :uninstall_applications
end end
get :cluster_status, format: :json get :cluster_status, format: :json
......
...@@ -22,6 +22,13 @@ module Gitlab ...@@ -22,6 +22,13 @@ module Gitlab
alias_method :update, :install alias_method :update, :install
def uninstall(command)
namespace.ensure_exists!
delete_pod!(command.pod_name)
kubeclient.create_pod(command.pod_resource)
end
## ##
# Returns Pod phase # Returns Pod phase
# #
......
...@@ -942,6 +942,9 @@ msgstr "" ...@@ -942,6 +942,9 @@ msgstr ""
msgid "Application settings saved successfully" msgid "Application settings saved successfully"
msgstr "" msgstr ""
msgid "Application uninstalled but failed to destroy: %{error_message}"
msgstr ""
msgid "Application was successfully destroyed." msgid "Application was successfully destroyed."
msgstr "" msgstr ""
...@@ -6274,6 +6277,12 @@ msgstr "" ...@@ -6274,6 +6277,12 @@ msgstr ""
msgid "Opens in a new window" msgid "Opens in a new window"
msgstr "" msgstr ""
msgid "Operation failed. Check pod logs for %{pod_name} for more details."
msgstr ""
msgid "Operation timed out. Check pod logs for %{pod_name} for more details."
msgstr ""
msgid "Operations" msgid "Operations"
msgstr "" msgstr ""
......
...@@ -145,4 +145,66 @@ describe Projects::Clusters::ApplicationsController do ...@@ -145,4 +145,66 @@ describe Projects::Clusters::ApplicationsController do
it_behaves_like 'a secure endpoint' it_behaves_like 'a secure endpoint'
end end
end end
describe 'DELETE destroy' do
subject do
delete :destroy, 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_prometheus, :installed, cluster: cluster) }
let(:application_name) { application.name }
let(:params) { { application: application_name, id: cluster.id } }
let(:worker_class) { Clusters::Applications::UninstallWorker }
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(worker_class).to receive(:perform_async).with(application.name, application.id).once
is_expected.to have_http_status(:no_content)
expect(cluster.application_prometheus).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(worker_class).to receive(:perform_async)
end
it_behaves_like 'a secure endpoint'
end
end
end end
...@@ -6,6 +6,11 @@ FactoryBot.define do ...@@ -6,6 +6,11 @@ FactoryBot.define do
status(-2) status(-2)
end end
trait :errored do
status(-1)
status_reason 'something went wrong'
end
trait :installable do trait :installable do
status 0 status 0
end end
...@@ -30,17 +35,21 @@ FactoryBot.define do ...@@ -30,17 +35,21 @@ FactoryBot.define do
status 5 status 5
end end
trait :errored do trait :update_errored do
status(-1) status(6)
status_reason 'something went wrong' status_reason 'something went wrong'
end end
trait :update_errored do trait :uninstalling do
status(6) status 7
end
trait :uninstall_errored do
status(8)
status_reason 'something went wrong' status_reason 'something went wrong'
end end
trait :timeouted do trait :timed_out do
installing installing
updated_at { ClusterWaitForAppInstallationWorker::TIMEOUT.ago } updated_at { ClusterWaitForAppInstallationWorker::TIMEOUT.ago }
end end
......
...@@ -36,7 +36,8 @@ ...@@ -36,7 +36,8 @@
"external_hostname": { "type": ["string", "null"] }, "external_hostname": { "type": ["string", "null"] },
"hostname": { "type": ["string", "null"] }, "hostname": { "type": ["string", "null"] },
"email": { "type": ["string", "null"] }, "email": { "type": ["string", "null"] },
"update_available": { "type": ["boolean", "null"] } "update_available": { "type": ["boolean", "null"] },
"can_uninstall": { "type": "boolean" }
}, },
"required" : [ "name", "status" ] "required" : [ "name", "status" ]
} }
......
...@@ -33,6 +33,28 @@ describe Gitlab::Kubernetes::Helm::Api do ...@@ -33,6 +33,28 @@ describe Gitlab::Kubernetes::Helm::Api do
end end
end end
describe '#uninstall' do
before do
allow(client).to receive(:create_pod).and_return(nil)
allow(client).to receive(:delete_pod).and_return(nil)
allow(namespace).to receive(:ensure_exists!).once
end
it 'ensures the namespace exists before creating the POD' do
expect(namespace).to receive(:ensure_exists!).once.ordered
expect(client).to receive(:create_pod).once.ordered
subject.uninstall(command)
end
it 'removes an existing pod before installing' do
expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps').once.ordered
expect(client).to receive(:create_pod).once.ordered
subject.uninstall(command)
end
end
describe '#install' do describe '#install' do
before do before do
allow(client).to receive(:create_pod).and_return(nil) allow(client).to receive(:create_pod).and_return(nil)
......
...@@ -10,6 +10,12 @@ describe Clusters::Applications::CertManager do ...@@ -10,6 +10,12 @@ describe Clusters::Applications::CertManager do
include_examples 'cluster application version specs', :clusters_applications_cert_managers include_examples 'cluster application version specs', :clusters_applications_cert_managers
include_examples 'cluster application initial status specs' include_examples 'cluster application initial status specs'
describe '#can_uninstall?' do
subject { cert_manager.can_uninstall? }
it { is_expected.to be_falsey }
end
describe '#install_command' do describe '#install_command' do
let(:cert_email) { 'admin@example.com' } let(:cert_email) { 'admin@example.com' }
......
...@@ -18,6 +18,14 @@ describe Clusters::Applications::Helm do ...@@ -18,6 +18,14 @@ describe Clusters::Applications::Helm do
it { is_expected.to contain_exactly(installed_cluster, updated_cluster) } it { is_expected.to contain_exactly(installed_cluster, updated_cluster) }
end end
describe '#can_uninstall?' do
let(:helm) { create(:clusters_applications_helm) }
subject { helm.can_uninstall? }
it { is_expected.to be_falsey }
end
describe '#issue_client_cert' do describe '#issue_client_cert' do
let(:application) { create(:clusters_applications_helm) } let(:application) { create(:clusters_applications_helm) }
subject { application.issue_client_cert } subject { application.issue_client_cert }
......
...@@ -18,6 +18,12 @@ describe Clusters::Applications::Ingress do ...@@ -18,6 +18,12 @@ describe Clusters::Applications::Ingress do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
end end
describe '#can_uninstall?' do
subject { ingress.can_uninstall? }
it { is_expected.to be_falsey }
end
describe '#make_installed!' do describe '#make_installed!' do
before do before do
application.make_installed! application.make_installed!
......
...@@ -10,6 +10,15 @@ describe Clusters::Applications::Jupyter do ...@@ -10,6 +10,15 @@ describe Clusters::Applications::Jupyter do
it { is_expected.to belong_to(:oauth_application) } it { is_expected.to belong_to(:oauth_application) }
describe '#can_uninstall?' do
let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
subject { jupyter.can_uninstall? }
it { is_expected.to be_falsey }
end
describe '#set_initial_status' do describe '#set_initial_status' do
before do before do
jupyter.set_initial_status jupyter.set_initial_status
......
...@@ -39,6 +39,12 @@ describe Clusters::Applications::Knative do ...@@ -39,6 +39,12 @@ describe Clusters::Applications::Knative do
end end
end end
describe '#can_uninstall?' do
subject { knative.can_uninstall? }
it { is_expected.to be_falsey }
end
describe '#schedule_status_update with external_ip' do describe '#schedule_status_update with external_ip' do
let(:application) { create(:clusters_applications_knative, :installed) } let(:application) { create(:clusters_applications_knative, :installed) }
......
...@@ -11,6 +11,21 @@ describe Clusters::Applications::Prometheus do ...@@ -11,6 +11,21 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application helm specs', :clusters_applications_prometheus include_examples 'cluster application helm specs', :clusters_applications_prometheus
include_examples 'cluster application initial status specs' include_examples 'cluster application initial status specs'
describe 'after_destroy' do
let(:project) { create(:project) }
let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
let!(:prometheus_service) { project.create_prometheus_service(active: true) }
it 'deactivates prometheus_service after destroy' do
expect do
application.destroy!
prometheus_service.reload
end.to change(prometheus_service, :active).from(true).to(false)
end
end
describe 'transition to installed' do describe 'transition to installed' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
...@@ -23,12 +38,20 @@ describe Clusters::Applications::Prometheus do ...@@ -23,12 +38,20 @@ describe Clusters::Applications::Prometheus do
end end
it 'ensures Prometheus service is activated' do it 'ensures Prometheus service is activated' do
expect(prometheus_service).to receive(:update).with(active: true) expect(prometheus_service).to receive(:update!).with(active: true)
subject.make_installed subject.make_installed
end end
end end
describe '#can_uninstall?' do
let(:prometheus) { create(:clusters_applications_prometheus) }
subject { prometheus.can_uninstall? }
it { is_expected.to be_truthy }
end
describe '#prometheus_client' do describe '#prometheus_client' do
context 'cluster is nil' do context 'cluster is nil' do
it 'returns nil' do it 'returns nil' do
...@@ -134,6 +157,34 @@ describe Clusters::Applications::Prometheus do ...@@ -134,6 +157,34 @@ describe Clusters::Applications::Prometheus do
end end
end end
describe '#uninstall_command' do
let(:prometheus) { create(:clusters_applications_prometheus) }
subject { prometheus.uninstall_command }
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) }
it 'has the application name' do
expect(subject.name).to eq('prometheus')
end
it 'has files' do
expect(subject.files).to eq(prometheus.files)
end
it 'is rbac' do
expect(subject).to be_rbac
end
context 'on a non rbac enabled cluster' do
before do
prometheus.cluster.platform_kubernetes.abac!
end
it { is_expected.not_to be_rbac }
end
end
describe '#upgrade_command' do describe '#upgrade_command' do
let(:prometheus) { build(:clusters_applications_prometheus) } let(:prometheus) { build(:clusters_applications_prometheus) }
let(:values) { prometheus.values } let(:values) { prometheus.values }
......
...@@ -13,6 +13,14 @@ describe Clusters::Applications::Runner do ...@@ -13,6 +13,14 @@ describe Clusters::Applications::Runner do
it { is_expected.to belong_to(:runner) } it { is_expected.to belong_to(:runner) }
describe '#can_uninstall?' do
let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
subject { gitlab_runner.can_uninstall? }
it { is_expected.to be_falsey }
end
describe '#install_command' do describe '#install_command' do
let(:kubeclient) { double('kubernetes client') } let(:kubeclient) { double('kubernetes client') }
let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
......
...@@ -21,6 +21,10 @@ describe ClusterApplicationEntity do ...@@ -21,6 +21,10 @@ describe ClusterApplicationEntity do
expect(subject[:status_reason]).to be_nil expect(subject[:status_reason]).to be_nil
end end
it 'has can_uninstall' do
expect(subject[:can_uninstall]).to be_falsey
end
context 'non-helm application' do context 'non-helm application' do
let(:application) { build(:clusters_applications_runner, version: '0.0.0') } let(:application) { build(:clusters_applications_runner, version: '0.0.0') }
......
...@@ -18,7 +18,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do ...@@ -18,7 +18,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end end
context "when phase is #{a_phase}" do context "when phase is #{a_phase}" do
context 'when not timeouted' do context 'when not timed_out' do
it 'reschedule a new check' do it 'reschedule a new check' do
expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
expect(service).not_to receive(:remove_installation_pod) expect(service).not_to receive(:remove_installation_pod)
...@@ -113,7 +113,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do ...@@ -113,7 +113,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end end
context 'when timed out' do context 'when timed out' do
let(:application) { create(:clusters_applications_helm, :timeouted, :updating) } let(:application) { create(:clusters_applications_helm, :timed_out, :updating) }
before do before do
expect(service).to receive(:installation_phase).once.and_return(phase) expect(service).to receive(:installation_phase).once.and_return(phase)
...@@ -174,7 +174,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do ...@@ -174,7 +174,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end end
context 'when timed out' do context 'when timed out' do
let(:application) { create(:clusters_applications_helm, :timeouted) } let(:application) { create(:clusters_applications_helm, :timed_out) }
before do before do
expect(service).to receive(:installation_phase).once.and_return(phase) expect(service).to receive(:installation_phase).once.and_return(phase)
......
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::Applications::CheckUninstallProgressService do
RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
let(:application) { create(:clusters_applications_prometheus, :uninstalling) }
let(:service) { described_class.new(application) }
let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
let(:errors) { nil }
let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker }
before do
allow(service).to receive(:installation_errors).and_return(errors)
allow(service).to receive(:remove_installation_pod)
end
shared_examples 'a not yet terminated installation' do |a_phase|
let(:phase) { a_phase }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
end
context "when phase is #{a_phase}" do
context 'when not timed_out' do
it 'reschedule a new check' do
expect(worker_class).to receive(:perform_in).once
expect(service).not_to receive(:remove_installation_pod)
expect do
service.execute
application.reload
end.not_to change(application, :status)
expect(application.status_reason).to be_nil
end
end
end
end
context 'when application is installing' do
RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
context 'when installation POD succeeded' do
let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
end
it 'removes the installation POD' do
expect(service).to receive(:remove_installation_pod).once
service.execute
end
it 'destroys the application' do
expect(worker_class).not_to receive(:perform_in)
service.execute
expect(application).to be_destroyed
end
context 'an error occurs while destroying' do
before do
expect(application).to receive(:destroy!).once.and_raise("destroy failed")
end
it 'still removes the installation POD' do
expect(service).to receive(:remove_installation_pod).once
service.execute
end
it 'makes the application uninstall_errored' do
service.execute
expect(application).to be_uninstall_errored
expect(application.status_reason).to eq('Application uninstalled but failed to destroy: destroy failed')
end
end
end
context 'when installation POD failed' do
let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
let(:errors) { 'test installation failed' }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
end
it 'make the application errored' do
service.execute
expect(application).to be_uninstall_errored
expect(application.status_reason).to eq('Operation failed. Check pod logs for uninstall-prometheus for more details.')
end
end
context 'when timed out' do
let(:application) { create(:clusters_applications_prometheus, :timed_out, :uninstalling) }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
end
it 'make the application errored' do
expect(worker_class).not_to receive(:perform_in)
service.execute
expect(application).to be_uninstall_errored
expect(application.status_reason).to eq('Operation timed out. Check pod logs for uninstall-prometheus for more details.')
end
end
context 'when installation raises a Kubeclient::HttpError' do
let(:cluster) { create(:cluster, :provided_by_user, :project) }
let(:logger) { service.send(:logger) }
let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) }
before do
application.update!(cluster: cluster)
expect(service).to receive(:installation_phase).and_raise(error)
end
include_examples 'logs kubernetes errors' do
let(:error_name) { 'Kubeclient::HttpError' }
let(:error_message) { 'Unauthorized' }
let(:error_code) { 401 }
end
it 'shows the response code from the error' do
service.execute
expect(application).to be_uninstall_errored
expect(application.status_reason).to eq('Kubernetes error: 401')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::Applications::DestroyService, '#execute' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:user) { create(:user) }
let(:params) { { application: 'prometheus' } }
let(:service) { described_class.new(cluster, user, params) }
let(:test_request) { double }
let(:worker_class) { Clusters::Applications::UninstallWorker }
subject { service.execute(test_request) }
before do
allow(worker_class).to receive(:perform_async)
end
context 'application is not installed' do
it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do
expect(worker_class).not_to receive(:perform_async)
expect { subject }
.to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError }
.and not_change { Clusters::Applications::Prometheus.count }
.and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count }
end
end
context 'application is installed' do
context 'application is schedulable' do
let!(:application) do
create(:clusters_applications_prometheus, :installed, cluster: cluster)
end
it 'makes application scheduled!' do
subject
expect(application.reload).to be_scheduled
end
it 'schedules UninstallWorker' do
expect(worker_class).to receive(:perform_async).with(application.name, application.id)
subject
end
end
context 'application is not schedulable' do
let!(:application) do
create(:clusters_applications_prometheus, :updating, cluster: cluster)
end
it 'raises StateMachines::InvalidTransition' do
expect(worker_class).not_to receive(:perform_async)
expect { subject }
.to raise_exception { StateMachines::InvalidTransition }
.and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::Applications::UninstallService, '#execute' do
let(:application) { create(:clusters_applications_prometheus, :scheduled) }
let(:service) { described_class.new(application) }
let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) }
let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker }
before do
allow(service).to receive(:helm_api).and_return(helm_client)
end
context 'when there are no errors' do
before do
expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand))
allow(worker_class).to receive(:perform_in).and_return(nil)
end
it 'make the application to be uninstalling' do
expect(application.cluster).not_to be_nil
service.execute
expect(application).to be_uninstalling
end
it 'schedule async installation status check' do
expect(worker_class).to receive(:perform_in).once
service.execute
end
end
context 'when k8s cluster communication fails' do
let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
before do
expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).and_raise(error)
end
include_examples 'logs kubernetes errors' do
let(:error_name) { 'Kubeclient::HttpError' }
let(:error_message) { 'system failure' }
let(:error_code) { 500 }
end
it 'make the application errored' do
service.execute
expect(application).to be_uninstall_errored
expect(application.status_reason).to match('Kubernetes error: 500')
end
end
context 'a non kubernetes error happens' do
let(:application) { create(:clusters_applications_prometheus, :scheduled) }
let(:error) { StandardError.new('something bad happened') }
before do
expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).and_raise(error)
end
include_examples 'logs kubernetes errors' do
let(:error_name) { 'StandardError' }
let(:error_message) { 'something bad happened' }
let(:error_code) { nil }
end
it 'make the application errored' do
service.execute
expect(application).to be_uninstall_errored
expect(application.status_reason).to eq('Failed to uninstall.')
end
end
end
...@@ -2,6 +2,14 @@ shared_examples 'cluster application core specs' do |application_name| ...@@ -2,6 +2,14 @@ shared_examples 'cluster application core specs' do |application_name|
it { is_expected.to belong_to(:cluster) } it { is_expected.to belong_to(:cluster) }
it { is_expected.to validate_presence_of(:cluster) } it { is_expected.to validate_presence_of(:cluster) }
describe '#can_uninstall?' do
it 'calls allowed_to_uninstall?' do
expect(subject).to receive(:allowed_to_uninstall?).and_return(true)
expect(subject.can_uninstall?).to be_truthy
end
end
describe '#name' do describe '#name' do
it 'is .application_name' do it 'is .application_name' do
expect(subject.name).to eq(described_class.application_name) expect(subject.name).to eq(described_class.application_name)
......
...@@ -114,6 +114,17 @@ shared_examples 'cluster application status specs' do |application_name| ...@@ -114,6 +114,17 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.status_reason).to eq(reason) expect(subject.status_reason).to eq(reason)
end end
end end
context 'application is uninstalling' do
subject { create(application_name, :uninstalling) }
it 'is uninstall_errored' do
subject.make_errored(reason)
expect(subject).to be_uninstall_errored
expect(subject.status_reason).to eq(reason)
end
end
end end
describe '#make_scheduled' do describe '#make_scheduled' do
...@@ -125,6 +136,16 @@ shared_examples 'cluster application status specs' do |application_name| ...@@ -125,6 +136,16 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject).to be_scheduled expect(subject).to be_scheduled
end end
describe 'when installed' do
subject { create(application_name, :installed) }
it 'is scheduled' do
subject.make_scheduled
expect(subject).to be_scheduled
end
end
describe 'when was errored' do describe 'when was errored' do
subject { create(application_name, :errored) } subject { create(application_name, :errored) }
...@@ -148,6 +169,28 @@ shared_examples 'cluster application status specs' do |application_name| ...@@ -148,6 +169,28 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.status_reason).to be_nil expect(subject.status_reason).to be_nil
end end
end end
describe 'when was uninstall_errored' do
subject { create(application_name, :uninstall_errored) }
it 'clears #status_reason' do
expect(subject.status_reason).not_to be_nil
subject.make_scheduled!
expect(subject.status_reason).to be_nil
end
end
end
describe '#make_uninstalling' do
subject { create(application_name, :scheduled) }
it 'is uninstalling' do
subject.make_uninstalling!
expect(subject).to be_uninstalling
end
end end
end end
...@@ -155,16 +198,18 @@ shared_examples 'cluster application status specs' do |application_name| ...@@ -155,16 +198,18 @@ shared_examples 'cluster application status specs' do |application_name|
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
where(:trait, :available) do where(:trait, :available) do
:not_installable | false :not_installable | false
:installable | false :installable | false
:scheduled | false :scheduled | false
:installing | false :installing | false
:installed | true :installed | true
:updating | false :updating | false
:updated | true :updated | true
:errored | false :errored | false
:update_errored | false :update_errored | false
:timeouted | false :uninstalling | false
:uninstall_errored | false
:timed_out | false
end end
with_them do with_them do
......
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::Applications::WaitForUninstallAppWorker, '#perform' do
let(:app) { create(:clusters_applications_helm) }
let(:app_name) { app.name }
let(:app_id) { app.id }
subject { described_class.new.perform(app_name, app_id) }
context 'app exists' do
let(:service) { instance_double(Clusters::Applications::CheckUninstallProgressService) }
it 'calls the check service' do
expect(Clusters::Applications::CheckUninstallProgressService).to receive(:new).with(app).and_return(service)
expect(service).to receive(:execute).once
subject
end
end
context 'app does not exist' do
let(:app_id) { 0 }
it 'does not call the check service' do
expect(Clusters::Applications::CheckUninstallProgressService).not_to receive(:new)
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment