Commit 4e9cee4d authored by Rémy Coutable's avatar Rémy Coutable Committed by Marin Jankovski

AutomatedCleanup class for stale review apps

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent 5f97be8b
# frozen_string_literal: true
require 'time'
require_relative '../gitlab/popen' unless defined?(Gitlab::Popen)
module Quality
class HelmClient
attr_reader :namespace
Release = Struct.new(:name, :revision, :last_update, :status, :chart, :namespace) do
def revision
@revision ||= self[:revision].to_i
end
def last_update
@last_update ||= Time.parse(self[:last_update])
end
end
def initialize(namespace: ENV['KUBE_NAMESPACE'])
@namespace = namespace
end
def releases(args: [])
command = ['list', %(--namespace "#{namespace}"), *args]
run_command(command)
.stdout
.lines
.select { |line| line.include?(namespace) }
.map { |line| Release.new(*line.split(/\t/).map(&:strip)) }
end
def delete(release_name:)
run_command(['delete', '--purge', release_name])
end
private
def run_command(command)
final_command = ['helm', *command].join(' ')
puts "Running command: `#{final_command}`" # rubocop:disable Rails/Output
Gitlab::Popen.popen_with_detail([final_command])
end
end
end
# frozen_string_literal: true
require_relative '../gitlab/popen' unless defined?(Gitlab::Popen)
module Quality
class KubernetesClient
attr_reader :namespace
def initialize(namespace: ENV['KUBE_NAMESPACE'])
@namespace = namespace
end
def cleanup(release_name:)
command = [%(-n "#{namespace}" get ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa 2>&1)]
command << '|' << %(grep "#{release_name}")
command << '|' << "awk '{print $1}'"
command << '|' << %(xargs kubectl -n "#{namespace}" delete)
command << '||' << 'true'
run_command(command)
end
private
def run_command(command)
final_command = ['kubectl', *command].join(' ')
puts "Running command: `#{final_command}`" # rubocop:disable Rails/Output
Gitlab::Popen.popen_with_detail([final_command])
end
end
end
# frozen_string_literal: true
require 'gitlab'
require_relative File.expand_path('../../lib/quality/helm_client.rb', __dir__)
require_relative File.expand_path('../../lib/quality/kubernetes_client.rb', __dir__)
class AutomatedCleanup
STALE_REVIEW_APP_DAYS_THRESHOLD = 10.freeze
attr_reader :project_path, :gitlab_token, :cleaned_up_releases
def initialize(project_path: ENV['CI_PROJECT_PATH'], gitlab_token: ENV['GITLAB_BOT_REVIEW_APPS_CLEANUP_TOKEN'])
@project_path = project_path
@gitlab_token = gitlab_token
@cleaned_up_releases = []
end
def gitlab
@gitlab ||= begin
Gitlab.configure do |config|
config.endpoint = 'https://gitlab.com/api/v4'
# gitlab-bot's token "GitLab review apps cleanup"
config.private_token = gitlab_token
end
Gitlab
end
end
def helm
@helm ||= Quality::HelmClient.new
end
def kubernetes
@kubernetes ||= Quality::KubernetesClient.new
end
def perform_gitlab_environment_cleanup!(days_for_stop: STALE_REVIEW_APP_DAYS_THRESHOLD, days_for_delete: STALE_REVIEW_APP_DAYS_THRESHOLD)
puts "Checking for review apps not updated in the last #{days_for_stop} days..."
checked_environments = []
gitlab.deployments(project_path, per_page: 50).auto_paginate do |deployment|
next unless deployment.environment.name.start_with?('review/')
next if checked_environments.include?(deployment.environment.slug)
puts
checked_environments << deployment.environment.slug
deployed_at = Time.parse(deployment.created_at)
if deployed_at < threshold_time(days: days_for_delete)
print_release_state(subject: 'Review app', release_name: deployment.environment.slug, release_date: deployment.created_at, action: 'deleting')
gitlab.delete_environment(project_path, deployment.environment.id)
cleaned_up_releases << deployment.environment.slug
elsif deployed_at < threshold_time(days: days_for_stop)
print_release_state(subject: 'Review app', release_name: deployment.environment.slug, release_date: deployment.created_at, action: 'stopping')
gitlab.stop_environment(project_path, deployment.environment.id)
cleaned_up_releases << deployment.environment.slug
else
print_release_state(subject: 'Review app', release_name: deployment.environment.slug, release_date: deployment.created_at, action: 'leaving')
end
end
end
def perform_helm_releases_cleanup!(days: STALE_REVIEW_APP_DAYS_THRESHOLD)
puts "Checking for Helm releases not updated in the last #{days} days..."
helm.releases(args: ['--deployed', '--failed', '--date', '--reverse', '--max 25']).each do |release|
next if cleaned_up_releases.include?(release.name)
if release.last_update < threshold_time(days: days)
print_release_state(subject: 'Release', release_name: release.name, release_date: release.last_update, action: 'cleaning')
helm.delete(release_name: release.name)
kubernetes.cleanup(release_name: release.name)
else
print_release_state(subject: 'Release', release_name: release.name, release_date: release.last_update, action: 'leaving')
end
end
end
def threshold_time(days:)
Time.now - days * 24 * 3600
end
def print_release_state(subject:, release_name:, release_date:, action:)
puts "\n#{subject} '#{release_name}' was last deployed on #{release_date}: #{action} it."
end
end
def timed(task)
start = Time.now
yield(self)
puts "#{task} finished in #{Time.now - start} seconds.\n"
end
automated_cleanup = AutomatedCleanup.new
timed('Review apps cleanup') do
automated_cleanup.perform_gitlab_environment_cleanup!(days_for_stop: 5, days_for_delete: 6)
end
puts
timed('Helm releases cleanup') do
automated_cleanup.perform_helm_releases_cleanup!(days: 7)
end
exit(0)
......@@ -83,6 +83,8 @@ function deploy() {
gitlab_migrations_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-rails-ce"
gitlab_sidekiq_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-sidekiq-ce"
gitlab_unicorn_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-unicorn-ce"
gitlab_gitaly_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitaly"
gitlab_shell_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-shell"
if [[ "$CI_PROJECT_NAME" == "gitlab-ee" ]]; then
gitlab_migrations_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-rails-ee"
......@@ -117,6 +119,40 @@ function deploy() {
helm repo add gitlab https://charts.gitlab.io/
helm dep update .
cat << EOF
Deploying:
helm upgrade --install \
--wait \
--timeout 600 \
--set releaseOverride="$CI_ENVIRONMENT_SLUG" \
--set global.hosts.hostSuffix="$HOST_SUFFIX" \
--set global.hosts.domain="$REVIEW_APPS_DOMAIN" \
--set global.hosts.externalIP="$REVIEW_APPS_DOMAIN_IP" \
--set certmanager.install=false \
--set global.ingress.configureCertmanager=false \
--set global.ingress.tls.secretName=tls-cert \
--set gitlab.unicorn.resources.requests.cpu=200m \
--set gitlab.sidekiq.resources.requests.cpu=100m \
--set gitlab.gitlab-shell.resources.requests.cpu=100m \
--set redis.resources.requests.cpu=100m \
--set minio.resources.requests.cpu=100m \
--set gitlab.migrations.image.repository="$gitlab_migrations_image_repository" \
--set gitlab.migrations.image.tag="$CI_COMMIT_REF_NAME" \
--set gitlab.sidekiq.image.repository="$gitlab_sidekiq_image_repository" \
--set gitlab.sidekiq.image.tag="$CI_COMMIT_REF_NAME" \
--set gitlab.unicorn.image.repository="$gitlab_unicorn_image_repository" \
--set gitlab.unicorn.image.tag="$CI_COMMIT_REF_NAME" \
--set gitlab.gitaly.image.repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitaly" \
--set gitlab.gitaly.image.tag="v$GITALY_VERSION" \
--set gitlab.gitlab-shell.image.repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-shell" \
--set gitlab.gitlab-shell.image.tag="v$GITLAB_SHELL_VERSION" \
--namespace="$KUBE_NAMESPACE" \
--version="$CI_PIPELINE_ID-$CI_JOB_ID" \
"$name" \
.
EOF
helm upgrade --install \
--wait \
--timeout 600 \
......@@ -155,10 +191,13 @@ function delete() {
if [[ "$track" != "stable" ]]; then
name="$name-$track"
fi
echo "Deleting release '$name'..."
helm delete --purge "$name" || true
}
function cleanup() {
echo "Cleaning up $CI_ENVIRONMENT_SLUG..."
kubectl -n "$KUBE_NAMESPACE" get ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa 2>&1 \
| grep "$CI_ENVIRONMENT_SLUG" \
| awk '{print $1}' \
......
# frozen_string_literal: true
RSpec.describe Quality::HelmClient do
let(:namespace) { 'review-apps-ee' }
let(:release_name) { 'my-release' }
let(:raw_helm_list_result) do
<<~OUTPUT
NAME REVISION UPDATED STATUS CHART NAMESPACE
review-improve-re-2dsd9d 1 Tue Jul 31 15:53:17 2018 FAILED gitlab-0.3.4 #{namespace}
review-11-1-stabl-3r2fso 1 Mon Jul 30 22:44:14 2018 FAILED gitlab-0.3.3 #{namespace}
review-49375-css-fk664j 1 Thu Jul 19 11:01:30 2018 FAILED gitlab-0.2.4 #{namespace}
OUTPUT
end
subject { described_class.new(namespace: namespace) }
describe '#releases' do
it 'calls helm list with default arguments' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with([%(helm list --namespace "#{namespace}")])
.and_return(Gitlab::Popen::Result.new([], ''))
subject.releases
end
it 'calls helm list with given arguments' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with([%(helm list --namespace "#{namespace}" --deployed)])
.and_return(Gitlab::Popen::Result.new([], ''))
subject.releases(args: ['--deployed'])
end
it 'returns a list of Release objects' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with([%(helm list --namespace "#{namespace}" --deployed)])
.and_return(Gitlab::Popen::Result.new([], raw_helm_list_result))
releases = subject.releases(args: ['--deployed'])
expect(releases.size).to eq(3)
expect(releases[0].name).to eq('review-improve-re-2dsd9d')
expect(releases[0].revision).to eq(1)
expect(releases[0].last_update).to eq(Time.parse('Tue Jul 31 15:53:17 2018'))
expect(releases[0].status).to eq('FAILED')
expect(releases[0].chart).to eq('gitlab-0.3.4')
expect(releases[0].namespace).to eq(namespace)
end
end
describe '#delete' do
it 'calls helm delete with default arguments' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with(["helm delete --purge #{release_name}"])
.and_return(Gitlab::Popen::Result.new([], '', '', 0))
expect(subject.delete(release_name: release_name).status).to eq(0)
end
end
end
# frozen_string_literal: true
RSpec.describe Quality::KubernetesClient do
let(:namespace) { 'review-apps-ee' }
let(:release_name) { 'my-release' }
subject { described_class.new(namespace: namespace) }
describe '#cleanup' do
it 'calls helm list with default arguments' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with([
%(kubectl -n "#{namespace}" get ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa 2>&1) +
%( | grep "#{release_name}") +
" | awk '{print $1}'" +
%( | xargs kubectl -n "#{namespace}" delete) +
' || true'
])
.and_return(Gitlab::Popen::Result.new([], ''))
subject.cleanup(release_name: release_name)
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