Class: Captive::Kubectl
- Inherits:
-
Object
- Object
- Captive::Kubectl
- Defined in:
- lib/captive/kubectl.rb
Constant Summary collapse
- EXIT_SUCCESS =
0- EXIT_FAILURE =
1- TERMINAL_PHASES =
%w[Succeeded Failed].freeze
- ADMIN_ROLE =
Rôle admin non-superuser sous lequel tourne le SQL utilisateur, sur le modèle du rôle
postgresde Supabase hébergé et duneon_superuserde Neon. Les apps sans backend Supabase n'ont que le rôle propriétaireapp. "app_admin"- ADMIN_SECRET =
"supabase-app-admin"- OWNER_ROLE =
"app"
Instance Method Summary collapse
- #admin_credentials(namespace, app) ⇒ Object
- #attach(namespace, pod) ⇒ Object
- #create_empty_secret_yaml(name, namespace) ⇒ Object
- #create_from_resource(resource) ⇒ Object
- #create_secret_yaml(name, vars, namespace: name) ⇒ Object
- #delete_job(namespace, job) ⇒ Object
- #delete_pod(namespace, pod) ⇒ Object
- #dump_from_pod(namespace, pod, local_path, cmd) ⇒ Object
- #exec_on_pod(namespace, pod, cmd) ⇒ Object
- #exec_on_pod_output(namespace, pod, cmd) ⇒ Object
- #exec_with_file(namespace, pod, file, *cmd) ⇒ Object
- #follow_job_logs(namespace, job) ⇒ Object
- #get_deployment_json(namespace, name) ⇒ Object
- #get_pod(namespace, app) ⇒ Object
- #get_secret_data(namespace, secret_name) ⇒ Object
- #get_secret_password(namespace, secret_name) ⇒ Object
-
#initialize(kubeconfig: nil) ⇒ Kubectl
constructor
A new instance of Kubectl.
- #job_exit_code(namespace, job) ⇒ Object
- #job_pod_name(namespace, job) ⇒ Object
- #kubeconfig_args ⇒ Object
- #kubectl_cmd ⇒ Object
- #logs(namespace, selector, follow: false, container: nil, previous: false, tail: 100, formatter: Presenter::LogLine.new(app: namespace), filter: LogNoiseFilter.new) ⇒ Object
- #pod_crashing?(namespace, selector) ⇒ Boolean
- #pod_info(namespace, app) ⇒ Object
- #pod_phase(namespace, pod) ⇒ Object
-
#pod_terminated?(namespace, pod, timeout: "10s") ⇒ Boolean
Attend que le pod atteigne une phase terminale.
- #postgres_pod_name(namespace, app) ⇒ Object
- #postgres_secret_password(namespace, app) ⇒ Object
- #print_logs(args, formatter, filter) ⇒ Object
- #psql_file(namespace, pod, file, user:, password:, single_transaction: true) ⇒ Object
- #psql_interactive(namespace, pod, user:, password:) ⇒ Object
- #restart_deployment(namespace, name) ⇒ Object
- #restore(namespace, pod, dump_file, format) ⇒ Object
- #run(*args) ⇒ Object
- #run_interactive(*args) ⇒ Object
- #scale(namespace, deployment, replicas) ⇒ Object
-
#stream_logs(*args) ⇒ Object
external_encoding forcé : sans LANG, Ruby lit en US-ASCII et les octets UTF-8 des séquences TTY d’une console one-off font planter la lecture.
- #system_pipe(cmd) ⇒ Object
- #transfer_file_to_pod(namespace, pod, stream_cmd, remote_path) ⇒ Object
- #wait_job_complete(namespace, job, timeout: "3600s") ⇒ Object
- #wait_job_pod(namespace, job, timeout: "120s") ⇒ Object
- #wait_pod_ready(namespace, pod, timeout: "120s") ⇒ Object
Constructor Details
#initialize(kubeconfig: nil) ⇒ Kubectl
Returns a new instance of Kubectl.
24 25 26 |
# File 'lib/captive/kubectl.rb', line 24 def initialize(kubeconfig: nil) @kubeconfig = kubeconfig end |
Instance Method Details
#admin_credentials(namespace, app) ⇒ Object
155 156 157 158 159 160 161 |
# File 'lib/captive/kubectl.rb', line 155 def admin_credentials(namespace, app) [ ADMIN_ROLE, get_secret_password(namespace, ADMIN_SECRET) ] rescue RuntimeError => e raise unless e..include?("NotFound") [ OWNER_ROLE, postgres_secret_password(namespace, app) ] end |
#attach(namespace, pod) ⇒ Object
59 60 61 |
# File 'lib/captive/kubectl.rb', line 59 def attach(namespace, pod) run_interactive("attach", "-n", namespace, "-it", pod, "-c", OneOffSpec::CONTAINER_NAME) end |
#create_empty_secret_yaml(name, namespace) ⇒ Object
135 136 137 |
# File 'lib/captive/kubectl.rb', line 135 def create_empty_secret_yaml(name, namespace) run("create", "secret", "generic", name, "-n", namespace, "--dry-run=client", "-o", "yaml") end |
#create_from_resource(resource) ⇒ Object
39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/captive/kubectl.rb', line 39 def create_from_resource(resource) json = JSON.generate(resource) out, err, status = Open3.capture3( "kubectl", *kubeconfig_args, "create", "-f", "-", "-o", "jsonpath={.metadata.name}", stdin_data: json ) raise AuthError, "Votre session a expiré. Reconnectez-vous avec : captive login" if err.include?("Unauthorized") raise "kubectl create failed: #{err}" unless status.success? out.strip end |
#create_secret_yaml(name, vars, namespace: name) ⇒ Object
139 140 141 142 |
# File 'lib/captive/kubectl.rb', line 139 def create_secret_yaml(name, vars, namespace: name) literals = vars.map { |k, v| "--from-literal=#{k}=#{v}" } run("create", "secret", "generic", name, "-n", namespace, *literals, "--dry-run=client", "-o", "yaml") end |
#delete_job(namespace, job) ⇒ Object
86 87 88 |
# File 'lib/captive/kubectl.rb', line 86 def delete_job(namespace, job) run("delete", "job", job, "-n", namespace, "--ignore-not-found=true", "--wait=false") end |
#delete_pod(namespace, pod) ⇒ Object
63 64 65 |
# File 'lib/captive/kubectl.rb', line 63 def delete_pod(namespace, pod) run("delete", "pod", pod, "-n", namespace, "--ignore-not-found=true", "--wait=false") end |
#dump_from_pod(namespace, pod, local_path, cmd) ⇒ Object
269 270 271 |
# File 'lib/captive/kubectl.rb', line 269 def dump_from_pod(namespace, pod, local_path, cmd) system_pipe("#{kubectl_cmd} exec #{pod} -n #{namespace} -- bash -c '#{cmd}' > #{Shellwords.escape(local_path)}") end |
#exec_on_pod(namespace, pod, cmd) ⇒ Object
285 286 287 |
# File 'lib/captive/kubectl.rb', line 285 def exec_on_pod(namespace, pod, cmd) run_interactive("exec", pod, "-n", namespace, "--", "bash", "-c", cmd) end |
#exec_on_pod_output(namespace, pod, cmd) ⇒ Object
289 290 291 292 |
# File 'lib/captive/kubectl.rb', line 289 def exec_on_pod_output(namespace, pod, cmd) out, _err, status = Open3.capture3("kubectl", *kubeconfig_args, "exec", pod, "-n", namespace, "--", "bash", "-c", cmd) status.success? ? out : nil end |
#exec_with_file(namespace, pod, file, *cmd) ⇒ Object
173 174 175 |
# File 'lib/captive/kubectl.rb', line 173 def exec_with_file(namespace, pod, file, *cmd) system("kubectl", *kubeconfig_args, "exec", "-n", namespace, "-i", pod, "--", *cmd, in: File.open(file)) end |
#follow_job_logs(namespace, job) ⇒ Object
90 91 92 93 94 95 |
# File 'lib/captive/kubectl.rb', line 90 def follow_job_logs(namespace, job) wait_job_pod(namespace, job) stream_logs("logs", "-n", namespace, "job/#{job}", "-c", OneOffSpec::CONTAINER_NAME, "-f") do |line| puts line end end |
#get_deployment_json(namespace, name) ⇒ Object
35 36 37 |
# File 'lib/captive/kubectl.rb', line 35 def get_deployment_json(namespace, name) JSON.parse(run("get", "deployment", name, "-n", namespace, "-o", "json")) end |
#get_pod(namespace, app) ⇒ Object
28 29 30 31 32 33 |
# File 'lib/captive/kubectl.rb', line 28 def get_pod(namespace, app) name, = pod_info(namespace, app) raise NoPodFound, "aucun pod Running pour '#{app}' dans '#{namespace}'" if name.nil? || name.empty? name end |
#get_secret_data(namespace, secret_name) ⇒ Object
240 241 242 243 244 |
# File 'lib/captive/kubectl.rb', line 240 def get_secret_data(namespace, secret_name) json = run("get", "secret", secret_name, "-n", namespace, "-o", "json") data = JSON.parse(json).fetch("data", {}) data.transform_values { |v| Base64.strict_decode64(v) } end |
#get_secret_password(namespace, secret_name) ⇒ Object
246 247 248 249 250 |
# File 'lib/captive/kubectl.rb', line 246 def get_secret_password(namespace, secret_name) encoded = run("get", "secret", secret_name, "-n", namespace, "-o", "jsonpath={.data.password}").strip Base64.strict_decode64(encoded) end |
#job_exit_code(namespace, job) ⇒ Object
97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/captive/kubectl.rb', line 97 def job_exit_code(namespace, job) json = JSON.parse(run("get", "job", job, "-n", namespace, "-o", "json")) return EXIT_SUCCESS if json.dig("status", "succeeded").to_i >= 1 pod = job_pod_name(namespace, job) return EXIT_FAILURE if pod.nil? pod_json = JSON.parse(run("get", "pod", pod, "-n", namespace, "-o", "json")) statuses = pod_json.dig("status", "containerStatuses") || [] statuses.first&.dig("state", "terminated", "exitCode") || statuses.first&.dig("lastState", "terminated", "exitCode") || EXIT_FAILURE end |
#job_pod_name(namespace, job) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/captive/kubectl.rb', line 123 def job_pod_name(namespace, job) output = run( "get", "pod", "-n", namespace, "-l", "job-name=#{job}", "-o", "jsonpath={.items[0].metadata.name}" ).strip output.empty? ? nil : output rescue RuntimeError => e return nil if e..include?("index out of bounds") || e..include?("array index") raise end |
#kubeconfig_args ⇒ Object
273 274 275 |
# File 'lib/captive/kubectl.rb', line 273 def kubeconfig_args @kubeconfig ? [ "--kubeconfig", @kubeconfig ] : [] end |
#kubectl_cmd ⇒ Object
277 278 279 |
# File 'lib/captive/kubectl.rb', line 277 def kubectl_cmd ([ "kubectl" ] + kubeconfig_args).map { |a| Shellwords.escape(a) }.join(" ") end |
#logs(namespace, selector, follow: false, container: nil, previous: false, tail: 100, formatter: Presenter::LogLine.new(app: namespace), filter: LogNoiseFilter.new) ⇒ Object
191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/captive/kubectl.rb', line 191 def logs(namespace, selector, follow: false, container: nil, previous: false, tail: 100, formatter: Presenter::LogLine.new(app: namespace), filter: LogNoiseFilter.new) args = [ "logs", "-n", namespace, "-l", selector, "--timestamps", "--prefix" ] args << "-f" if follow args << "-p" if previous args += [ "--tail", tail.to_s ] unless follow args += [ "-c", container ] if container success = print_logs(args, formatter, filter) return if success || !previous warn "⚠️ Logs du run précédent non disponibles (purgés par le runtime), affichage des logs actuels" print_logs(args - [ "-p" ], formatter, filter) end |
#pod_crashing?(namespace, selector) ⇒ Boolean
177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/captive/kubectl.rb', line 177 def pod_crashing?(namespace, selector) json = run("get", "pod", "-n", namespace, "-l", selector, "-o", "json") items = JSON.parse(json)["items"] || [] items.any? do |pod| statuses = (pod.dig("status", "containerStatuses") || []) + (pod.dig("status", "initContainerStatuses") || []) statuses.any? do |s| s["restartCount"].to_i > 0 || s.dig("state", "waiting", "reason")&.include?("CrashLoop") || s.dig("lastState", "terminated", "exitCode").to_i != 0 end end end |
#pod_info(namespace, app) ⇒ Object
227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/captive/kubectl.rb', line 227 def pod_info(namespace, app) output = run( "get", "pod", "-n", namespace, "-l", "app=#{app}", "--field-selector=status.phase=Running", "-o", "jsonpath={.items[0].metadata.name} {.items[0].metadata.creationTimestamp}" ).strip output.empty? ? [ nil, nil ] : output.split(" ", 2) rescue RuntimeError => e return [ nil, nil ] if e..include?("index out of bounds") || e..include?("array index") raise end |
#pod_phase(namespace, pod) ⇒ Object
80 81 82 83 84 |
# File 'lib/captive/kubectl.rb', line 80 def pod_phase(namespace, pod) run("get", "pod", pod, "-n", namespace, "-o", "jsonpath={.status.phase}").strip rescue RuntimeError "" end |
#pod_terminated?(namespace, pod, timeout: "10s") ⇒ Boolean
Attend que le pod atteigne une phase terminale. Sert à distinguer une session console sortie proprement (à laisser au TTL du Job) d’une session abandonnée (à supprimer tout de suite).
70 71 72 73 74 75 76 77 78 |
# File 'lib/captive/kubectl.rb', line 70 def pod_terminated?(namespace, pod, timeout: "10s") deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + parse_timeout(timeout) loop do return true if TERMINAL_PHASES.include?(pod_phase(namespace, pod)) return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline sleep 0.5 end end |
#postgres_pod_name(namespace, app) ⇒ Object
258 259 260 261 262 263 |
# File 'lib/captive/kubectl.rb', line 258 def postgres_pod_name(namespace, app) with_cnpg_fallback("postgres-#{app}-1", "postgres-#{app}-cnpg-1") do |name| run("get", "pod", name, "-n", namespace, "-o", "jsonpath={.metadata.name}") name end end |
#postgres_secret_password(namespace, app) ⇒ Object
252 253 254 255 256 |
# File 'lib/captive/kubectl.rb', line 252 def postgres_secret_password(namespace, app) with_cnpg_fallback("postgres-#{app}-app", "postgres-#{app}-cnpg-app") do |name| get_secret_password(namespace, name) end end |
#print_logs(args, formatter, filter) ⇒ Object
205 206 207 208 209 210 211 |
# File 'lib/captive/kubectl.rb', line 205 def print_logs(args, formatter, filter) stream_logs(*args) do |line| next if filter.noise?(line) puts formatter.call(line) end end |
#psql_file(namespace, pod, file, user:, password:, single_transaction: true) ⇒ Object
163 164 165 166 167 |
# File 'lib/captive/kubectl.rb', line 163 def psql_file(namespace, pod, file, user:, password:, single_transaction: true) flags = [ "-v", "ON_ERROR_STOP=1" ] flags << "--single-transaction" if single_transaction exec_with_file(namespace, pod, file, *psql_argv(user, password, flags)) end |
#psql_interactive(namespace, pod, user:, password:) ⇒ Object
169 170 171 |
# File 'lib/captive/kubectl.rb', line 169 def psql_interactive(namespace, pod, user:, password:) run_interactive("exec", "-n", namespace, "-it", pod, "--", *psql_argv(user, password)) end |
#restart_deployment(namespace, name) ⇒ Object
294 295 296 |
# File 'lib/captive/kubectl.rb', line 294 def restart_deployment(namespace, name) run("rollout", "restart", "deployment", "-n", namespace, name) end |
#restore(namespace, pod, dump_file, format) ⇒ Object
148 149 150 151 152 153 |
# File 'lib/captive/kubectl.rb', line 148 def restore(namespace, pod, dump_file, format) cmd = format == :custom ? [ "pg_restore", "--clean", "--if-exists", "--no-owner", "--no-acl", "-d", "app" ] : [ "psql", "app" ] exec_with_file(namespace, pod, dump_file, *cmd) end |
#run(*args) ⇒ Object
298 299 300 301 302 303 304 |
# File 'lib/captive/kubectl.rb', line 298 def run(*args) out, err, status = Open3.capture3("kubectl", *kubeconfig_args, *args) raise AuthError, "Votre session a expiré. Reconnectez-vous avec : captive login" if err.include?("Unauthorized") raise "kubectl #{args.join(" ")} failed: #{err}" unless status.success? out end |
#run_interactive(*args) ⇒ Object
213 214 215 |
# File 'lib/captive/kubectl.rb', line 213 def run_interactive(*args) system("kubectl", *kubeconfig_args, *args) end |
#scale(namespace, deployment, replicas) ⇒ Object
144 145 146 |
# File 'lib/captive/kubectl.rb', line 144 def scale(namespace, deployment, replicas) run("scale", "deployment", deployment, "-n", namespace, "--replicas=#{replicas}") end |
#stream_logs(*args) ⇒ Object
external_encoding forcé : sans LANG, Ruby lit en US-ASCII et les octets UTF-8 des séquences TTY d’une console one-off font planter la lecture.
219 220 221 222 223 224 225 |
# File 'lib/captive/kubectl.rb', line 219 def stream_logs(*args) IO.popen([ "kubectl", *kubeconfig_args, *args ], err: [ :child, :out ], external_encoding: Encoding::UTF_8) do |io| io.each_line { |line| yield line.scrub } end $?.success? end |
#system_pipe(cmd) ⇒ Object
281 282 283 |
# File 'lib/captive/kubectl.rb', line 281 def system_pipe(cmd) system("bash", "-c", cmd) end |
#transfer_file_to_pod(namespace, pod, stream_cmd, remote_path) ⇒ Object
265 266 267 |
# File 'lib/captive/kubectl.rb', line 265 def transfer_file_to_pod(namespace, pod, stream_cmd, remote_path) system_pipe("#{stream_cmd} | #{kubectl_cmd} exec -i #{pod} -n #{namespace} -- bash -c 'cat > #{remote_path}'") end |
#wait_job_complete(namespace, job, timeout: "3600s") ⇒ Object
55 56 57 |
# File 'lib/captive/kubectl.rb', line 55 def wait_job_complete(namespace, job, timeout: "3600s") run("wait", "--for=condition=complete", "job/#{job}", "-n", namespace, "--timeout=#{timeout}") end |
#wait_job_pod(namespace, job, timeout: "120s") ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/captive/kubectl.rb', line 111 def wait_job_pod(namespace, job, timeout: "120s") deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + parse_timeout(timeout) loop do name = job_pod_name(namespace, job) return name if name raise "aucun pod pour le Job '#{job}' dans '#{namespace}'" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline sleep 0.5 end end |
#wait_pod_ready(namespace, pod, timeout: "120s") ⇒ Object
51 52 53 |
# File 'lib/captive/kubectl.rb', line 51 def wait_pod_ready(namespace, pod, timeout: "120s") run("wait", "--for=condition=Ready", "pod/#{pod}", "-n", namespace, "--timeout=#{timeout}") end |