debian-mirror-gitlab/scripts/review_apps/automated_cleanup.rb

332 lines
12 KiB
Ruby
Raw Normal View History

2022-11-25 23:54:43 +05:30
#!/usr/bin/env ruby
2018-12-05 23:21:45 +05:30
# frozen_string_literal: true
2022-11-25 23:54:43 +05:30
require 'optparse'
2018-12-05 23:21:45 +05:30
require 'gitlab'
2020-07-28 23:09:34 +05:30
require_relative File.expand_path('../../tooling/lib/tooling/helm3_client.rb', __dir__)
require_relative File.expand_path('../../tooling/lib/tooling/kubernetes_client.rb', __dir__)
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
module ReviewApps
class AutomatedCleanup
DEPLOYMENTS_PER_PAGE = 100
ENVIRONMENT_PREFIX = {
review_app: 'review/',
docs_review_app: 'review-docs/'
}.freeze
IGNORED_HELM_ERRORS = [
'transport is closing',
'error upgrading connection',
'not found'
].freeze
IGNORED_KUBERNETES_ERRORS = [
'NotFound'
].freeze
2023-03-04 22:38:38 +05:30
ENVIRONMENTS_NOT_FOUND_THRESHOLD = 3
2022-11-25 23:54:43 +05:30
# $GITLAB_PROJECT_REVIEW_APP_CLEANUP_API_TOKEN => `Automated Review App Cleanup` project token
def initialize(
project_path: ENV['CI_PROJECT_PATH'],
gitlab_token: ENV['GITLAB_PROJECT_REVIEW_APP_CLEANUP_API_TOKEN'],
api_endpoint: ENV['CI_API_V4_URL'],
options: {}
)
2023-03-04 22:38:38 +05:30
@project_path = project_path
@gitlab_token = gitlab_token
@api_endpoint = api_endpoint
@dry_run = options[:dry_run]
@environments_not_found_count = 0
2022-11-25 23:54:43 +05:30
end
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
def gitlab
@gitlab ||= begin
Gitlab.configure do |config|
config.endpoint = api_endpoint
# gitlab-bot's token "GitLab review apps cleanup"
config.private_token = gitlab_token
end
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
Gitlab
2018-12-05 23:21:45 +05:30
end
end
2022-11-25 23:54:43 +05:30
def review_apps_namespace
'review-apps'
end
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
def helm
2023-04-23 21:23:45 +05:30
@helm ||= Tooling::Helm3Client.new
2022-11-25 23:54:43 +05:30
end
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
def kubernetes
@kubernetes ||= Tooling::KubernetesClient.new(namespace: review_apps_namespace)
end
2018-12-05 23:21:45 +05:30
2023-04-23 21:23:45 +05:30
def perform_gitlab_environment_cleanup!(days_for_delete:)
2023-05-27 22:25:52 +05:30
puts "Dry-run mode." if dry_run
2023-04-23 21:23:45 +05:30
puts "Checking for Review Apps not updated in the last #{days_for_delete} days..."
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
checked_environments = []
delete_threshold = threshold_time(days: days_for_delete)
deployments_look_back_threshold = threshold_time(days: days_for_delete * 5)
2019-12-26 22:10:19 +05:30
2022-11-25 23:54:43 +05:30
releases_to_delete = []
2019-12-26 22:10:19 +05:30
2022-11-25 23:54:43 +05:30
# Delete environments via deployments
gitlab.deployments(project_path, per_page: DEPLOYMENTS_PER_PAGE, sort: 'desc').auto_paginate do |deployment|
2023-04-23 21:23:45 +05:30
last_deploy = deployment.created_at
deployed_at = Time.parse(last_deploy)
break if deployed_at < deployments_look_back_threshold
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
environment = deployment.environment
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
next unless environment
next unless environment.name.start_with?(ENVIRONMENT_PREFIX[:review_app])
next if checked_environments.include?(environment.slug)
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
if deployed_at < delete_threshold
deleted_environment = delete_environment(environment, deployment)
2023-04-23 21:23:45 +05:30
2022-11-25 23:54:43 +05:30
if deleted_environment
2023-04-23 21:23:45 +05:30
release = Tooling::Helm3Client::Release.new(name: environment.slug, namespace: environment.slug, revision: 1)
2022-11-25 23:54:43 +05:30
releases_to_delete << release
end
2020-04-08 14:13:33 +05:30
end
2022-11-25 23:54:43 +05:30
checked_environments << environment.slug
2018-12-05 23:21:45 +05:30
end
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
delete_stopped_environments(environment_type: :review_app, checked_environments: checked_environments, last_updated_threshold: delete_threshold) do |environment|
2023-04-23 21:23:45 +05:30
releases_to_delete << Tooling::Helm3Client::Release.new(name: environment.slug, namespace: environment.slug, revision: 1, updated: environment.updated_at)
2022-11-25 23:54:43 +05:30
end
2019-12-26 22:10:19 +05:30
2022-11-25 23:54:43 +05:30
delete_helm_releases(releases_to_delete)
2021-11-18 22:05:49 +05:30
end
2022-11-25 23:54:43 +05:30
def perform_gitlab_docs_environment_cleanup!(days_for_stop:, days_for_delete:)
2023-05-27 22:25:52 +05:30
puts "Dry-run mode." if dry_run
2022-11-25 23:54:43 +05:30
puts "Checking for Docs Review Apps not updated in the last #{days_for_stop} days..."
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
checked_environments = []
stop_threshold = threshold_time(days: days_for_stop)
delete_threshold = threshold_time(days: days_for_delete)
2023-04-23 21:23:45 +05:30
deployments_look_back_threshold = threshold_time(days: days_for_delete * 5)
2021-11-11 11:23:49 +05:30
2022-11-25 23:54:43 +05:30
# Delete environments via deployments
gitlab.deployments(project_path, per_page: DEPLOYMENTS_PER_PAGE, sort: 'desc').auto_paginate do |deployment|
2023-04-23 21:23:45 +05:30
last_deploy = deployment.created_at
deployed_at = Time.parse(last_deploy)
break if deployed_at < deployments_look_back_threshold
2022-11-25 23:54:43 +05:30
environment = deployment.environment
2021-11-11 11:23:49 +05:30
2022-11-25 23:54:43 +05:30
next unless environment
next unless environment.name.start_with?(ENVIRONMENT_PREFIX[:docs_review_app])
next if checked_environments.include?(environment.slug)
2021-11-11 11:23:49 +05:30
2022-11-25 23:54:43 +05:30
if deployed_at < stop_threshold
environment_state = fetch_environment(environment)&.state
stop_environment(environment, deployment) if environment_state && environment_state != 'stopped'
end
2021-11-11 11:23:49 +05:30
2022-11-25 23:54:43 +05:30
delete_environment(environment, deployment) if deployed_at < delete_threshold
2021-11-11 11:23:49 +05:30
2022-11-25 23:54:43 +05:30
checked_environments << environment.slug
end
2021-11-11 11:23:49 +05:30
2022-11-25 23:54:43 +05:30
delete_stopped_environments(environment_type: :docs_review_app, checked_environments: checked_environments, last_updated_threshold: delete_threshold)
2021-11-11 11:23:49 +05:30
end
2021-11-18 22:05:49 +05:30
2022-11-25 23:54:43 +05:30
def perform_helm_releases_cleanup!(days:)
2023-05-27 22:25:52 +05:30
puts "Dry-run mode." if dry_run
2022-11-25 23:54:43 +05:30
puts "Checking for Helm releases that are failed or not updated in the last #{days} days..."
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
threshold = threshold_time(days: days)
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
releases_to_delete = []
2019-12-26 22:10:19 +05:30
2022-11-25 23:54:43 +05:30
helm_releases.each do |release|
# Prevents deleting `dns-gitlab-review-app` releases or other unrelated releases
2023-04-23 21:23:45 +05:30
next unless Tooling::KubernetesClient::K8S_ALLOWED_NAMESPACES_REGEX.match?(release.namespace)
2022-11-25 23:54:43 +05:30
next unless release.name.start_with?('review-')
2019-12-26 22:10:19 +05:30
2022-11-25 23:54:43 +05:30
if release.status == 'failed' || release.last_update < threshold
releases_to_delete << release
else
print_release_state(subject: 'Release', release_name: release.name, release_date: release.last_update, action: 'leaving')
end
2018-12-05 23:21:45 +05:30
end
2022-11-25 23:54:43 +05:30
delete_helm_releases(releases_to_delete)
2018-12-05 23:21:45 +05:30
end
2019-12-26 22:10:19 +05:30
2022-11-25 23:54:43 +05:30
def perform_stale_namespace_cleanup!(days:)
2023-05-27 22:25:52 +05:30
puts "Dry-run mode." if dry_run
2022-11-25 23:54:43 +05:30
kubernetes_client = Tooling::KubernetesClient.new(namespace: nil)
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
kubernetes_client.cleanup_review_app_namespaces(created_before: threshold_time(days: days), wait: false) unless dry_run
end
2021-09-30 23:02:18 +05:30
2022-11-25 23:54:43 +05:30
def perform_stale_pvc_cleanup!(days:)
2023-05-27 22:25:52 +05:30
puts "Dry-run mode." if dry_run
2022-11-25 23:54:43 +05:30
kubernetes.cleanup_by_created_at(resource_type: 'pvc', created_before: threshold_time(days: days), wait: false) unless dry_run
end
2021-09-30 23:02:18 +05:30
2022-11-25 23:54:43 +05:30
private
2021-03-08 18:12:59 +05:30
2023-03-04 22:38:38 +05:30
attr_reader :api_endpoint, :dry_run, :gitlab_token, :project_path
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
def fetch_environment(environment)
gitlab.environment(project_path, environment.id)
rescue Errno::ETIMEDOUT => ex
puts "Failed to fetch '#{environment.name}' / '#{environment.slug}' (##{environment.id}):\n#{ex.message}"
nil
end
2020-04-08 14:13:33 +05:30
2022-11-25 23:54:43 +05:30
def delete_environment(environment, deployment = nil)
release_date = deployment ? deployment.created_at : environment.updated_at
print_release_state(subject: 'Review app', release_name: environment.slug, release_date: release_date, action: 'deleting')
gitlab.delete_environment(project_path, environment.id) unless dry_run
2020-01-01 13:55:28 +05:30
2023-03-04 22:38:38 +05:30
rescue Gitlab::Error::NotFound
puts "Review app '#{environment.name}' / '#{environment.slug}' (##{environment.id}) was not found: ignoring it"
@environments_not_found_count += 1
if @environments_not_found_count >= ENVIRONMENTS_NOT_FOUND_THRESHOLD
raise "At least #{ENVIRONMENTS_NOT_FOUND_THRESHOLD} environments were missing when we tried to delete them. Please investigate"
end
2022-11-25 23:54:43 +05:30
rescue Gitlab::Error::Forbidden
puts "Review app '#{environment.name}' / '#{environment.slug}' (##{environment.id}) is forbidden: skipping it"
2023-01-13 00:05:48 +05:30
rescue Gitlab::Error::InternalServerError
2023-03-04 22:38:38 +05:30
puts "Review app '#{environment.name}' / '#{environment.slug}' (##{environment.id}) 500 error: ignoring it"
2022-11-25 23:54:43 +05:30
end
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
def stop_environment(environment, deployment)
print_release_state(subject: 'Review app', release_name: environment.slug, release_date: deployment.created_at, action: 'stopping')
gitlab.stop_environment(project_path, environment.id) unless dry_run
2020-01-01 13:55:28 +05:30
2022-11-25 23:54:43 +05:30
rescue Gitlab::Error::Forbidden
puts "Review app '#{environment.name}' / '#{environment.slug}' (##{environment.id}) is forbidden: skipping it"
end
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
def delete_stopped_environments(environment_type:, checked_environments:, last_updated_threshold:)
gitlab.environments(project_path, per_page: DEPLOYMENTS_PER_PAGE, sort: 'desc', states: 'stopped', search: ENVIRONMENT_PREFIX[environment_type]).auto_paginate do |environment|
next if skip_environment?(environment: environment, checked_environments: checked_environments, last_updated_threshold: last_updated_threshold, environment_type: environment_type)
2021-11-18 22:05:49 +05:30
2023-03-17 16:20:25 +05:30
yield environment if delete_environment(environment) && block_given?
2021-11-18 22:05:49 +05:30
2022-11-25 23:54:43 +05:30
checked_environments << environment.slug
end
2021-11-18 22:05:49 +05:30
end
2022-11-25 23:54:43 +05:30
def skip_environment?(environment:, checked_environments:, last_updated_threshold:, environment_type:)
return true unless environment.name.start_with?(ENVIRONMENT_PREFIX[environment_type])
return true if checked_environments.include?(environment.slug)
return true if Time.parse(environment.updated_at) > last_updated_threshold
2021-11-18 22:05:49 +05:30
2022-11-25 23:54:43 +05:30
false
end
2021-11-18 22:05:49 +05:30
2022-11-25 23:54:43 +05:30
def helm_releases
2023-04-23 21:23:45 +05:30
args = ['--all', '--all-namespaces', '--date']
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
helm.releases(args: args)
end
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
def delete_helm_releases(releases)
return if releases.empty?
2019-12-26 22:10:19 +05:30
2022-11-25 23:54:43 +05:30
releases.each do |release|
print_release_state(subject: 'Release', release_name: release.name, release_status: release.status, release_date: release.last_update, action: 'cleaning')
end
2019-12-26 22:10:19 +05:30
2022-11-25 23:54:43 +05:30
releases_names = releases.map(&:name)
unless dry_run
helm.delete(release_name: releases_names)
kubernetes.cleanup_by_release(release_name: releases_names, wait: false)
2023-05-27 22:25:52 +05:30
kubernetes.delete_namespaces_by_exact_names(resource_names: releases_names, wait: false)
2022-11-25 23:54:43 +05:30
end
2019-12-26 22:10:19 +05:30
2022-11-25 23:54:43 +05:30
rescue Tooling::Helm3Client::CommandFailedError => ex
raise ex unless ignore_exception?(ex.message, IGNORED_HELM_ERRORS)
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
puts "Ignoring the following Helm error:\n#{ex}\n"
rescue Tooling::KubernetesClient::CommandFailedError => ex
raise ex unless ignore_exception?(ex.message, IGNORED_KUBERNETES_ERRORS)
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
puts "Ignoring the following Kubernetes error:\n#{ex}\n"
end
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
def threshold_time(days:)
2023-05-27 22:25:52 +05:30
days_integer = days.to_i
raise "days should be an integer between 1 and 365 inclusive! Got #{days_integer}" unless days_integer.between?(1, 365)
Time.now - days_integer * 24 * 3600
2022-11-25 23:54:43 +05:30
end
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
def ignore_exception?(exception_message, exceptions_ignored)
exception_message.match?(/(#{exceptions_ignored})/)
end
2018-12-13 13:39:08 +05:30
2022-11-25 23:54:43 +05:30
def print_release_state(subject:, release_name:, release_date:, action:, release_status: nil)
puts "\n#{subject} '#{release_name}' #{"(#{release_status}) " if release_status}was last deployed on #{release_date}: #{action} it.\n"
end
2018-12-05 23:21:45 +05:30
end
end
def timed(task)
start = Time.now
yield(self)
puts "#{task} finished in #{Time.now - start} seconds.\n"
end
2022-11-25 23:54:43 +05:30
if $PROGRAM_NAME == __FILE__
options = {
dry_run: false
}
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
OptionParser.new do |opts|
opts.on("-d", "--dry-run", "Whether to perform a dry-run or not.") do |value|
options[:dry_run] = true
end
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end.parse!
2021-11-11 11:23:49 +05:30
2022-11-25 23:54:43 +05:30
automated_cleanup = ReviewApps::AutomatedCleanup.new(options: options)
2018-12-05 23:21:45 +05:30
2022-11-25 23:54:43 +05:30
timed('Docs Review Apps cleanup') do
automated_cleanup.perform_gitlab_docs_environment_cleanup!(days_for_stop: 20, days_for_delete: 30)
end
2021-09-30 23:02:18 +05:30
2022-11-25 23:54:43 +05:30
puts
timed('Helm releases cleanup') do
2023-04-23 21:23:45 +05:30
automated_cleanup.perform_helm_releases_cleanup!(days: 2)
2022-11-25 23:54:43 +05:30
end
2021-03-08 18:12:59 +05:30
2023-04-23 21:23:45 +05:30
puts
timed('Review Apps cleanup') do
automated_cleanup.perform_gitlab_environment_cleanup!(days_for_delete: 3)
end
puts
2022-11-25 23:54:43 +05:30
timed('Stale Namespace cleanup') do
2023-04-23 21:23:45 +05:30
automated_cleanup.perform_stale_namespace_cleanup!(days: 3)
2022-11-25 23:54:43 +05:30
end
2023-04-23 21:23:45 +05:30
puts
2022-11-25 23:54:43 +05:30
timed('Stale PVC cleanup') do
automated_cleanup.perform_stale_pvc_cleanup!(days: 30)
end
end