captive-cli 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/captive/cli/dependencies.rb +1 -0
- data/lib/captive/cli.rb +9 -9
- data/lib/captive/commands/addons_commands.rb +68 -0
- data/lib/captive/commands/logs.rb +24 -13
- data/lib/captive/dashboard/addons_resource.rb +22 -0
- data/lib/captive/kubectl.rb +0 -38
- data/lib/captive/loader.rb +3 -3
- data/lib/captive/log_stream.rb +75 -0
- data/lib/captive/one_off_spec.rb +0 -4
- data/lib/captive/presenter/addon.rb +30 -0
- data/lib/captive/presenter/app_info.rb +2 -0
- data/lib/captive/presenter/supabase_auth.rb +31 -0
- data/lib/captive/shell_completions.rb +3 -1
- data/lib/captive/sse_parser.rb +26 -0
- data/lib/captive/version.rb +1 -1
- metadata +7 -2
- data/lib/captive/presenter/log_line.rb +0 -40
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a31d2e7433e7b31ac59e131ba6a4ffd55732c3e71641fd5e22a45340e8f3f814
|
|
4
|
+
data.tar.gz: 72b86e63fd6a755f22fab06fd60f1eff32fab9dd8f2c1dfff7a485b11e1a0cd2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4b15fc087ba7cc009d657b7ce9d0f5738b141d423797d25ab435c43e2aa77442b408db411772dbf1cc3a7b7244c087c56668d6d89c0ccd418fd18a2ce41d6e5
|
|
7
|
+
data.tar.gz: 688bfa7d022d5a4e7605624741153fbb84c0aa9ba90c677035acb87b6935d1febec027421ec3762360ce6fda3ea9c840a9d52f48b816da57a8feffb02caa3da4
|
|
@@ -25,6 +25,7 @@ module Captive
|
|
|
25
25
|
def env_api = @env_api ||= Dashboard::EnvVarsResource.new(api: dashboard)
|
|
26
26
|
def crons_api = @crons_api ||= Dashboard::ScheduledTasksResource.new(api: dashboard)
|
|
27
27
|
def domains_api = @domains_api ||= Dashboard::DomainsResource.new(api: dashboard)
|
|
28
|
+
def addons_api = @addons_api ||= Dashboard::AddonsResource.new(api: dashboard)
|
|
28
29
|
|
|
29
30
|
def active_kubeconfig
|
|
30
31
|
env_kc = ENV["KUBECONFIG"]
|
data/lib/captive/cli.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "commands/apps_commands"
|
|
|
7
7
|
require_relative "commands/env_commands"
|
|
8
8
|
require_relative "commands/crons_commands"
|
|
9
9
|
require_relative "commands/domains_commands"
|
|
10
|
+
require_relative "commands/addons_commands"
|
|
10
11
|
require_relative "commands/db_commands"
|
|
11
12
|
require_relative "commands/user_login"
|
|
12
13
|
require_relative "commands/auth_commands"
|
|
@@ -66,6 +67,9 @@ module Captive
|
|
|
66
67
|
desc "domains SUBCOMMAND", "Domaines d'une app"
|
|
67
68
|
subcommand "domains", Commands::DomainsCommands
|
|
68
69
|
|
|
70
|
+
desc "addons SUBCOMMAND", "Add-ons d'une app (bases, Supabase…)"
|
|
71
|
+
subcommand "addons", Commands::AddonsCommands
|
|
72
|
+
|
|
69
73
|
desc "auth SUBCOMMAND", "Jeton pour kubectl", hide: true
|
|
70
74
|
subcommand "auth", Commands::AuthCommands
|
|
71
75
|
|
|
@@ -146,13 +150,12 @@ module Captive
|
|
|
146
150
|
|
|
147
151
|
desc "logs APP", "Affiche les logs de l'app"
|
|
148
152
|
long_desc <<~DESC
|
|
149
|
-
MODE TEMPS RÉEL (défaut) —
|
|
153
|
+
MODE TEMPS RÉEL (défaut) — dernières lignes du web et des one-offs, via le dashboard.
|
|
150
154
|
-f, --follow Suit en continu (Ctrl+C pour arrêter)
|
|
151
155
|
-n, --lines N Nombre de lignes affichées (défaut : 100)
|
|
152
|
-
-
|
|
153
|
-
-w, --worker Cible le pod worker au lieu du pod web
|
|
156
|
+
-w, --worker Affiche le worker au lieu du web
|
|
154
157
|
--raw Garde les lignes rack-timeout de la readinessProbe
|
|
155
|
-
|
|
158
|
+
Il suffit d'être Membre de l'Organization de l'app, sans accès au repo GitHub.
|
|
156
159
|
|
|
157
160
|
MODE HISTORIQUE (--from / --to) — interroge Loki via le dashboard.
|
|
158
161
|
--from DATE Début de la plage (requis pour ce mode)
|
|
@@ -165,14 +168,12 @@ module Captive
|
|
|
165
168
|
\x05 captive logs my-app
|
|
166
169
|
\x05 captive logs my-app -f
|
|
167
170
|
\x05 captive logs my-app -n 200
|
|
168
|
-
\x05 captive logs my-app -c migrate
|
|
169
171
|
\x05 captive logs my-app --worker
|
|
170
172
|
\x05 captive logs my-app --raw
|
|
171
173
|
\x05 captive logs my-app --from "2026-08-31 23:00" --to "2026-09-01 00:00"
|
|
172
174
|
\x05 captive logs my-app --from "2026-08-31 23:00"
|
|
173
175
|
DESC
|
|
174
176
|
method_option :follow, aliases: "-f", type: :boolean, default: false
|
|
175
|
-
method_option :container, aliases: "-c", type: :string
|
|
176
177
|
method_option :worker, aliases: "-w", type: :boolean, default: false
|
|
177
178
|
method_option :lines, aliases: "-n", type: :numeric, default: 100
|
|
178
179
|
method_option :raw, type: :boolean, default: false, desc: "Affiche le bruit rack-timeout des sondes readiness"
|
|
@@ -181,9 +182,8 @@ module Captive
|
|
|
181
182
|
def logs(app)
|
|
182
183
|
from = parse_time(options[:from])
|
|
183
184
|
to = parse_time(options[:to])
|
|
184
|
-
Commands::Logs.new
|
|
185
|
-
app, follow: options[:follow],
|
|
186
|
-
worker: options[:worker], lines: options[:lines],
|
|
185
|
+
Commands::Logs.new.run(
|
|
186
|
+
app, follow: options[:follow], worker: options[:worker], lines: options[:lines],
|
|
187
187
|
from: from, to: to, raw: options[:raw]
|
|
188
188
|
)
|
|
189
189
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Captive
|
|
4
|
+
module Commands
|
|
5
|
+
# Les Add-ons d'une app via le dashboard (ADR 0034) : tout ce que la page Add-ons permet,
|
|
6
|
+
# sans quitter le terminal. L'ajout est réservé aux admins de l'organisation.
|
|
7
|
+
class AddonsCommands < Thor
|
|
8
|
+
include CLIDependencies
|
|
9
|
+
|
|
10
|
+
def self.exit_on_failure? = true
|
|
11
|
+
|
|
12
|
+
desc "list APP", "Liste les add-ons de l'app et leur statut"
|
|
13
|
+
long_desc <<~DESC
|
|
14
|
+
\x05Exemple :
|
|
15
|
+
\x05 captive addons list my-app-staging
|
|
16
|
+
DESC
|
|
17
|
+
def list(app)
|
|
18
|
+
installed = addons_api.list(app)["installed"]
|
|
19
|
+
return puts(paint.dim("Aucun add-on. Voir : captive addons catalog #{app}")) if installed.empty?
|
|
20
|
+
|
|
21
|
+
installed.each { |addon| puts presenter.installed(addon) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "catalog APP", "Liste les add-ons qu'on peut ajouter à l'app"
|
|
25
|
+
long_desc <<~DESC
|
|
26
|
+
\x05Exemple :
|
|
27
|
+
\x05 captive addons catalog my-app-staging
|
|
28
|
+
DESC
|
|
29
|
+
def catalog(app)
|
|
30
|
+
addons_api.list(app)["catalog"].each { |entry| puts presenter.catalog(entry) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "add APP ADDON", "Ajoute un add-on à l'app (postgres, supabase…)"
|
|
34
|
+
long_desc <<~DESC
|
|
35
|
+
Un add-on « sur demande » (redis, egress-ip) prévient l'équipe plateforme au lieu de
|
|
36
|
+
l'ajouter. Réservé aux admins de l'organisation.
|
|
37
|
+
|
|
38
|
+
\x05Exemple :
|
|
39
|
+
\x05 captive addons add my-app-staging supabase
|
|
40
|
+
DESC
|
|
41
|
+
def add(app, addon)
|
|
42
|
+
result = addons_api.add(app, addon)
|
|
43
|
+
if result["status"] == "requested"
|
|
44
|
+
puts paint.green(" ✓ #{addon} demandé à l'équipe plateforme")
|
|
45
|
+
else
|
|
46
|
+
puts paint.green(" ✓ #{addon} en cours d'ajout : #{ASYNC_NOTICE}")
|
|
47
|
+
puts paint.dim(" Suivre : captive addons list #{app}")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "auth APP", "Affiche les réglages d'authentification de l'add-on Supabase"
|
|
52
|
+
long_desc <<~DESC
|
|
53
|
+
Auto-confirmation des emails et inscriptions ouvertes, en lecture seule : seule
|
|
54
|
+
l'équipe plateforme les modifie.
|
|
55
|
+
|
|
56
|
+
\x05Exemple :
|
|
57
|
+
\x05 captive addons auth my-app-staging
|
|
58
|
+
DESC
|
|
59
|
+
def auth(app)
|
|
60
|
+
Presenter::SupabaseAuth.new(paint: paint).lines(addons_api.supabase_auth(app), app: app).each { |line| puts line }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def presenter = Presenter::Addon.new(paint: paint)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -2,36 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
4
|
require_relative "../log_noise_filter"
|
|
5
|
+
require_relative "../log_stream"
|
|
5
6
|
require_relative "../null_log_filter"
|
|
6
|
-
require_relative "../one_off_spec"
|
|
7
7
|
require_relative "../presenter/log_colorizer"
|
|
8
8
|
|
|
9
9
|
module Captive
|
|
10
10
|
module Commands
|
|
11
|
-
# Affiche les logs d'une app
|
|
11
|
+
# Affiche les logs d'une app, toujours via le dashboard (Loki) : seule l'Organization décide
|
|
12
|
+
# de l'accès, plus le repo GitHub. Dernières lignes, suivi en direct (-f) ou plage (--from/--to).
|
|
12
13
|
class Logs
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
WORKER = "worker"
|
|
15
|
+
|
|
16
|
+
def initialize(dashboard: DashboardClient.new, stream: LogStream.new, paint: Pastel.new)
|
|
15
17
|
@dashboard = dashboard
|
|
18
|
+
@stream = stream
|
|
16
19
|
@colorizer = Presenter::LogColorizer.new(paint: paint)
|
|
17
20
|
end
|
|
18
21
|
|
|
19
|
-
def run(app, follow: false,
|
|
22
|
+
def run(app, follow: false, worker: false, lines: 100, from: nil, to: nil, raw: false)
|
|
20
23
|
if from || to
|
|
21
|
-
run_historical(app, from: from, to: to,
|
|
24
|
+
run_historical(app, from: from, to: to, raw: raw)
|
|
22
25
|
else
|
|
23
|
-
run_live(app, follow: follow,
|
|
26
|
+
run_live(app, follow: follow, worker: worker, lines: lines, raw: raw)
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
private
|
|
28
31
|
|
|
29
|
-
def run_live(app, follow:,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
def run_live(app, follow:, worker:, lines:, raw:)
|
|
33
|
+
printer = live_printer(worker, log_filter(raw))
|
|
34
|
+
follow ? @stream.call(app, tail: lines, &printer) : @dashboard.logs(app, limit: lines).each(&printer)
|
|
35
|
+
rescue Interrupt
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Comme le sélecteur kubectl d'avant : le web et les one-offs, ou le worker seul.
|
|
40
|
+
def live_printer(worker, filter)
|
|
41
|
+
lambda do |line|
|
|
42
|
+
next if (line["process"] == WORKER) != worker || filter.noise?(line["text"].to_s)
|
|
43
|
+
|
|
44
|
+
$stdout.puts format_line(line)
|
|
45
|
+
end
|
|
35
46
|
end
|
|
36
47
|
|
|
37
48
|
def run_historical(app, from:, to:, raw:, **)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Captive
|
|
4
|
+
module Dashboard
|
|
5
|
+
# Les Add-ons d'une app et le Catalogue (ADR 0034) : la même API que la page Add-ons du
|
|
6
|
+
# dashboard. Un ajout est appliqué plus tard par captive-platform (réponse 202).
|
|
7
|
+
class AddonsResource
|
|
8
|
+
PATH = AppPath.new("addons")
|
|
9
|
+
SUPABASE_AUTH_PATH = AppPath.new("addons/supabase/auth")
|
|
10
|
+
|
|
11
|
+
def initialize(api:)
|
|
12
|
+
@api = api
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def list(app) = @api.get(PATH.call(app))
|
|
16
|
+
def add(app, addon) = @api.post(PATH.call(app), { addon: addon })
|
|
17
|
+
|
|
18
|
+
# En lecture seule : seule l'équipe plateforme modifie ces réglages, dans le dépôt GitOps.
|
|
19
|
+
def supabase_auth(app) = @api.get(SUPABASE_AUTH_PATH.call(app))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/captive/kubectl.rb
CHANGED
|
@@ -4,9 +4,7 @@ require "base64"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "open3"
|
|
6
6
|
require_relative "errors"
|
|
7
|
-
require_relative "log_noise_filter"
|
|
8
7
|
require_relative "one_off_spec"
|
|
9
|
-
require_relative "presenter/log_line"
|
|
10
8
|
|
|
11
9
|
module Captive
|
|
12
10
|
class Kubectl
|
|
@@ -174,42 +172,6 @@ module Captive
|
|
|
174
172
|
system("kubectl", *kubeconfig_args, "exec", "-n", namespace, "-i", pod, "--", *cmd, in: File.open(file))
|
|
175
173
|
end
|
|
176
174
|
|
|
177
|
-
def pod_crashing?(namespace, selector)
|
|
178
|
-
json = run("get", "pod", "-n", namespace, "-l", selector, "-o", "json")
|
|
179
|
-
items = JSON.parse(json)["items"] || []
|
|
180
|
-
items.any? do |pod|
|
|
181
|
-
statuses = (pod.dig("status", "containerStatuses") || []) +
|
|
182
|
-
(pod.dig("status", "initContainerStatuses") || [])
|
|
183
|
-
statuses.any? do |s|
|
|
184
|
-
s["restartCount"].to_i > 0 ||
|
|
185
|
-
s.dig("state", "waiting", "reason")&.include?("CrashLoop") ||
|
|
186
|
-
s.dig("lastState", "terminated", "exitCode").to_i != 0
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def logs(namespace, selector, follow: false, container: nil, previous: false, tail: 100,
|
|
192
|
-
formatter: Presenter::LogLine.new(app: namespace), filter: LogNoiseFilter.new)
|
|
193
|
-
args = [ "logs", "-n", namespace, "-l", selector, "--timestamps", "--prefix" ]
|
|
194
|
-
args << "-f" if follow
|
|
195
|
-
args << "-p" if previous
|
|
196
|
-
args += [ "--tail", tail.to_s ] unless follow
|
|
197
|
-
args += [ "-c", container ] if container
|
|
198
|
-
success = print_logs(args, formatter, filter)
|
|
199
|
-
return if success || !previous
|
|
200
|
-
|
|
201
|
-
warn "⚠️ Logs du run précédent non disponibles (purgés par le runtime), affichage des logs actuels"
|
|
202
|
-
print_logs(args - [ "-p" ], formatter, filter)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def print_logs(args, formatter, filter)
|
|
206
|
-
stream_logs(*args) do |line|
|
|
207
|
-
next if filter.noise?(line)
|
|
208
|
-
|
|
209
|
-
puts formatter.call(line)
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
|
|
213
175
|
def run_interactive(*args)
|
|
214
176
|
system("kubectl", *kubeconfig_args, *args)
|
|
215
177
|
end
|
data/lib/captive/loader.rb
CHANGED
|
@@ -18,9 +18,12 @@ require_relative "dashboard/apps_resource"
|
|
|
18
18
|
require_relative "dashboard/env_vars_resource"
|
|
19
19
|
require_relative "dashboard/scheduled_tasks_resource"
|
|
20
20
|
require_relative "dashboard/domains_resource"
|
|
21
|
+
require_relative "dashboard/addons_resource"
|
|
21
22
|
require_relative "presenter/app_info"
|
|
22
23
|
require_relative "presenter/scheduled_task"
|
|
23
24
|
require_relative "presenter/domain"
|
|
25
|
+
require_relative "presenter/addon"
|
|
26
|
+
require_relative "presenter/supabase_auth"
|
|
24
27
|
require_relative "activity_trail"
|
|
25
28
|
require_relative "errors"
|
|
26
29
|
require_relative "ui/spinner"
|
|
@@ -35,6 +38,3 @@ require_relative "commands/domain_wait"
|
|
|
35
38
|
require_relative "commands/domain_announcer"
|
|
36
39
|
require_relative "commands/db_dump"
|
|
37
40
|
require_relative "commands/db_restore"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
require_relative "presenter/log_line"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "dashboard_client"
|
|
7
|
+
require_relative "dashboard_response"
|
|
8
|
+
require_relative "session_token"
|
|
9
|
+
require_relative "sse_parser"
|
|
10
|
+
|
|
11
|
+
module Captive
|
|
12
|
+
# Suit les logs d'une app en direct, en Server-Sent Events depuis le dashboard
|
|
13
|
+
# (`GET /api/apps/:id/logs/stream`). Le dashboard ferme le flux au bout de quelques minutes :
|
|
14
|
+
# on se reconnecte alors avec le dernier id reçu, l'horodatage de la dernière ligne, pour
|
|
15
|
+
# reprendre sans perte ni doublon. Seul Ctrl+C (Interrupt) ou un refus du dashboard l'arrête.
|
|
16
|
+
class LogStream
|
|
17
|
+
RECONNECT_DELAY = 1
|
|
18
|
+
NETWORK_ERRORS = [ IOError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError ].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(session: SessionToken.new)
|
|
21
|
+
@session = session
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.stream_uri(app, tail)
|
|
25
|
+
uri = URI.parse("#{DashboardClient::DASHBOARD_URL}/api/apps/#{URI.encode_uri_component(app)}/logs/stream")
|
|
26
|
+
uri.query = URI.encode_www_form(tail:)
|
|
27
|
+
uri
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.headers(last_id)
|
|
31
|
+
last_id ? { "Accept" => "text/event-stream", "Last-Event-ID" => last_id } : { "Accept" => "text/event-stream" }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.read(response, &)
|
|
35
|
+
DashboardResponse.new(response).parse unless response.code.to_i == 200
|
|
36
|
+
response.read_body(&)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call(app, tail:, &)
|
|
40
|
+
uri = self.class.stream_uri(app, tail)
|
|
41
|
+
@last_id = nil
|
|
42
|
+
loop do
|
|
43
|
+
follow(uri, &)
|
|
44
|
+
rescue *NETWORK_ERRORS
|
|
45
|
+
sleep RECONNECT_DELAY
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# @last_id avance à chaque événement, pas en fin de connexion : une coupure réseau ne doit
|
|
52
|
+
# pas faire reprendre plus tôt, ce qui réafficherait des lignes.
|
|
53
|
+
def follow(uri, &)
|
|
54
|
+
parser = SseParser.new
|
|
55
|
+
open_stream(uri, self.class.headers(@last_id)) { |chunk| relay(parser, chunk, &) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def relay(parser, chunk)
|
|
59
|
+
parser.feed(chunk) do |event|
|
|
60
|
+
@last_id = event[:id] || @last_id
|
|
61
|
+
yield JSON.parse(event[:data])
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Pas de read_timeout court : le flux reste ouvert, le serveur envoie un battement de cœur.
|
|
66
|
+
def open_stream(uri, headers, &)
|
|
67
|
+
@session.authorize do |token|
|
|
68
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 5, read_timeout: 60) do |http|
|
|
69
|
+
request = Net::HTTP::Get.new(uri, headers.merge("Authorization" => "Bearer #{token}"))
|
|
70
|
+
http.request(request) { |response| self.class.read(response, &) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/captive/one_off_spec.rb
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Captive
|
|
4
|
+
module Presenter
|
|
5
|
+
# Une ligne par Add-on présent (« Les vôtres ») ou proposé (« Catalogue »).
|
|
6
|
+
class Addon
|
|
7
|
+
def initialize(paint:)
|
|
8
|
+
@paint = paint
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def installed(addon)
|
|
12
|
+
details = [ addon["name"], addon["tier"]&.upcase ].compact.join(" ")
|
|
13
|
+
[ @paint.bold(addon["addon"]), details, status(addon["status"]) ].reject(&:empty?).join(" ")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def catalog(entry)
|
|
17
|
+
head = "#{@paint.bold(entry["id"])} #{entry["description"]}"
|
|
18
|
+
notes = []
|
|
19
|
+
notes << "inclut #{entry["includes"].join(", ")}" if entry["includes"].any?
|
|
20
|
+
notes << "sur demande" if entry["mode"] == "request"
|
|
21
|
+
notes << @paint.green("ajouté") if entry["installed"]
|
|
22
|
+
notes.empty? ? head : "#{head} #{@paint.dim("(#{notes.join(", ")})")}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def status(value) = value == "ready" ? @paint.green("✓") : @paint.yellow("en cours")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -26,6 +26,8 @@ module Captive
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def workload(entry)
|
|
29
|
+
return "#{entry["name"]} #{@paint.yellow(entry["status_reason"])}" if entry["status"] == "config_required"
|
|
30
|
+
|
|
29
31
|
ready = "#{entry["ready"]}/#{entry["total"]}"
|
|
30
32
|
ready = @paint.yellow(ready) unless entry["ready"] == entry["total"]
|
|
31
33
|
"#{entry["name"]} #{ready} prêt(s), tier #{entry["tier"] || "–"}"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Captive
|
|
4
|
+
module Presenter
|
|
5
|
+
# Les réglages d'authentification de l'Add-on Supabase, en lecture seule comme sur la page
|
|
6
|
+
# du dashboard : une valeur que le dashboard n'a pas pu lire reste « inconnu ».
|
|
7
|
+
class SupabaseAuth
|
|
8
|
+
VALEURS = { true => "oui", false => "non" }.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(paint:)
|
|
11
|
+
@paint = paint
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def lines(settings, app:)
|
|
15
|
+
[
|
|
16
|
+
[ "Auto-confirmation des emails", valeur(settings["autoconfirm"]), alerte(settings, app) ].compact.join(" "),
|
|
17
|
+
"Inscriptions ouvertes #{valeur(settings["signup_open"])}",
|
|
18
|
+
@paint.dim("Modifiable par l'équipe plateforme"),
|
|
19
|
+
]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def valeur(value) = VALEURS.fetch(value, "inconnu")
|
|
25
|
+
|
|
26
|
+
def alerte(settings, app)
|
|
27
|
+
@paint.red("⚠ active en production") if settings["autoconfirm"] && app.end_with?("-production")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Captive
|
|
4
4
|
module ShellCompletions
|
|
5
|
-
ROOT_COMMANDS = %w[apps env crons domains db login whoami console run logs version completions].freeze
|
|
5
|
+
ROOT_COMMANDS = %w[apps env crons domains addons db login whoami console run logs version completions].freeze
|
|
6
6
|
|
|
7
7
|
SUBCOMMANDS = {
|
|
8
8
|
"apps" => %w[list info restart rebuild],
|
|
9
9
|
"env" => %w[list set unset],
|
|
10
10
|
"crons" => %w[list add edit rm suspend resume trigger runs],
|
|
11
11
|
"domains" => %w[list add rm redirect serve],
|
|
12
|
+
"addons" => %w[list catalog add],
|
|
12
13
|
"db" => %w[restore],
|
|
13
14
|
}.freeze
|
|
14
15
|
|
|
@@ -25,6 +26,7 @@ module Captive
|
|
|
25
26
|
env) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["env"].join(" ")}" -- "$cur")); return ;;
|
|
26
27
|
crons) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["crons"].join(" ")}" -- "$cur")); return ;;
|
|
27
28
|
domains) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["domains"].join(" ")}" -- "$cur")); return ;;
|
|
29
|
+
addons) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["addons"].join(" ")}" -- "$cur")); return ;;
|
|
28
30
|
db) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["db"].join(" ")}" -- "$cur")); return ;;
|
|
29
31
|
esac
|
|
30
32
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Captive
|
|
4
|
+
# Découpe un flux Server-Sent Events, reçu par morceaux arbitraires, en événements
|
|
5
|
+
# `{ id:, data: }`. Les commentaires (`:`, battements de cœur) et `retry` sont ignorés.
|
|
6
|
+
class SseParser
|
|
7
|
+
def initialize
|
|
8
|
+
@buffer = +""
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def feed(chunk)
|
|
12
|
+
@buffer << chunk
|
|
13
|
+
while (fin = @buffer.index("\n\n"))
|
|
14
|
+
event = self.class.parse(@buffer.slice!(0, fin + 2))
|
|
15
|
+
yield event if event[:data]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.parse(bloc)
|
|
20
|
+
bloc.each_line(chomp: true).each_with_object({}) do |ligne, event|
|
|
21
|
+
champ, valeur = ligne.split(": ", 2)
|
|
22
|
+
event[champ.to_sym] = valeur if %w[id data].include?(champ)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/captive/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: captive-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Captive Studio
|
|
@@ -107,6 +107,7 @@ files:
|
|
|
107
107
|
- lib/captive/cli.rb
|
|
108
108
|
- lib/captive/cli/dependencies.rb
|
|
109
109
|
- lib/captive/cluster_endpoint.rb
|
|
110
|
+
- lib/captive/commands/addons_commands.rb
|
|
110
111
|
- lib/captive/commands/apps_commands.rb
|
|
111
112
|
- lib/captive/commands/apps_restart.rb
|
|
112
113
|
- lib/captive/commands/auth_commands.rb
|
|
@@ -131,6 +132,7 @@ files:
|
|
|
131
132
|
- lib/captive/commands/user_login.rb
|
|
132
133
|
- lib/captive/credentials.rb
|
|
133
134
|
- lib/captive/credentials_store.rb
|
|
135
|
+
- lib/captive/dashboard/addons_resource.rb
|
|
134
136
|
- lib/captive/dashboard/app_path.rb
|
|
135
137
|
- lib/captive/dashboard/apps_resource.rb
|
|
136
138
|
- lib/captive/dashboard/domains_resource.rb
|
|
@@ -146,18 +148,21 @@ files:
|
|
|
146
148
|
- lib/captive/kubectl.rb
|
|
147
149
|
- lib/captive/loader.rb
|
|
148
150
|
- lib/captive/log_noise_filter.rb
|
|
151
|
+
- lib/captive/log_stream.rb
|
|
149
152
|
- lib/captive/null_log_filter.rb
|
|
150
153
|
- lib/captive/one_off_spec.rb
|
|
151
154
|
- lib/captive/poller.rb
|
|
155
|
+
- lib/captive/presenter/addon.rb
|
|
152
156
|
- lib/captive/presenter/ansi_stripper.rb
|
|
153
157
|
- lib/captive/presenter/app_info.rb
|
|
154
158
|
- lib/captive/presenter/domain.rb
|
|
155
159
|
- lib/captive/presenter/log_colorizer.rb
|
|
156
|
-
- lib/captive/presenter/log_line.rb
|
|
157
160
|
- lib/captive/presenter/scheduled_task.rb
|
|
161
|
+
- lib/captive/presenter/supabase_auth.rb
|
|
158
162
|
- lib/captive/session_id.rb
|
|
159
163
|
- lib/captive/session_token.rb
|
|
160
164
|
- lib/captive/shell_completions.rb
|
|
165
|
+
- lib/captive/sse_parser.rb
|
|
161
166
|
- lib/captive/terminal.rb
|
|
162
167
|
- lib/captive/token_refresher.rb
|
|
163
168
|
- lib/captive/ui/banner.rb
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "captive/log_utils"
|
|
4
|
-
require_relative "log_colorizer"
|
|
5
|
-
|
|
6
|
-
module Captive
|
|
7
|
-
module Presenter
|
|
8
|
-
# Parses a raw kubectl log line and renders it as a colorized terminal string.
|
|
9
|
-
class LogLine
|
|
10
|
-
BRACKET_RE = /\A\[(?:pod\/)?(?<pod>[^\/\]]+)(?:\/(?<container>[^\/\]]+))?\]\z/
|
|
11
|
-
|
|
12
|
-
def initialize(app:, paint: Pastel.new)
|
|
13
|
-
@app = app
|
|
14
|
-
@colorizer = LogColorizer.new(paint: paint)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def call(raw_line)
|
|
18
|
-
line = raw_line.chomp
|
|
19
|
-
first, second, message = line.split(" ", 3)
|
|
20
|
-
return line if first.nil? || second.nil? || message.nil?
|
|
21
|
-
|
|
22
|
-
bracket, ts = pick_bracket_and_timestamp(first, second)
|
|
23
|
-
return line unless bracket
|
|
24
|
-
|
|
25
|
-
label = LogUtils::ProcessLabel.for(pod: bracket[:pod], namespace: @app, container: bracket[:container])
|
|
26
|
-
@colorizer.format_line(ts: ts, label: label, message: message)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def pick_bracket_and_timestamp(first, second)
|
|
32
|
-
if (bracket = BRACKET_RE.match(first))
|
|
33
|
-
[ bracket, second ]
|
|
34
|
-
elsif (bracket = BRACKET_RE.match(second))
|
|
35
|
-
[ bracket, first ]
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|