1# frozen_string_literal: true 2 3require 'json' 4require 'time' 5require_relative '../../../lib/gitlab/popen' unless defined?(Gitlab::Popen) 6 7module Tooling 8 class KubernetesClient 9 RESOURCE_LIST = 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa,crd' 10 CommandFailedError = Class.new(StandardError) 11 12 attr_reader :namespace 13 14 def initialize(namespace:) 15 @namespace = namespace 16 end 17 18 def cleanup_by_release(release_name:, wait: true) 19 delete_by_selector(release_name: release_name, wait: wait) 20 delete_by_matching_name(release_name: release_name) 21 end 22 23 def cleanup_by_created_at(resource_type:, created_before:, wait: true) 24 resource_names = resource_names_created_before(resource_type: resource_type, created_before: created_before) 25 return if resource_names.empty? 26 27 delete_by_exact_names(resource_type: resource_type, resource_names: resource_names, wait: wait) 28 end 29 30 def cleanup_review_app_namespaces(created_before:, wait: true) 31 namespaces = review_app_namespaces_created_before(created_before: created_before) 32 return if namespaces.empty? 33 34 delete_namespaces_by_exact_names(resource_names: namespaces, wait: wait) 35 end 36 37 private 38 39 def delete_by_selector(release_name:, wait:) 40 selector = case release_name 41 when String 42 %(-l release="#{release_name}") 43 when Array 44 %(-l 'release in (#{release_name.join(', ')})') 45 else 46 raise ArgumentError, 'release_name must be a string or an array' 47 end 48 49 command = [ 50 'delete', 51 RESOURCE_LIST, 52 %(--namespace "#{namespace}"), 53 '--now', 54 '--ignore-not-found', 55 %(--wait=#{wait}), 56 selector 57 ] 58 59 run_command(command) 60 end 61 62 def delete_by_exact_names(resource_names:, wait:, resource_type: nil) 63 command = [ 64 'delete', 65 resource_type, 66 %(--namespace "#{namespace}"), 67 '--now', 68 '--ignore-not-found', 69 %(--wait=#{wait}), 70 resource_names.join(' ') 71 ] 72 73 run_command(command) 74 end 75 76 def delete_namespaces_by_exact_names(resource_names:, wait:) 77 command = [ 78 'delete', 79 'namespace', 80 '--now', 81 '--ignore-not-found', 82 %(--wait=#{wait}), 83 resource_names.join(' ') 84 ] 85 86 run_command(command) 87 end 88 89 def delete_by_matching_name(release_name:) 90 resource_names = raw_resource_names 91 command = [ 92 'delete', 93 %(--namespace "#{namespace}"), 94 '--ignore-not-found' 95 ] 96 97 Array(release_name).each do |release| 98 resource_names 99 .select { |resource_name| resource_name.include?(release) } 100 .each { |matching_resource| run_command(command + [matching_resource]) } 101 end 102 end 103 104 def raw_resource_names 105 command = [ 106 'get', 107 RESOURCE_LIST, 108 %(--namespace "#{namespace}"), 109 '-o name' 110 ] 111 run_command(command).lines.map(&:strip) 112 end 113 114 def resource_names_created_before(resource_type:, created_before:) 115 command = [ 116 'get', 117 resource_type, 118 %(--namespace "#{namespace}"), 119 "--sort-by='{.metadata.creationTimestamp}'", 120 '-o json' 121 ] 122 123 response = run_command(command) 124 125 resources_created_before_date(response, created_before) 126 end 127 128 def review_app_namespaces_created_before(created_before:) 129 command = [ 130 'get', 131 'namespace', 132 "-l tls=review-apps-tls", # Get only namespaces used for review-apps 133 "--sort-by='{.metadata.creationTimestamp}'", 134 '-o json' 135 ] 136 137 response = run_command(command) 138 139 resources_created_before_date(response, created_before) 140 end 141 142 def resources_created_before_date(response, date) 143 items = JSON.parse(response)['items'] # rubocop:disable Gitlab/Json 144 145 items.each_with_object([]) do |item, result| 146 item_created_at = Time.parse(item.dig('metadata', 'creationTimestamp')) 147 148 if item_created_at < date 149 resource_name = item.dig('metadata', 'name') 150 result << resource_name 151 end 152 end 153 rescue ::JSON::ParserError => ex 154 puts "Ignoring this JSON parsing error: #{ex}\n\nResponse was:\n#{response}" # rubocop:disable Rails/Output 155 [] 156 end 157 158 def run_command(command) 159 final_command = ['kubectl', *command.compact].join(' ') 160 puts "Running command: `#{final_command}`" # rubocop:disable Rails/Output 161 162 result = Gitlab::Popen.popen_with_detail([final_command]) 163 164 if result.status.success? 165 result.stdout.chomp.freeze 166 else 167 raise CommandFailedError, "The `#{final_command}` command failed (status: #{result.status}) with the following error:\n#{result.stderr}" 168 end 169 end 170 end 171end 172