captive-cli 1.3.0 → 1.5.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 +2 -0
- data/lib/captive/cli.rb +13 -9
- data/lib/captive/commands/addons_commands.rb +56 -0
- data/lib/captive/commands/domain_announcer.rb +32 -0
- data/lib/captive/commands/domain_wait.rb +44 -0
- data/lib/captive/commands/domains_commands.rb +108 -0
- data/lib/captive/commands/logs.rb +24 -13
- data/lib/captive/dashboard/addons_resource.rb +18 -0
- data/lib/captive/dashboard/app_path.rb +14 -0
- data/lib/captive/dashboard/domains_resource.rb +27 -0
- data/lib/captive/dashboard/env_vars_resource.rb +3 -4
- data/lib/captive/dashboard/scheduled_tasks_resource.rb +3 -4
- data/lib/captive/errors.rb +1 -0
- data/lib/captive/kubectl.rb +0 -38
- data/lib/captive/loader.rb +7 -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/domain.rb +51 -0
- data/lib/captive/shell_completions.rb +5 -1
- data/lib/captive/sse_parser.rb +26 -0
- data/lib/captive/version.rb +1 -1
- metadata +12 -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: 415acf4d1e664b24fe084bd17b74fbbb5922f64465e37a360464b138dcc58bab
|
|
4
|
+
data.tar.gz: 48ea97307c38ab7db3f28ce3da3950f876aacc8fb528effd46758cf4b35131a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2cf47f38a58c871f6b8aae1e92b168ee95068ee333b9f93f1bfdba0161eb5c05615f2031e97de47091459c21ebbc9a5b1e061752b9d4d373d6e66fd0a1495dde
|
|
7
|
+
data.tar.gz: 897af0a7bd2e7685c143a3b2b69b2c4a4dcea02cae52fce02b1ff6c7677be1da310d8f2db9022d6840b7cb4d3f670ae2c441053f7bec717c6a1f609008b06d88
|
|
@@ -24,6 +24,8 @@ module Captive
|
|
|
24
24
|
def apps_api = @apps_api ||= Dashboard::AppsResource.new(api: dashboard)
|
|
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
|
+
def domains_api = @domains_api ||= Dashboard::DomainsResource.new(api: dashboard)
|
|
28
|
+
def addons_api = @addons_api ||= Dashboard::AddonsResource.new(api: dashboard)
|
|
27
29
|
|
|
28
30
|
def active_kubeconfig
|
|
29
31
|
env_kc = ENV["KUBECONFIG"]
|
data/lib/captive/cli.rb
CHANGED
|
@@ -6,6 +6,8 @@ require_relative "cli/dependencies"
|
|
|
6
6
|
require_relative "commands/apps_commands"
|
|
7
7
|
require_relative "commands/env_commands"
|
|
8
8
|
require_relative "commands/crons_commands"
|
|
9
|
+
require_relative "commands/domains_commands"
|
|
10
|
+
require_relative "commands/addons_commands"
|
|
9
11
|
require_relative "commands/db_commands"
|
|
10
12
|
require_relative "commands/user_login"
|
|
11
13
|
require_relative "commands/auth_commands"
|
|
@@ -62,6 +64,12 @@ module Captive
|
|
|
62
64
|
desc "crons SUBCOMMAND", "Tâches planifiées d'une app"
|
|
63
65
|
subcommand "crons", Commands::CronsCommands
|
|
64
66
|
|
|
67
|
+
desc "domains SUBCOMMAND", "Domaines d'une app"
|
|
68
|
+
subcommand "domains", Commands::DomainsCommands
|
|
69
|
+
|
|
70
|
+
desc "addons SUBCOMMAND", "Add-ons d'une app (bases, Supabase…)"
|
|
71
|
+
subcommand "addons", Commands::AddonsCommands
|
|
72
|
+
|
|
65
73
|
desc "auth SUBCOMMAND", "Jeton pour kubectl", hide: true
|
|
66
74
|
subcommand "auth", Commands::AuthCommands
|
|
67
75
|
|
|
@@ -142,13 +150,12 @@ module Captive
|
|
|
142
150
|
|
|
143
151
|
desc "logs APP", "Affiche les logs de l'app"
|
|
144
152
|
long_desc <<~DESC
|
|
145
|
-
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.
|
|
146
154
|
-f, --follow Suit en continu (Ctrl+C pour arrêter)
|
|
147
155
|
-n, --lines N Nombre de lignes affichées (défaut : 100)
|
|
148
|
-
-
|
|
149
|
-
-w, --worker Cible le pod worker au lieu du pod web
|
|
156
|
+
-w, --worker Affiche le worker au lieu du web
|
|
150
157
|
--raw Garde les lignes rack-timeout de la readinessProbe
|
|
151
|
-
|
|
158
|
+
Il suffit d'être Membre de l'Organization de l'app, sans accès au repo GitHub.
|
|
152
159
|
|
|
153
160
|
MODE HISTORIQUE (--from / --to) — interroge Loki via le dashboard.
|
|
154
161
|
--from DATE Début de la plage (requis pour ce mode)
|
|
@@ -161,14 +168,12 @@ module Captive
|
|
|
161
168
|
\x05 captive logs my-app
|
|
162
169
|
\x05 captive logs my-app -f
|
|
163
170
|
\x05 captive logs my-app -n 200
|
|
164
|
-
\x05 captive logs my-app -c migrate
|
|
165
171
|
\x05 captive logs my-app --worker
|
|
166
172
|
\x05 captive logs my-app --raw
|
|
167
173
|
\x05 captive logs my-app --from "2026-08-31 23:00" --to "2026-09-01 00:00"
|
|
168
174
|
\x05 captive logs my-app --from "2026-08-31 23:00"
|
|
169
175
|
DESC
|
|
170
176
|
method_option :follow, aliases: "-f", type: :boolean, default: false
|
|
171
|
-
method_option :container, aliases: "-c", type: :string
|
|
172
177
|
method_option :worker, aliases: "-w", type: :boolean, default: false
|
|
173
178
|
method_option :lines, aliases: "-n", type: :numeric, default: 100
|
|
174
179
|
method_option :raw, type: :boolean, default: false, desc: "Affiche le bruit rack-timeout des sondes readiness"
|
|
@@ -177,9 +182,8 @@ module Captive
|
|
|
177
182
|
def logs(app)
|
|
178
183
|
from = parse_time(options[:from])
|
|
179
184
|
to = parse_time(options[:to])
|
|
180
|
-
Commands::Logs.new
|
|
181
|
-
app, follow: options[:follow],
|
|
182
|
-
worker: options[:worker], lines: options[:lines],
|
|
185
|
+
Commands::Logs.new.run(
|
|
186
|
+
app, follow: options[:follow], worker: options[:worker], lines: options[:lines],
|
|
183
187
|
from: from, to: to, raw: options[:raw]
|
|
184
188
|
)
|
|
185
189
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
private
|
|
52
|
+
|
|
53
|
+
def presenter = Presenter::Addon.new(paint: paint)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
5
|
+
module Captive
|
|
6
|
+
module Commands
|
|
7
|
+
# Dit ce qu'est devenu un geste sur un Domaine. Sans attente, la demande est seulement
|
|
8
|
+
# acceptée ; avec une attente, le bloc la reçoit et rend le Domaine appliqué.
|
|
9
|
+
class DomainAnnouncer
|
|
10
|
+
def initialize(paint:, presenter:, waiter: nil)
|
|
11
|
+
@paint = paint
|
|
12
|
+
@presenter = presenter
|
|
13
|
+
@waiter = waiter
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(message)
|
|
17
|
+
return puts(@paint.green(" ✓ #{message} : #{CLIDependencies::ASYNC_NOTICE}")) unless @waiter
|
|
18
|
+
|
|
19
|
+
applied(message, yield(@waiter))
|
|
20
|
+
rescue PollTimeout => timeout
|
|
21
|
+
abort @paint.red("✗ Timeout : #{timeout.message}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def applied(message, entry)
|
|
27
|
+
puts @paint.green(" ✓ #{message} : appliqué")
|
|
28
|
+
puts " #{@presenter.line(entry)}" if entry
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../poller"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
|
|
6
|
+
module Captive
|
|
7
|
+
module Commands
|
|
8
|
+
# Attend que le cluster reflète un geste sur un Domaine : ni en attente ni échoué, et
|
|
9
|
+
# présent (absent après une suppression). Le DNS n'entre pas en compte : il dépend du
|
|
10
|
+
# propriétaire du Domaine, pas de Captive.
|
|
11
|
+
class DomainWait
|
|
12
|
+
def initialize(domains:, poller: nil, timeout: 600)
|
|
13
|
+
@domains = domains
|
|
14
|
+
@poller = poller || Poller.new(interval: 10, timeout: timeout)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Rend le Domaine tel que le cluster le montre une fois le geste appliqué.
|
|
18
|
+
def until_applied(app, hostname)
|
|
19
|
+
entry = nil
|
|
20
|
+
poll(app, hostname) { |found| (entry = found) && !found["pending"] }
|
|
21
|
+
entry
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def until_gone(app, hostname)
|
|
25
|
+
poll(app, hostname, &:nil?)
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def poll(app, hostname)
|
|
32
|
+
@poller.poll_until(context: "#{hostname} sur #{app}") do
|
|
33
|
+
yield check(hostname, @domains.find(app, hostname))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def check(hostname, entry)
|
|
38
|
+
return entry unless entry&.fetch("failed", false)
|
|
39
|
+
|
|
40
|
+
raise DomainChangeFailed, "#{hostname} : #{entry["failure_reason"] || "échec"}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Captive
|
|
4
|
+
module Commands
|
|
5
|
+
# Les Domaines d'une app via le dashboard. Chaque geste est appliqué plus tard par
|
|
6
|
+
# captive-platform ; --wait attend que le cluster le reflète.
|
|
7
|
+
class DomainsCommands < Thor
|
|
8
|
+
include CLIDependencies
|
|
9
|
+
|
|
10
|
+
def self.exit_on_failure? = true
|
|
11
|
+
|
|
12
|
+
WAIT_OPTIONS = {
|
|
13
|
+
wait: { type: :boolean, desc: "Attend que le changement soit appliqué" },
|
|
14
|
+
timeout: { type: :numeric, default: 600, desc: "Attente max en secondes avec --wait" },
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def self.wait_options
|
|
18
|
+
WAIT_OPTIONS.each { |name, settings| method_option(name, **settings) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc "list APP", "Liste les domaines de l'app et l'état de leur DNS"
|
|
22
|
+
long_desc <<~DESC
|
|
23
|
+
\x05Exemple :
|
|
24
|
+
\x05 captive domains list my-app
|
|
25
|
+
\x05 captive domains list my-app --refresh
|
|
26
|
+
DESC
|
|
27
|
+
method_option :refresh, type: :boolean, desc: "Vérifie à nouveau le DNS sans attendre le cache"
|
|
28
|
+
def list(app)
|
|
29
|
+
entries = options[:refresh] ? domains_api.recheck(app) : domains_api.list(app)
|
|
30
|
+
return puts(paint.dim("Aucun domaine.")) if entries.empty?
|
|
31
|
+
|
|
32
|
+
entries.each { |entry| puts presenter.line(entry) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc "add APP DOMAINE", "Ajoute un domaine à l'app"
|
|
36
|
+
long_desc <<~DESC
|
|
37
|
+
L'enregistrement DNS à créer s'affiche ensuite dans `captive domains list`.
|
|
38
|
+
|
|
39
|
+
\x05Exemple :
|
|
40
|
+
\x05 captive domains add my-app www.example.fr --wait
|
|
41
|
+
DESC
|
|
42
|
+
wait_options
|
|
43
|
+
def add(app, hostname)
|
|
44
|
+
domains_api.add(app, hostname)
|
|
45
|
+
announcer.call("#{hostname} ajouté") { |wait| wait.until_applied(app, hostname) }
|
|
46
|
+
puts paint.dim(" DNS à créer : captive domains list #{app}") unless options[:wait]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "rm APP DOMAINE", "Supprime un domaine de l'app"
|
|
50
|
+
long_desc <<~DESC
|
|
51
|
+
Le domaine cesse aussitôt de servir l'app : il faut le retaper pour confirmer.
|
|
52
|
+
|
|
53
|
+
\x05Exemple :
|
|
54
|
+
\x05 captive domains rm my-app www.example.fr
|
|
55
|
+
\x05 captive domains rm my-app www.example.fr --yes
|
|
56
|
+
DESC
|
|
57
|
+
method_option :yes, type: :boolean, desc: "Supprime sans demander de confirmation"
|
|
58
|
+
wait_options
|
|
59
|
+
def rm(app, hostname)
|
|
60
|
+
confirm_removal(hostname)
|
|
61
|
+
domains_api.remove(app, hostname)
|
|
62
|
+
announcer.call("#{hostname} supprimé") { |wait| wait.until_gone(app, hostname) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
desc "redirect APP DOMAINE", "Redirige un domaine vers un autre domaine de l'app"
|
|
66
|
+
long_desc <<~DESC
|
|
67
|
+
\x05Exemple :
|
|
68
|
+
\x05 captive domains redirect my-app example.fr --to www.example.fr
|
|
69
|
+
DESC
|
|
70
|
+
method_option :to, type: :string, required: true, desc: "Domaine servi vers lequel rediriger"
|
|
71
|
+
method_option :status, type: :numeric, default: 301, enum: [ 301, 302 ], desc: "Code HTTP de redirection"
|
|
72
|
+
wait_options
|
|
73
|
+
def redirect(app, hostname)
|
|
74
|
+
target = options[:to]
|
|
75
|
+
domains_api.redirect(app, hostname, { to: target, status: options[:status] })
|
|
76
|
+
announcer.call("#{hostname} redirigé vers #{target}") { |wait| wait.until_applied(app, hostname) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
desc "serve APP DOMAINE", "Repasse un domaine redirigé en domaine servi"
|
|
80
|
+
long_desc <<~DESC
|
|
81
|
+
\x05Exemple :
|
|
82
|
+
\x05 captive domains serve my-app example.fr
|
|
83
|
+
DESC
|
|
84
|
+
wait_options
|
|
85
|
+
def serve(app, hostname)
|
|
86
|
+
domains_api.serve(app, hostname)
|
|
87
|
+
announcer.call("#{hostname} servi") { |wait| wait.until_applied(app, hostname) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def presenter = Presenter::Domain.new(paint: paint)
|
|
93
|
+
def domain_wait = DomainWait.new(domains: domains_api, timeout: options[:timeout])
|
|
94
|
+
|
|
95
|
+
def announcer
|
|
96
|
+
DomainAnnouncer.new(paint: paint, presenter: presenter, waiter: (domain_wait if options[:wait]))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def confirm_removal(hostname)
|
|
100
|
+
return if options[:yes]
|
|
101
|
+
return if ask("Retape #{hostname} pour confirmer la suppression :").to_s.strip == hostname
|
|
102
|
+
|
|
103
|
+
warn paint.red("Abandon : rien n'a été supprimé.")
|
|
104
|
+
exit 1
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
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,18 @@
|
|
|
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
|
+
|
|
10
|
+
def initialize(api:)
|
|
11
|
+
@api = api
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def list(app) = @api.get(PATH.call(app))
|
|
15
|
+
def add(app, addon) = @api.post(PATH.call(app), { addon: addon })
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Captive
|
|
4
|
+
module Dashboard
|
|
5
|
+
# Le chemin d'une collection rattachée à une app (`/apps/:app/<collection>`), et d'un de
|
|
6
|
+
# ses éléments, chaque segment encodé.
|
|
7
|
+
AppPath = Data.define(:collection) do
|
|
8
|
+
def call(app, key = nil)
|
|
9
|
+
base = "/apps/#{DashboardApi.segment(app)}/#{collection}"
|
|
10
|
+
key ? "#{base}/#{DashboardApi.segment(key)}" : base
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Captive
|
|
4
|
+
module Dashboard
|
|
5
|
+
# Les Domaines d'une app : lus du cluster, modifiés via captive-platform (réponse 202).
|
|
6
|
+
# `recheck` relit le DNS sans attendre le cache ; une redirection est `{ to:, status: }`.
|
|
7
|
+
class DomainsResource
|
|
8
|
+
PATH = AppPath.new("domains")
|
|
9
|
+
|
|
10
|
+
def initialize(api:)
|
|
11
|
+
@api = api
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def list(app) = @api.get(path(app))
|
|
15
|
+
def recheck(app) = @api.get("#{path(app)}?refresh=1")
|
|
16
|
+
def find(app, hostname) = list(app).find { |domain| domain["hostname"] == hostname }
|
|
17
|
+
def add(app, hostname) = @api.post(path(app), { hostname: hostname })
|
|
18
|
+
def remove(app, hostname) = @api.delete(path(app, hostname))
|
|
19
|
+
def serve(app, hostname) = @api.patch(path(app, hostname), { mode: "serve" })
|
|
20
|
+
def redirect(app, hostname, rule) = @api.patch(path(app, hostname), rule)
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def path(...) = PATH.call(...)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -4,6 +4,8 @@ module Captive
|
|
|
4
4
|
module Dashboard
|
|
5
5
|
# Les variables d'environnement d'une app : lues du Secret, écrites par set-env.
|
|
6
6
|
class EnvVarsResource
|
|
7
|
+
PATH = AppPath.new("env_vars")
|
|
8
|
+
|
|
7
9
|
def initialize(api:)
|
|
8
10
|
@api = api
|
|
9
11
|
end
|
|
@@ -18,10 +20,7 @@ module Captive
|
|
|
18
20
|
|
|
19
21
|
private
|
|
20
22
|
|
|
21
|
-
def path(
|
|
22
|
-
base = "/apps/#{DashboardApi.segment(app)}/env_vars"
|
|
23
|
-
key ? "#{base}/#{DashboardApi.segment(key)}" : base
|
|
24
|
-
end
|
|
23
|
+
def path(...) = PATH.call(...)
|
|
25
24
|
end
|
|
26
25
|
end
|
|
27
26
|
end
|
|
@@ -4,6 +4,8 @@ module Captive
|
|
|
4
4
|
module Dashboard
|
|
5
5
|
# Les tâches planifiées d'une app : lues du cluster, déclarées via captive-platform.
|
|
6
6
|
class ScheduledTasksResource
|
|
7
|
+
PATH = AppPath.new("scheduled_tasks")
|
|
8
|
+
|
|
7
9
|
def initialize(api:)
|
|
8
10
|
@api = api
|
|
9
11
|
end
|
|
@@ -23,10 +25,7 @@ module Captive
|
|
|
23
25
|
|
|
24
26
|
private
|
|
25
27
|
|
|
26
|
-
def path(
|
|
27
|
-
base = "/apps/#{DashboardApi.segment(app)}/scheduled_tasks"
|
|
28
|
-
name ? "#{base}/#{DashboardApi.segment(name)}" : base
|
|
29
|
-
end
|
|
28
|
+
def path(...) = PATH.call(...)
|
|
30
29
|
end
|
|
31
30
|
end
|
|
32
31
|
end
|
data/lib/captive/errors.rb
CHANGED
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
|
@@ -13,11 +13,16 @@ require_relative "one_off_spec"
|
|
|
13
13
|
require_relative "session_id"
|
|
14
14
|
require_relative "poller"
|
|
15
15
|
require_relative "dashboard_api"
|
|
16
|
+
require_relative "dashboard/app_path"
|
|
16
17
|
require_relative "dashboard/apps_resource"
|
|
17
18
|
require_relative "dashboard/env_vars_resource"
|
|
18
19
|
require_relative "dashboard/scheduled_tasks_resource"
|
|
20
|
+
require_relative "dashboard/domains_resource"
|
|
21
|
+
require_relative "dashboard/addons_resource"
|
|
19
22
|
require_relative "presenter/app_info"
|
|
20
23
|
require_relative "presenter/scheduled_task"
|
|
24
|
+
require_relative "presenter/domain"
|
|
25
|
+
require_relative "presenter/addon"
|
|
21
26
|
require_relative "activity_trail"
|
|
22
27
|
require_relative "errors"
|
|
23
28
|
require_relative "ui/spinner"
|
|
@@ -28,8 +33,7 @@ require_relative "commands/one_off_session"
|
|
|
28
33
|
require_relative "commands/run"
|
|
29
34
|
require_relative "commands/logs"
|
|
30
35
|
require_relative "commands/apps_restart"
|
|
36
|
+
require_relative "commands/domain_wait"
|
|
37
|
+
require_relative "commands/domain_announcer"
|
|
31
38
|
require_relative "commands/db_dump"
|
|
32
39
|
require_relative "commands/db_restore"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
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,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Captive
|
|
4
|
+
module Presenter
|
|
5
|
+
# Une ligne par Domaine : son nom, son état (servi, redirigé, en attente, échoué) et ce que
|
|
6
|
+
# dit le DNS. Un geste en cours masque le diagnostic, qui ne vaut que pour l'état atteint.
|
|
7
|
+
class Domain
|
|
8
|
+
HEALTH = { "certificate_pending" => "certificat en cours", "invalid" => "route refusée" }.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(paint:)
|
|
11
|
+
@paint = paint
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def line(entry)
|
|
15
|
+
change = change(entry)
|
|
16
|
+
[ entry["hostname"], change || reached(entry), (health(entry) unless change) ].compact.join(" ")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# Un geste en attente ou échoué ; nil quand le Domaine est dans l'état demandé.
|
|
22
|
+
def change(entry)
|
|
23
|
+
return failure(entry) if entry["failed"]
|
|
24
|
+
|
|
25
|
+
@paint.yellow("en attente (#{entry["pending_change"]})") if entry["pending"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reached(entry)
|
|
29
|
+
redirect = entry["redirect"]
|
|
30
|
+
return "→ #{redirect["to"]} (#{redirect["status"]})" if redirect
|
|
31
|
+
|
|
32
|
+
entry["primary"] ? "servi, principal" : "servi"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def failure(entry)
|
|
36
|
+
reason = entry["failure_reason"]
|
|
37
|
+
@paint.red(reason ? "échoué : #{reason}" : "échoué")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def health(entry)
|
|
41
|
+
status = entry["health"]
|
|
42
|
+
return @paint.green("✓") if status == "valid"
|
|
43
|
+
|
|
44
|
+
expected = entry.dig("dns", "expected")
|
|
45
|
+
return @paint.red("DNS à créer : #{expected.values_at("type", "name").join(" ")} → #{expected["value"]}") if expected
|
|
46
|
+
|
|
47
|
+
@paint.yellow(HEALTH.fetch(status, status.to_s))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Captive
|
|
4
4
|
module ShellCompletions
|
|
5
|
-
ROOT_COMMANDS = %w[apps env crons 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
|
+
"domains" => %w[list add rm redirect serve],
|
|
12
|
+
"addons" => %w[list catalog add],
|
|
11
13
|
"db" => %w[restore],
|
|
12
14
|
}.freeze
|
|
13
15
|
|
|
@@ -23,6 +25,8 @@ module Captive
|
|
|
23
25
|
apps) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["apps"].join(" ")}" -- "$cur")); return ;;
|
|
24
26
|
env) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["env"].join(" ")}" -- "$cur")); return ;;
|
|
25
27
|
crons) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["crons"].join(" ")}" -- "$cur")); return ;;
|
|
28
|
+
domains) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["domains"].join(" ")}" -- "$cur")); return ;;
|
|
29
|
+
addons) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["addons"].join(" ")}" -- "$cur")); return ;;
|
|
26
30
|
db) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["db"].join(" ")}" -- "$cur")); return ;;
|
|
27
31
|
esac
|
|
28
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.5.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
|
|
@@ -120,6 +121,9 @@ files:
|
|
|
120
121
|
- lib/captive/commands/db_psql.rb
|
|
121
122
|
- lib/captive/commands/db_restore.rb
|
|
122
123
|
- lib/captive/commands/db_target.rb
|
|
124
|
+
- lib/captive/commands/domain_announcer.rb
|
|
125
|
+
- lib/captive/commands/domain_wait.rb
|
|
126
|
+
- lib/captive/commands/domains_commands.rb
|
|
123
127
|
- lib/captive/commands/env_commands.rb
|
|
124
128
|
- lib/captive/commands/env_set.rb
|
|
125
129
|
- lib/captive/commands/logs.rb
|
|
@@ -128,7 +132,10 @@ files:
|
|
|
128
132
|
- lib/captive/commands/user_login.rb
|
|
129
133
|
- lib/captive/credentials.rb
|
|
130
134
|
- lib/captive/credentials_store.rb
|
|
135
|
+
- lib/captive/dashboard/addons_resource.rb
|
|
136
|
+
- lib/captive/dashboard/app_path.rb
|
|
131
137
|
- lib/captive/dashboard/apps_resource.rb
|
|
138
|
+
- lib/captive/dashboard/domains_resource.rb
|
|
132
139
|
- lib/captive/dashboard/env_vars_resource.rb
|
|
133
140
|
- lib/captive/dashboard/scheduled_tasks_resource.rb
|
|
134
141
|
- lib/captive/dashboard_api.rb
|
|
@@ -141,17 +148,20 @@ files:
|
|
|
141
148
|
- lib/captive/kubectl.rb
|
|
142
149
|
- lib/captive/loader.rb
|
|
143
150
|
- lib/captive/log_noise_filter.rb
|
|
151
|
+
- lib/captive/log_stream.rb
|
|
144
152
|
- lib/captive/null_log_filter.rb
|
|
145
153
|
- lib/captive/one_off_spec.rb
|
|
146
154
|
- lib/captive/poller.rb
|
|
155
|
+
- lib/captive/presenter/addon.rb
|
|
147
156
|
- lib/captive/presenter/ansi_stripper.rb
|
|
148
157
|
- lib/captive/presenter/app_info.rb
|
|
158
|
+
- lib/captive/presenter/domain.rb
|
|
149
159
|
- lib/captive/presenter/log_colorizer.rb
|
|
150
|
-
- lib/captive/presenter/log_line.rb
|
|
151
160
|
- lib/captive/presenter/scheduled_task.rb
|
|
152
161
|
- lib/captive/session_id.rb
|
|
153
162
|
- lib/captive/session_token.rb
|
|
154
163
|
- lib/captive/shell_completions.rb
|
|
164
|
+
- lib/captive/sse_parser.rb
|
|
155
165
|
- lib/captive/terminal.rb
|
|
156
166
|
- lib/captive/token_refresher.rb
|
|
157
167
|
- 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
|