captive-cli 1.0.0 → 1.3.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 +12 -2
- data/lib/captive/cli.rb +30 -13
- data/lib/captive/commands/apps_commands.rb +25 -50
- data/lib/captive/commands/apps_restart.rb +23 -9
- data/lib/captive/commands/auth_commands.rb +16 -0
- data/lib/captive/commands/auth_token.rb +29 -0
- data/lib/captive/commands/cron_declaration.rb +25 -0
- data/lib/captive/commands/crons_commands.rb +123 -0
- data/lib/captive/commands/env_commands.rb +66 -0
- data/lib/captive/commands/env_set.rb +40 -0
- data/lib/captive/commands/user_login.rb +20 -13
- data/lib/captive/credentials.rb +43 -0
- data/lib/captive/credentials_store.rb +47 -0
- data/lib/captive/dashboard/apps_resource.rb +26 -0
- data/lib/captive/dashboard/env_vars_resource.rb +27 -0
- data/lib/captive/dashboard/scheduled_tasks_resource.rb +32 -0
- data/lib/captive/dashboard_api.rb +58 -0
- data/lib/captive/dashboard_client.rb +25 -25
- data/lib/captive/dashboard_response.rb +39 -0
- data/lib/captive/errors.rb +14 -3
- data/lib/captive/flux.rb +11 -2
- data/lib/captive/github_device_flow.rb +5 -3
- data/lib/captive/kubectl.rb +0 -10
- data/lib/captive/loader.rb +6 -4
- data/lib/captive/presenter/app_info.rb +40 -0
- data/lib/captive/presenter/scheduled_task.rb +36 -0
- data/lib/captive/session_token.rb +60 -0
- data/lib/captive/shell_completions.rb +4 -2
- data/lib/captive/token_refresher.rb +27 -0
- data/lib/captive/validation_message.rb +26 -0
- data/lib/captive/version.rb +1 -1
- metadata +19 -8
- data/lib/captive/cluster_apps.rb +0 -82
- data/lib/captive/commands/apps_info.rb +0 -70
- data/lib/captive/commands/apps_rebuild.rb +0 -52
- data/lib/captive/commands/env_kube_commands.rb +0 -25
- data/lib/captive/commands/env_list_kube.rb +0 -16
- data/lib/captive/gh_cli.rb +0 -89
- data/lib/captive/platform_root.rb +0 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f79f7992c692e80419a6da77317d0e3d50d72ee4b48d41ea6001fa3e8a968400
|
|
4
|
+
data.tar.gz: 43220b3bb3e0b9cdaae4bf96de10d80eb3145af3f52584bff1c73fb45d4e7349
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cec86c2467edb73bb4c46c520c2fb793c1c4b67800213d5ef68348aa2ddae4c91b25e85d82fe73e599fc7cb188aee96b63feb5674b10885e7aa7100027f240ac
|
|
7
|
+
data.tar.gz: a69f88f24623b2329515daff60be1df8d3092046c51cd20b995cb6a0ba58a176dc29898640a07cdc6d9904315b10a321b7d7f6cc9cb7c301e768e991d55a3980
|
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Captive
|
|
4
4
|
module CLIDependencies
|
|
5
|
+
# Les écritures du dashboard sont appliquées plus tard par captive-platform (réponse 202).
|
|
6
|
+
ASYNC_NOTICE = "demande envoyée, appliquée sous quelques minutes"
|
|
7
|
+
|
|
5
8
|
private
|
|
6
9
|
|
|
7
10
|
def kubectl
|
|
8
11
|
@kubectl ||= begin
|
|
9
12
|
raise NotAuthenticatedError, "Pas de session active. Lance : captive login" if active_kubeconfig.nil?
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
unless binary_on_path?("kubectl")
|
|
15
|
+
raise MissingDependencyError, "kubectl introuvable. Installe-le : brew install kubernetes-cli"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Kubectl.new(kubeconfig: active_kubeconfig)
|
|
12
19
|
end
|
|
13
20
|
end
|
|
14
|
-
def gh_cli = @gh_cli ||= GhCLI.new
|
|
15
21
|
def trail = @trail ||= ActivityTrail.new
|
|
16
22
|
def paint = @paint ||= Pastel.new
|
|
23
|
+
def dashboard = @dashboard ||= DashboardApi.new
|
|
24
|
+
def apps_api = @apps_api ||= Dashboard::AppsResource.new(api: dashboard)
|
|
25
|
+
def env_api = @env_api ||= Dashboard::EnvVarsResource.new(api: dashboard)
|
|
26
|
+
def crons_api = @crons_api ||= Dashboard::ScheduledTasksResource.new(api: dashboard)
|
|
17
27
|
|
|
18
28
|
def active_kubeconfig
|
|
19
29
|
env_kc = ENV["KUBECONFIG"]
|
data/lib/captive/cli.rb
CHANGED
|
@@ -4,9 +4,12 @@ require "time"
|
|
|
4
4
|
require_relative "loader"
|
|
5
5
|
require_relative "cli/dependencies"
|
|
6
6
|
require_relative "commands/apps_commands"
|
|
7
|
-
require_relative "commands/
|
|
7
|
+
require_relative "commands/env_commands"
|
|
8
|
+
require_relative "commands/crons_commands"
|
|
8
9
|
require_relative "commands/db_commands"
|
|
9
10
|
require_relative "commands/user_login"
|
|
11
|
+
require_relative "commands/auth_commands"
|
|
12
|
+
require_relative "credentials_store"
|
|
10
13
|
require_relative "dashboard_client"
|
|
11
14
|
require_relative "github_device_flow"
|
|
12
15
|
require_relative "cluster_endpoint"
|
|
@@ -22,15 +25,9 @@ module Captive
|
|
|
22
25
|
def self.start(args = ARGV, config = {})
|
|
23
26
|
args = [ "help", args.first ] if args.length == 2 && %w[--help -h].include?(args.last)
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
when :blocked
|
|
27
|
-
warn "Version #{VERSION} n'est plus supportée. Mettez à jour : brew upgrade captive-studio/tap/captive-cli"
|
|
28
|
-
exit 1
|
|
29
|
-
when :update_available
|
|
30
|
-
warn "⚠ Nouvelle version disponible. Mettez à jour : brew upgrade captive-studio/tap/captive-cli"
|
|
31
|
-
end
|
|
28
|
+
check_for_update unless args.first(2) == %w[auth token]
|
|
32
29
|
super
|
|
33
|
-
rescue AuthError, NotAuthenticatedError, NoTtyError => e
|
|
30
|
+
rescue AuthError, NotAuthenticatedError, NoTtyError, MissingDependencyError, DashboardError => e
|
|
34
31
|
warn e.message
|
|
35
32
|
exit 1
|
|
36
33
|
rescue StandardError => e
|
|
@@ -39,6 +36,19 @@ module Captive
|
|
|
39
36
|
exit 1
|
|
40
37
|
end
|
|
41
38
|
|
|
39
|
+
# kubectl appelle `captive auth token` à chaque commande : ni requête réseau en plus, ni
|
|
40
|
+
# avertissement à afficher.
|
|
41
|
+
def self.check_for_update
|
|
42
|
+
case UpdateChecker.new.check
|
|
43
|
+
when :blocked
|
|
44
|
+
warn "Version #{VERSION} n'est plus supportée. Mettez à jour : brew upgrade captive-studio/tap/captive-cli"
|
|
45
|
+
exit 1
|
|
46
|
+
when :update_available
|
|
47
|
+
warn "⚠ Nouvelle version disponible. Mettez à jour : brew upgrade captive-studio/tap/captive-cli"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
private_class_method :check_for_update
|
|
51
|
+
|
|
42
52
|
# ── Subcommands ─────────────────────────────────────────────────────────
|
|
43
53
|
desc "apps SUBCOMMAND", "Gestion des apps"
|
|
44
54
|
subcommand "apps", Commands::AppsCommands
|
|
@@ -47,7 +57,13 @@ module Captive
|
|
|
47
57
|
subcommand "db", Commands::DbCommands
|
|
48
58
|
|
|
49
59
|
desc "env SUBCOMMAND", "Variables d'environnement d'une app"
|
|
50
|
-
subcommand "env", Commands::
|
|
60
|
+
subcommand "env", Commands::EnvCommands
|
|
61
|
+
|
|
62
|
+
desc "crons SUBCOMMAND", "Tâches planifiées d'une app"
|
|
63
|
+
subcommand "crons", Commands::CronsCommands
|
|
64
|
+
|
|
65
|
+
desc "auth SUBCOMMAND", "Jeton pour kubectl", hide: true
|
|
66
|
+
subcommand "auth", Commands::AuthCommands
|
|
51
67
|
|
|
52
68
|
# ── Root commands ────────────────────────────────────────────────────────
|
|
53
69
|
map "--version" => :version
|
|
@@ -65,7 +81,7 @@ module Captive
|
|
|
65
81
|
Commands::UserLogin.new(
|
|
66
82
|
github: GithubDeviceFlow.new,
|
|
67
83
|
cluster: ClusterEndpoint.new,
|
|
68
|
-
|
|
84
|
+
store: CredentialsStore.new,
|
|
69
85
|
kubeconfig_path: File.expand_path("~/.config/captive/kubeconfig")
|
|
70
86
|
).call
|
|
71
87
|
puts "Authentifié avec succès."
|
|
@@ -102,9 +118,10 @@ module Captive
|
|
|
102
118
|
exit(exit_code) if exit_code && exit_code != 0
|
|
103
119
|
end
|
|
104
120
|
|
|
105
|
-
desc "whoami", "Affiche l'
|
|
121
|
+
desc "whoami", "Affiche l'utilisateur connecté au dashboard"
|
|
106
122
|
def whoami
|
|
107
|
-
|
|
123
|
+
me = dashboard.get("/me")
|
|
124
|
+
puts [ me["login"], ("(#{me["name"]})" if me["name"]) ].compact.join(" ")
|
|
108
125
|
end
|
|
109
126
|
|
|
110
127
|
desc "completions SHELL", "Génère le script de complétion shell (bash ou zsh)"
|
|
@@ -5,99 +5,74 @@ module Captive
|
|
|
5
5
|
class AppsCommands < Thor
|
|
6
6
|
include CLIDependencies
|
|
7
7
|
|
|
8
|
+
STAGE_BADGES = { "staging" => [ :yellow, "[staging]" ], "production" => [ :green, "[prod]" ] }.freeze
|
|
9
|
+
|
|
8
10
|
def self.exit_on_failure? = true
|
|
9
11
|
|
|
10
|
-
desc "list", "Liste
|
|
12
|
+
desc "list", "Liste les apps auxquelles tu as accès"
|
|
11
13
|
long_desc <<~DESC
|
|
12
|
-
Liste les apps
|
|
14
|
+
Liste les apps de tes Organizations, avec leur stage et leur URL (API du dashboard).
|
|
13
15
|
|
|
14
16
|
\x05Exemple :
|
|
15
17
|
\x05 captive apps list
|
|
16
18
|
DESC
|
|
17
19
|
def list
|
|
18
|
-
apps =
|
|
20
|
+
apps = apps_api.list
|
|
19
21
|
puts paint.bold("Apps (#{apps.size}) :")
|
|
20
22
|
apps.each do |app|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
when "production" then " " + paint.green("[prod]")
|
|
24
|
-
else ""
|
|
25
|
-
end
|
|
26
|
-
puts " - #{paint.bold(app[:name])}#{stage_badge}"
|
|
27
|
-
if app[:domains]&.any?
|
|
28
|
-
app[:domains].each { |d| puts " #{paint.cyan("https://#{d}")}" }
|
|
29
|
-
else
|
|
30
|
-
puts paint.dim(" <no domain>")
|
|
31
|
-
end
|
|
23
|
+
puts " - #{paint.bold(app["name"])}#{stage_badge(app["stage"])}"
|
|
24
|
+
puts app["app_url"] ? " #{paint.cyan(app["app_url"])}" : paint.dim(" <no domain>")
|
|
32
25
|
end
|
|
33
26
|
end
|
|
34
27
|
|
|
35
28
|
desc "info APP", "Affiche le statut et les infos de l'app"
|
|
36
29
|
long_desc <<~DESC
|
|
37
|
-
Affiche
|
|
30
|
+
Affiche stage, statut, URL, processus (replicas prêts, tier), datastores et domaines.
|
|
38
31
|
|
|
39
32
|
\x05Exemple :
|
|
40
33
|
\x05 captive apps info my-app
|
|
41
34
|
DESC
|
|
42
35
|
def info(app)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
puts
|
|
48
|
-
puts " Postgres : #{i[:postgres] ? paint.green("✓") : paint.dim("–")}"
|
|
49
|
-
puts " Redis : #{i[:redis] ? paint.green("✓") : paint.dim("–")}"
|
|
50
|
-
puts " IP fixe : #{i[:egress_ip] ? paint.green("✓") : paint.dim("–")}"
|
|
51
|
-
i[:domains]&.any? && i[:domains].each { |d| puts " → #{d}" }
|
|
52
|
-
i[:workers]&.any? && puts(" Workers : #{i[:workers].join(", ")}")
|
|
53
|
-
(pg = i[:postgres_details]) && puts(" Postgres : #{pg[:instances] || 1} instance(s), #{pg[:memory] || "–"} RAM")
|
|
54
|
-
(rd = i[:redis_details]) && puts(" Redis : #{rd[:memory] || "–"} RAM, #{rd[:storage] || "–"}")
|
|
55
|
-
(eg = i[:egress_ip_details]) && puts(" IP fixe : #{eg[:ip] || "–"}")
|
|
56
|
-
puts
|
|
36
|
+
lines = Presenter::AppInfo.new(paint: paint).lines(
|
|
37
|
+
app: apps_api.show(app), workloads: apps_api.workloads(app).fetch("workloads", []),
|
|
38
|
+
datastores: apps_api.datastores(app), domains: apps_api.domains(app)
|
|
39
|
+
)
|
|
40
|
+
puts lines
|
|
57
41
|
end
|
|
58
42
|
|
|
59
|
-
desc "restart APP", "Redémarre
|
|
43
|
+
desc "restart APP", "Redémarre les processus de l'app"
|
|
60
44
|
long_desc <<~DESC
|
|
61
|
-
|
|
45
|
+
Demande le redémarrage au dashboard, puis attend (120 s max) que tous les
|
|
46
|
+
processus soient de nouveau prêts.
|
|
62
47
|
|
|
63
48
|
\x05Exemple :
|
|
64
49
|
\x05 captive apps restart my-app
|
|
65
50
|
DESC
|
|
66
51
|
def restart(app)
|
|
67
|
-
AppsRestart.new(
|
|
68
|
-
pod_name, created_at = kubectl.pod_info(app, app)
|
|
69
|
-
age = format_age(created_at)
|
|
52
|
+
AppsRestart.new(apps: apps_api).run(app)
|
|
70
53
|
puts paint.green(" ✓ #{app} redémarré")
|
|
71
|
-
puts paint.dim(" pod: #{pod_name} (#{age})")
|
|
72
54
|
rescue PollTimeout => e
|
|
73
55
|
abort paint.red("✗ Timeout : #{e.message}")
|
|
74
|
-
rescue RuntimeError => e
|
|
75
|
-
abort paint.red("✗ #{e.message.split("failed: ").last}")
|
|
76
56
|
end
|
|
77
57
|
|
|
78
|
-
desc "rebuild APP", "
|
|
58
|
+
desc "rebuild APP", "Redéploie l'app sans changer le code"
|
|
79
59
|
long_desc <<~DESC
|
|
80
|
-
|
|
60
|
+
Relance le build et le déploiement de la version courante via le dashboard.
|
|
61
|
+
Utile après un changement de variables d'environnement.
|
|
81
62
|
|
|
82
63
|
\x05Exemple :
|
|
83
64
|
\x05 captive apps rebuild my-app-staging
|
|
84
65
|
DESC
|
|
85
66
|
def rebuild(app)
|
|
86
|
-
|
|
87
|
-
puts paint.green(" ✓
|
|
88
|
-
rescue WorkflowDispatchNotConfigured, ImageYamlNotFound, PlatformRootNotFound => e
|
|
89
|
-
abort paint.red("✗ #{e.message}")
|
|
67
|
+
apps_api.redeploy(app)
|
|
68
|
+
puts paint.green(" ✓ Redéploiement déclenché pour '#{app}'")
|
|
90
69
|
end
|
|
91
70
|
|
|
92
71
|
private
|
|
93
72
|
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
require "time"
|
|
98
|
-
"#{((Time.now - Time.parse(created_at)) / 60).round}s"
|
|
99
|
-
rescue ArgumentError
|
|
100
|
-
"?"
|
|
73
|
+
def stage_badge(stage)
|
|
74
|
+
color, label = STAGE_BADGES[stage]
|
|
75
|
+
color ? " #{paint.public_send(color, label)}" : ""
|
|
101
76
|
end
|
|
102
77
|
end
|
|
103
78
|
end
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "time"
|
|
3
4
|
require_relative "../poller"
|
|
4
5
|
require_relative "../errors"
|
|
5
6
|
|
|
6
7
|
module Captive
|
|
7
8
|
module Commands
|
|
9
|
+
# Demande le redémarrage au dashboard, puis attend que chaque workload ait tous ses
|
|
10
|
+
# replicas prêts, sur des pods créés après la demande : juste après le POST, les anciens
|
|
11
|
+
# pods sont encore prêts et le redémarrage paraîtrait déjà fini.
|
|
8
12
|
class AppsRestart
|
|
9
|
-
def initialize(
|
|
10
|
-
@
|
|
11
|
-
@poller
|
|
13
|
+
def initialize(apps:, poller: nil)
|
|
14
|
+
@apps = apps
|
|
15
|
+
@poller = poller || Poller.new(interval: 5, timeout: 120)
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
def run(app)
|
|
15
|
-
|
|
16
|
-
@
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
restarted_at = Time.parse(@apps.restart(app).fetch("restarted_at"))
|
|
20
|
+
@poller.poll_until(context: "redémarrage de #{app}") { restarted?(app, restarted_at) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def restarted?(app, restarted_at)
|
|
26
|
+
state = @apps.workloads(app)
|
|
27
|
+
workloads = state.fetch("workloads", [])
|
|
28
|
+
workloads.any? && workloads.all? { |w| w["ready"] == w["total"] } &&
|
|
29
|
+
state.fetch("pods", []).all? { |pod| fresh?(pod, restarted_at) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fresh?(pod, restarted_at)
|
|
33
|
+
created_at = pod["created_at"]
|
|
34
|
+
created_at.nil? || Time.parse(created_at.to_s) >= restarted_at
|
|
21
35
|
end
|
|
22
36
|
end
|
|
23
37
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "auth_token"
|
|
4
|
+
require_relative "../session_token"
|
|
5
|
+
|
|
6
|
+
module Captive
|
|
7
|
+
module Commands
|
|
8
|
+
# `captive auth` : ce dont kubectl a besoin pour s'authentifier, masqué de l'aide.
|
|
9
|
+
class AuthCommands < Thor
|
|
10
|
+
def self.exit_on_failure? = true
|
|
11
|
+
|
|
12
|
+
desc "token", "Imprime un ExecCredential pour kubectl (appelé par le kubeconfig)"
|
|
13
|
+
def token = AuthToken.new(session: SessionToken.new).call
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Captive
|
|
7
|
+
module Commands
|
|
8
|
+
# Le plugin d'authentification exec de kubectl : un ExecCredential sur stdout, et rien d'autre.
|
|
9
|
+
class AuthToken
|
|
10
|
+
def initialize(session:, out: $stdout)
|
|
11
|
+
@session = session
|
|
12
|
+
@out = out
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
@out.puts JSON.generate(exec_credential(@session.current))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def exec_credential(credentials)
|
|
22
|
+
expires_at = credentials.expires_at
|
|
23
|
+
status = { token: credentials.access_token }
|
|
24
|
+
status[:expirationTimestamp] = expires_at.utc.iso8601 if expires_at
|
|
25
|
+
{ apiVersion: "client.authentication.k8s.io/v1", kind: "ExecCredential", status: status }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Captive
|
|
4
|
+
module Commands
|
|
5
|
+
# Le corps d'une tâche planifiée à déclarer : les options données sur la ligne de
|
|
6
|
+
# commande, posées sur une base (les défauts du dashboard pour un ajout, la tâche
|
|
7
|
+
# actuelle pour une édition).
|
|
8
|
+
class CronDeclaration
|
|
9
|
+
DEFAULTS = { "schedule" => "", "timezone" => "Etc/UTC", "mode" => "http", "target" => "",
|
|
10
|
+
"timeout_seconds" => 60, "suspended" => false, }.freeze
|
|
11
|
+
OPTIONS = { schedule: "schedule", timezone: "timezone", mode: "mode", target: "target",
|
|
12
|
+
timeout: "timeout_seconds", suspended: "suspended", }.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(name, base: DEFAULTS)
|
|
15
|
+
@name = name
|
|
16
|
+
@base = base
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def with(options)
|
|
20
|
+
given = OPTIONS.filter_map { |option, field| [ field, options[option] ] unless options[option].nil? }
|
|
21
|
+
DEFAULTS.merge(@base.slice(*DEFAULTS.keys)).merge(given.to_h).merge("name" => @name)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cron_declaration"
|
|
4
|
+
|
|
5
|
+
module Captive
|
|
6
|
+
module Commands
|
|
7
|
+
class CronsCommands < Thor
|
|
8
|
+
include CLIDependencies
|
|
9
|
+
|
|
10
|
+
def self.exit_on_failure? = true
|
|
11
|
+
|
|
12
|
+
DECLARATION_OPTIONS = {
|
|
13
|
+
schedule: { type: :string, desc: "Planning cron à 5 champs, ex. \"0 3 * * *\"" },
|
|
14
|
+
target: { type: :string, desc: "URL (mode http) ou commande (mode command)" },
|
|
15
|
+
mode: { type: :string, enum: %w[http command], desc: "http (défaut) ou command" },
|
|
16
|
+
timezone: { type: :string, desc: "Fuseau IANA (défaut Etc/UTC)" },
|
|
17
|
+
timeout: { type: :numeric, desc: "Durée max en secondes, 1 à 3600 (défaut 60)" },
|
|
18
|
+
suspended: { type: :boolean, desc: "Déclare la tâche suspendue" },
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def self.declaration_options
|
|
22
|
+
DECLARATION_OPTIONS.each { |name, settings| method_option(name, **settings) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "list APP", "Liste les tâches planifiées de l'app"
|
|
26
|
+
long_desc <<~DESC
|
|
27
|
+
\x05Exemple :
|
|
28
|
+
\x05 captive crons list my-app
|
|
29
|
+
DESC
|
|
30
|
+
def list(app)
|
|
31
|
+
tasks = crons_api.list(app)
|
|
32
|
+
return puts(paint.dim("Aucune tâche planifiée.")) if tasks.empty?
|
|
33
|
+
|
|
34
|
+
tasks.each { |task| puts presenter.lines(task) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "add APP NAME --schedule CRON --target CIBLE", "Déclare une tâche planifiée"
|
|
38
|
+
long_desc <<~DESC
|
|
39
|
+
\x05Exemple :
|
|
40
|
+
\x05 captive crons add my-app nettoyage --schedule "0 3 * * *" --target /cron/cleanup
|
|
41
|
+
\x05 captive crons add my-app purge --schedule "*/15 * * * *" --mode command --target "bin/rails purge"
|
|
42
|
+
DESC
|
|
43
|
+
declaration_options
|
|
44
|
+
def add(app, name)
|
|
45
|
+
crons_api.create(app, CronDeclaration.new(name).with(options.transform_keys(&:to_sym)))
|
|
46
|
+
accepted("#{name} déclarée")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "edit APP NAME [options]", "Modifie une tâche planifiée"
|
|
50
|
+
long_desc <<~DESC
|
|
51
|
+
Les options absentes gardent la valeur actuelle de la tâche.
|
|
52
|
+
|
|
53
|
+
\x05Exemple :
|
|
54
|
+
\x05 captive crons edit my-app nettoyage --schedule "0 4 * * *"
|
|
55
|
+
DESC
|
|
56
|
+
declaration_options
|
|
57
|
+
def edit(app, name)
|
|
58
|
+
base = crons_api.find(app, name)
|
|
59
|
+
crons_api.update(app, name, CronDeclaration.new(name, base: base).with(options.transform_keys(&:to_sym)))
|
|
60
|
+
accepted("#{name} modifiée")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
desc "rm APP NAME", "Supprime une tâche planifiée"
|
|
64
|
+
long_desc <<~DESC
|
|
65
|
+
\x05Exemple :
|
|
66
|
+
\x05 captive crons rm my-app nettoyage
|
|
67
|
+
DESC
|
|
68
|
+
def rm(app, name)
|
|
69
|
+
crons_api.delete(app, name)
|
|
70
|
+
accepted("#{name} supprimée")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
desc "suspend APP NAME", "Suspend une tâche planifiée"
|
|
74
|
+
long_desc <<~DESC
|
|
75
|
+
\x05Exemple :
|
|
76
|
+
\x05 captive crons suspend my-app nettoyage
|
|
77
|
+
DESC
|
|
78
|
+
def suspend(app, name)
|
|
79
|
+
crons_api.suspend(app, name)
|
|
80
|
+
accepted("#{name} suspendue")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
desc "resume APP NAME", "Reprend une tâche planifiée suspendue"
|
|
84
|
+
long_desc <<~DESC
|
|
85
|
+
\x05Exemple :
|
|
86
|
+
\x05 captive crons resume my-app nettoyage
|
|
87
|
+
DESC
|
|
88
|
+
def resume(app, name)
|
|
89
|
+
crons_api.resume(app, name)
|
|
90
|
+
accepted("#{name} reprise")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
desc "trigger APP NAME", "Lance une tâche planifiée maintenant"
|
|
94
|
+
long_desc <<~DESC
|
|
95
|
+
\x05Exemple :
|
|
96
|
+
\x05 captive crons trigger my-app nettoyage
|
|
97
|
+
DESC
|
|
98
|
+
def trigger(app, name)
|
|
99
|
+
job = crons_api.trigger(app, name).fetch("job_name")
|
|
100
|
+
puts paint.green(" ✓ #{name} lancée (job #{job})")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
desc "runs APP NAME", "Affiche la dernière exécution d'une tâche"
|
|
104
|
+
long_desc <<~DESC
|
|
105
|
+
Le cluster ne garde que la dernière exécution (un succès disparaît après 5 minutes).
|
|
106
|
+
|
|
107
|
+
\x05Exemple :
|
|
108
|
+
\x05 captive crons runs my-app nettoyage
|
|
109
|
+
DESC
|
|
110
|
+
def runs(app, name)
|
|
111
|
+
puts presenter.last_run(crons_api.find(app, name)["last_run"]).strip
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def presenter = Presenter::ScheduledTask.new(paint: paint)
|
|
117
|
+
|
|
118
|
+
def accepted(message)
|
|
119
|
+
puts paint.green(" ✓ #{message} : #{ASYNC_NOTICE}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "env_set"
|
|
4
|
+
|
|
5
|
+
module Captive
|
|
6
|
+
module Commands
|
|
7
|
+
class EnvCommands < Thor
|
|
8
|
+
include CLIDependencies
|
|
9
|
+
|
|
10
|
+
def self.exit_on_failure? = true
|
|
11
|
+
|
|
12
|
+
desc "list APP", "Liste les variables d'environnement de l'app"
|
|
13
|
+
long_desc <<~DESC
|
|
14
|
+
Lit les variables via le dashboard. Une écriture pas encore appliquée est signalée.
|
|
15
|
+
|
|
16
|
+
\x05Exemple :
|
|
17
|
+
\x05 captive env list my-app-staging
|
|
18
|
+
DESC
|
|
19
|
+
def list(app)
|
|
20
|
+
env_api.list(app).sort_by { |var| var["key"] }.each do |var|
|
|
21
|
+
puts "#{var["key"]}=#{var["value"]}#{state(var)}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "set APP KEY=VALUE [KEY=VALUE...]", "Crée ou modifie des variables d'environnement"
|
|
26
|
+
long_desc <<~DESC
|
|
27
|
+
Crée les clés absentes, met à jour les autres. Appliqué par la plateforme
|
|
28
|
+
sous quelques minutes.
|
|
29
|
+
|
|
30
|
+
\x05Exemple :
|
|
31
|
+
\x05 captive env set my-app-staging FOO=bar BAZ=qux
|
|
32
|
+
DESC
|
|
33
|
+
def set(app, *pairs)
|
|
34
|
+
abort "Indique au moins une paire KEY=VALUE." if pairs.empty?
|
|
35
|
+
|
|
36
|
+
EnvSet.new(env_vars: env_api).run(app, pairs).each do |key, outcome|
|
|
37
|
+
puts paint.green(" ✓ #{key} #{outcome == :created ? "créée" : "modifiée"}")
|
|
38
|
+
end
|
|
39
|
+
puts paint.dim(" #{ASYNC_NOTICE}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc "unset APP KEY [KEY...]", "Supprime des variables d'environnement"
|
|
43
|
+
long_desc <<~DESC
|
|
44
|
+
\x05Exemple :
|
|
45
|
+
\x05 captive env unset my-app-staging FOO BAZ
|
|
46
|
+
DESC
|
|
47
|
+
def unset(app, *keys)
|
|
48
|
+
abort "Indique au moins une clé." if keys.empty?
|
|
49
|
+
|
|
50
|
+
keys.each do |key|
|
|
51
|
+
env_api.delete(app, key)
|
|
52
|
+
puts paint.green(" ✓ #{key} supprimée")
|
|
53
|
+
end
|
|
54
|
+
puts paint.dim(" #{ASYNC_NOTICE}")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def state(var)
|
|
60
|
+
return paint.red(" [échec : #{var["failure_reason"] || "écriture refusée"}]") if var["failed"]
|
|
61
|
+
|
|
62
|
+
var["pending"] ? paint.yellow(" [en attente : #{var["pending_change"]}]") : ""
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
5
|
+
module Captive
|
|
6
|
+
module Commands
|
|
7
|
+
# Écrit des paires KEY=VALUE : une clé déjà présente se met à jour, une nouvelle se crée.
|
|
8
|
+
class EnvSet
|
|
9
|
+
def initialize(env_vars:)
|
|
10
|
+
@env_vars = env_vars
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Retourne [[clé, :created|:updated], …] dans l'ordre des paires.
|
|
14
|
+
def run(app, pairs)
|
|
15
|
+
parsed = pairs.map { |pair| parse(pair) }
|
|
16
|
+
existing = @env_vars.list(app).to_set { |var| var["key"] }
|
|
17
|
+
parsed.map { |key, value| write(app, key, value, existing.include?(key)) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def parse(pair)
|
|
23
|
+
key, separator, value = pair.partition("=")
|
|
24
|
+
raise ValidationError, "Paire invalide : #{pair.inspect}. Attendu KEY=VALUE" if separator.empty? || key.empty?
|
|
25
|
+
|
|
26
|
+
[ key, value ]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def write(app, key, value, exists)
|
|
30
|
+
if exists
|
|
31
|
+
@env_vars.update(app, key, value)
|
|
32
|
+
[ key, :updated ]
|
|
33
|
+
else
|
|
34
|
+
@env_vars.create(app, key, value)
|
|
35
|
+
[ key, :created ]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|