captive-cli 1.0.0 → 1.2.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.
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "errors"
5
+ require_relative "validation_message"
6
+
7
+ module Captive
8
+ # Traduit une réponse HTTP du dashboard : le corps JSON si elle réussit, sinon l'erreur
9
+ # que l'utilisateur peut comprendre.
10
+ class DashboardResponse
11
+ ERRORS = {
12
+ 401 => [ AuthError, "Token invalide ou expiré. Lance : captive login" ],
13
+ 403 => [ ForbiddenError, "Accès refusé : tu n'es pas Membre de l'Organization de cette app." ],
14
+ 404 => [ NotFoundError, "App ou ressource introuvable" ],
15
+ 503 => [ ServiceUnavailableError, "Service indisponible, réessaie" ],
16
+ }.freeze
17
+
18
+ def initialize(response)
19
+ @code = response.code.to_i
20
+ @body = response.body.to_s
21
+ end
22
+
23
+ def parse
24
+ return (@body.strip.empty? ? nil : JSON.parse(@body)) if @code.between?(200, 299)
25
+ raise ValidationError, ValidationMessage.new(json_body).to_s if @code == 422
26
+
27
+ klass, message = ERRORS.fetch(@code) { [ DashboardError, "Erreur dashboard (HTTP #{@code})" ] }
28
+ raise klass, message
29
+ end
30
+
31
+ private
32
+
33
+ def json_body
34
+ JSON.parse(@body)
35
+ rescue JSON::ParserError
36
+ {}
37
+ end
38
+ end
39
+ end
@@ -4,13 +4,19 @@ module Captive
4
4
  class NoPodFound < StandardError; end
5
5
  class UnknownAppType < StandardError; end
6
6
  class PollTimeout < StandardError; end
7
- class ImageYamlNotFound < StandardError; end
8
- class WorkflowDispatchNotConfigured < StandardError; end
9
7
  class RestoreError < StandardError; end
10
8
  class DumpError < StandardError; end
11
9
  class ExecError < StandardError; end
12
10
  class AuthError < StandardError; end
13
11
  class NotAuthenticatedError < StandardError; end
14
12
  class NoTtyError < StandardError; end
15
- class PlatformRootNotFound < StandardError; end
13
+ class MissingDependencyError < StandardError; end
14
+
15
+ # Réponses de l'API du dashboard qui ont un sens pour l'utilisateur : rattrapées par
16
+ # CLI.start et affichées sans trace.
17
+ class DashboardError < StandardError; end
18
+ class ForbiddenError < DashboardError; end
19
+ class NotFoundError < DashboardError; end
20
+ class ValidationError < DashboardError; end
21
+ class ServiceUnavailableError < DashboardError; end
16
22
  end
data/lib/captive/flux.rb CHANGED
@@ -1,15 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require_relative "errors"
4
5
 
5
6
  module Captive
6
7
  class Flux
7
8
  def suspend_kustomization(name)
8
- Open3.capture3("flux", "suspend", "kustomization", name)
9
+ capture("suspend", "kustomization", name)
9
10
  end
10
11
 
11
12
  def resume_kustomization(name)
12
- Open3.capture3("flux", "resume", "kustomization", name)
13
+ capture("resume", "kustomization", name)
14
+ end
15
+
16
+ private
17
+
18
+ def capture(*args)
19
+ Open3.capture3("flux", *args)
20
+ rescue Errno::ENOENT
21
+ raise MissingDependencyError, "flux introuvable. Installe-le : brew install flux"
13
22
  end
14
23
  end
15
24
  end
@@ -237,12 +237,6 @@ module Captive
237
237
  raise
238
238
  end
239
239
 
240
- def get_secret_data(namespace, secret_name)
241
- json = run("get", "secret", secret_name, "-n", namespace, "-o", "json")
242
- data = JSON.parse(json).fetch("data", {})
243
- data.transform_values { |v| Base64.strict_decode64(v) }
244
- end
245
-
246
240
  def get_secret_password(namespace, secret_name)
247
241
  encoded = run("get", "secret", secret_name, "-n", namespace,
248
242
  "-o", "jsonpath={.data.password}").strip
@@ -291,10 +285,6 @@ module Captive
291
285
  status.success? ? out : nil
292
286
  end
293
287
 
294
- def restart_deployment(namespace, name)
295
- run("rollout", "restart", "deployment", "-n", namespace, name)
296
- end
297
-
298
288
  def run(*args)
299
289
  out, err, status = Open3.capture3("kubectl", *kubeconfig_args, *args)
300
290
  raise AuthError, "Votre session a expiré. Reconnectez-vous avec : captive login" if err.include?("Unauthorized")
@@ -6,14 +6,18 @@ require "pastel"
6
6
  require "tty-prompt"
7
7
 
8
8
  require_relative "kubectl"
9
- require_relative "cluster_apps"
10
9
  require_relative "log_noise_filter"
11
10
  require_relative "presenter/ansi_stripper"
12
11
  require_relative "null_log_filter"
13
12
  require_relative "one_off_spec"
14
13
  require_relative "session_id"
15
14
  require_relative "poller"
16
- require_relative "gh_cli"
15
+ require_relative "dashboard_api"
16
+ require_relative "dashboard/apps_resource"
17
+ require_relative "dashboard/env_vars_resource"
18
+ require_relative "dashboard/scheduled_tasks_resource"
19
+ require_relative "presenter/app_info"
20
+ require_relative "presenter/scheduled_task"
17
21
  require_relative "activity_trail"
18
22
  require_relative "errors"
19
23
  require_relative "ui/spinner"
@@ -23,9 +27,7 @@ require_relative "commands/console"
23
27
  require_relative "commands/one_off_session"
24
28
  require_relative "commands/run"
25
29
  require_relative "commands/logs"
26
- require_relative "commands/apps_info"
27
30
  require_relative "commands/apps_restart"
28
- require_relative "commands/apps_rebuild"
29
31
  require_relative "commands/db_dump"
30
32
  require_relative "commands/db_restore"
31
33
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captive
4
+ module Presenter
5
+ # La fiche d'une app telle que `captive apps info` l'affiche, à partir des lectures du
6
+ # dashboard : l'app, ses workloads, ses datastores et ses domaines.
7
+ class AppInfo
8
+ def initialize(paint:)
9
+ @paint = paint
10
+ end
11
+
12
+ def lines(app:, workloads:, datastores:, domains:)
13
+ [ @paint.bold(app["name"]),
14
+ " Stage : #{app["stage"] || "–"}",
15
+ " Statut : #{app["status"] || "–"}",
16
+ " URL : #{app["app_url"] || "–"}",
17
+ *section("Processus", workloads.map { |w| workload(w) }),
18
+ *section("Datastores", datastores.map { |d| "#{d["kind"]} #{d["name"]}" }),
19
+ *section("Domaines", domains.map { |d| domain(d) }), ]
20
+ end
21
+
22
+ private
23
+
24
+ def section(title, items)
25
+ [ " #{title} :", *(items.empty? ? [ @paint.dim(" –") ] : items.map { |item| " #{item}" }) ]
26
+ end
27
+
28
+ def workload(entry)
29
+ ready = "#{entry["ready"]}/#{entry["total"]}"
30
+ ready = @paint.yellow(ready) unless entry["ready"] == entry["total"]
31
+ "#{entry["name"]} #{ready} prêt(s), tier #{entry["tier"] || "–"}"
32
+ end
33
+
34
+ def domain(entry)
35
+ flags = [ ("principal" if entry["primary"]), ("en attente" if entry["pending"]) ].compact
36
+ flags.empty? ? entry["hostname"] : "#{entry["hostname"]} (#{flags.join(", ")})"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captive
4
+ module Presenter
5
+ # Une tâche planifiée sur une ligne, puis sa dernière exécution si elle a tourné.
6
+ class ScheduledTask
7
+ def initialize(paint:)
8
+ @paint = paint
9
+ end
10
+
11
+ def lines(task)
12
+ head = "#{@paint.bold(task["name"])} #{task["schedule"]} (#{task["timezone"]}) " \
13
+ "#{task["mode"]} #{task["target"]}"
14
+ head += " #{@paint.yellow("[suspendue]")}" if task["suspended"]
15
+ [ head, last_run(task["last_run"]) ]
16
+ end
17
+
18
+ # Le cluster ne garde qu'une exécution réussie, cinq minutes : seule la dernière se lit.
19
+ def last_run(run)
20
+ return @paint.dim(" aucune exécution récente") unless run
21
+
22
+ " dernière : #{run["started_at"]} → #{run["finished_at"] || "en cours"} #{outcome(run["outcome"])}"
23
+ end
24
+
25
+ private
26
+
27
+ def outcome(value)
28
+ case value
29
+ when "succeeded" then @paint.green(value)
30
+ when "failed", "timeout" then @paint.red(value)
31
+ else value.to_s
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Captive
4
4
  module ShellCompletions
5
- ROOT_COMMANDS = %w[apps env db login whoami console run logs version completions].freeze
5
+ ROOT_COMMANDS = %w[apps env crons db login whoami console run logs version completions].freeze
6
6
 
7
7
  SUBCOMMANDS = {
8
8
  "apps" => %w[list info restart rebuild],
9
- "env" => %w[list],
9
+ "env" => %w[list set unset],
10
+ "crons" => %w[list add edit rm suspend resume trigger runs],
10
11
  "db" => %w[restore],
11
12
  }.freeze
12
13
 
@@ -21,6 +22,7 @@ module Captive
21
22
  case "$prev" in
22
23
  apps) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["apps"].join(" ")}" -- "$cur")); return ;;
23
24
  env) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["env"].join(" ")}" -- "$cur")); return ;;
25
+ crons) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["crons"].join(" ")}" -- "$cur")); return ;;
24
26
  db) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["db"].join(" ")}" -- "$cur")); return ;;
25
27
  esac
26
28
 
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module Captive
6
+ # Le jeton GitHub écrit par `captive login`, qui authentifie toutes les requêtes au dashboard.
7
+ class TokenFile
8
+ PATH = File.expand_path("~/.config/captive/token")
9
+
10
+ def initialize(path: PATH)
11
+ @path = path
12
+ end
13
+
14
+ def read
15
+ raise NotAuthenticatedError, "Pas de session active. Lance : captive login" unless File.exist?(@path)
16
+
17
+ File.read(@path).strip
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captive
4
+ # Les refus de validation, un par ligne : `errors` est une liste de {field, message}, un
5
+ # objet champ → messages, ou absent (seul `error` dit alors ce qui ne va pas).
6
+ class ValidationMessage
7
+ def initialize(body)
8
+ @body = body.is_a?(Hash) ? body : {}
9
+ end
10
+
11
+ def to_s
12
+ ([ "Requête refusée :" ] + lines.map { |line| " - #{line}" }).join("\n")
13
+ end
14
+
15
+ private
16
+
17
+ def lines
18
+ errors = @body["errors"]
19
+ case errors
20
+ when Array then errors.map { |e| e.is_a?(Hash) ? "#{e["field"]} : #{e["message"]}" : e.to_s }
21
+ when Hash then errors.map { |field, messages| "#{field} : #{Array(messages).join(", ")}" }
22
+ else [ @body.fetch("error", "invalide").to_s ]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Captive
4
- VERSION = "1.0.0"
4
+ VERSION = "1.2.0"
5
5
  end
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.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Captive Studio
@@ -106,29 +106,32 @@ files:
106
106
  - lib/captive/activity_trail.rb
107
107
  - lib/captive/cli.rb
108
108
  - lib/captive/cli/dependencies.rb
109
- - lib/captive/cluster_apps.rb
110
109
  - lib/captive/cluster_endpoint.rb
111
110
  - lib/captive/commands/apps_commands.rb
112
- - lib/captive/commands/apps_info.rb
113
- - lib/captive/commands/apps_rebuild.rb
114
111
  - lib/captive/commands/apps_restart.rb
115
112
  - lib/captive/commands/console.rb
113
+ - lib/captive/commands/cron_declaration.rb
114
+ - lib/captive/commands/crons_commands.rb
116
115
  - lib/captive/commands/db_commands.rb
117
116
  - lib/captive/commands/db_dump.rb
118
117
  - lib/captive/commands/db_exec.rb
119
118
  - lib/captive/commands/db_psql.rb
120
119
  - lib/captive/commands/db_restore.rb
121
120
  - lib/captive/commands/db_target.rb
122
- - lib/captive/commands/env_kube_commands.rb
123
- - lib/captive/commands/env_list_kube.rb
121
+ - lib/captive/commands/env_commands.rb
122
+ - lib/captive/commands/env_set.rb
124
123
  - lib/captive/commands/logs.rb
125
124
  - lib/captive/commands/one_off_session.rb
126
125
  - lib/captive/commands/run.rb
127
126
  - lib/captive/commands/user_login.rb
127
+ - lib/captive/dashboard/apps_resource.rb
128
+ - lib/captive/dashboard/env_vars_resource.rb
129
+ - lib/captive/dashboard/scheduled_tasks_resource.rb
130
+ - lib/captive/dashboard_api.rb
128
131
  - lib/captive/dashboard_client.rb
132
+ - lib/captive/dashboard_response.rb
129
133
  - lib/captive/errors.rb
130
134
  - lib/captive/flux.rb
131
- - lib/captive/gh_cli.rb
132
135
  - lib/captive/github_device_flow.rb
133
136
  - lib/captive/http_client.rb
134
137
  - lib/captive/kubectl.rb
@@ -136,17 +139,20 @@ files:
136
139
  - lib/captive/log_noise_filter.rb
137
140
  - lib/captive/null_log_filter.rb
138
141
  - lib/captive/one_off_spec.rb
139
- - lib/captive/platform_root.rb
140
142
  - lib/captive/poller.rb
141
143
  - lib/captive/presenter/ansi_stripper.rb
144
+ - lib/captive/presenter/app_info.rb
142
145
  - lib/captive/presenter/log_colorizer.rb
143
146
  - lib/captive/presenter/log_line.rb
147
+ - lib/captive/presenter/scheduled_task.rb
144
148
  - lib/captive/session_id.rb
145
149
  - lib/captive/shell_completions.rb
146
150
  - lib/captive/terminal.rb
151
+ - lib/captive/token_file.rb
147
152
  - lib/captive/ui/banner.rb
148
153
  - lib/captive/ui/spinner.rb
149
154
  - lib/captive/update_checker.rb
155
+ - lib/captive/validation_message.rb
150
156
  - lib/captive/version.rb
151
157
  homepage: https://github.com/captive-studio/captive-platform
152
158
  licenses:
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Captive
6
- # Ce que le cluster sait des apps déployées. Le CLI développeur décrit l'état réellement
7
- # servi, pas l'état désiré dans le repo GitOps — auquel il n'a pas accès.
8
- class ClusterApps
9
- def initialize(kubectl:)
10
- @kubectl = kubectl
11
- end
12
-
13
- def all
14
- web_deployments.map do |deployment|
15
- namespace = deployment.dig("metadata", "namespace")
16
- { name: namespace, stage: stage_of(namespace), domains: domains_by_namespace[namespace] || [] }
17
- end
18
- end
19
-
20
- def domains(app) = domains_by_namespace[app] || []
21
-
22
- def stage(app) = stage_of(app)
23
-
24
- def tier(app) = web_of(app)&.dig("metadata", "annotations", "captive.dev/tier")
25
-
26
- def workers(app) = of_type(app, "worker").map { |d| d.dig("metadata", "name") }
27
-
28
- # L'add-on peut ne pas être posé, ou la CRD absente du cluster : les deux se lisent
29
- # comme « pas d'IP fixe », pas comme une panne.
30
- def egress_ip(app)
31
- first_item("egresspolicy", app)&.dig("spec", "egressIP", "ipv4")
32
- end
33
-
34
- def postgres(app)
35
- spec = first_item("cluster", app)&.fetch("spec", nil) or return nil
36
-
37
- { instances: spec["instances"],
38
- memory: spec.dig("resources", "limits", "memory"),
39
- storage: spec.dig("storage", "size"), }.compact
40
- end
41
-
42
- private
43
-
44
- def first_item(kind, app)
45
- items(@kubectl.run("get", kind, "-n", app, "-o", "json")).first
46
- rescue RuntimeError
47
- nil
48
- end
49
-
50
- def web_of(app) = of_type(app, "web").first
51
-
52
- def of_type(app, type)
53
- workloads(app).select { |d| d.dig("metadata", "labels", "captive.dev/type") == type }
54
- end
55
-
56
- def workloads(app)
57
- @workloads ||= {}
58
- @workloads[app] ||= items(@kubectl.run("get", "deployment", "-n", app, "-o", "json"))
59
- end
60
-
61
-
62
- # Une app est un Deployment portant le type web : le même label que les composants de
63
- # tier ciblent, et que la conformité exige.
64
- def web_deployments
65
- items(@kubectl.run("get", "deployment", "-A", "-l", "captive.dev/type=web", "-o", "json"))
66
- end
67
-
68
- def domains_by_namespace
69
- @domains_by_namespace ||= items(@kubectl.run("get", "httproute", "-A", "-o", "json"))
70
- .group_by { |route| route.dig("metadata", "namespace") }
71
- .transform_values { |routes| routes.flat_map { |r| Array(r.dig("spec", "hostnames")) } }
72
- end
73
-
74
- def items(json) = JSON.parse(json).fetch("items", [])
75
-
76
- def stage_of(namespace)
77
- return "production" if namespace.end_with?("-production")
78
-
79
- "staging" if namespace.end_with?("-staging")
80
- end
81
- end
82
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Captive
4
- module Commands
5
- # L'état d'une app tel que le cluster le sert. Le CLI développeur n'a pas accès au repo
6
- # GitOps : tout se lit sur les ressources vivantes, ce qui décrit l'état réel plutôt que
7
- # l'état désiré — un déploiement en cours s'y voit, un manifeste poussé mais non
8
- # réconcilié ne s'y voit pas encore.
9
- class AppsInfo
10
- TIER_RESOURCES = {
11
- "xs" => { cpu: "100m → 500m", memory: "256Mi → 512Mi" },
12
- "s" => { cpu: "250m → 1", memory: "512Mi → 1Gi" },
13
- "m" => { cpu: "500m → 2", memory: "1Gi → 2Gi" },
14
- "l" => { cpu: "1 → 4", memory: "2Gi → 4Gi" },
15
- }.freeze
16
-
17
- def initialize(kubectl:, cluster_apps: nil)
18
- @kubectl = kubectl
19
- @apps = cluster_apps || ClusterApps.new(kubectl: kubectl)
20
- end
21
-
22
- def gather(app)
23
- tier = @apps.tier(app)
24
- postgres = @apps.postgres(app)
25
- egress = @apps.egress_ip(app)
26
- {
27
- pod_status: pod_status(app),
28
- replicas: replicas(app),
29
- postgres: !postgres.nil?,
30
- redis: redis?(app),
31
- egress_ip: !egress.nil?,
32
- tier: tier,
33
- resources: TIER_RESOURCES[tier],
34
- domains: @apps.domains(app),
35
- stage: @apps.stage(app),
36
- workers: @apps.workers(app),
37
- postgres_details: postgres,
38
- redis_details: redis_details(app),
39
- egress_ip_details: egress && { ip: egress },
40
- }
41
- end
42
-
43
- private
44
-
45
- def pod_status(app)
46
- @kubectl.run("get", "pod", "-n", app, "-l", "app=#{app}",
47
- "-o", "jsonpath={.items[0].status.phase}").strip
48
- end
49
-
50
- def replicas(app)
51
- @kubectl.run("get", "deployment", app, "-n", app,
52
- "-o", "jsonpath={.spec.replicas}").strip.to_i
53
- end
54
-
55
- def redis?(app)
56
- !@kubectl.run("get", "deployment", "-n", app, "-l", "app=redis",
57
- "--no-headers", "-o", "name").strip.empty?
58
- end
59
-
60
- # Redis est un Deployment ordinaire : sa taille se lit sur le conteneur, pas sur une CRD.
61
- def redis_details(app)
62
- limits = @kubectl.run(
63
- "get", "deployment", "-n", app, "-l", "app=redis",
64
- "-o", "jsonpath={.items[0].spec.template.spec.containers[0].resources.limits.memory}"
65
- ).strip
66
- limits.empty? ? nil : { memory: limits }
67
- end
68
- end
69
- end
70
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../errors"
4
- require_relative "../platform_root"
5
-
6
- module Captive
7
- module Commands
8
- class AppsRebuild
9
- # Dernière commande du CLI développeur qui lit le repo GitOps, contre l'ADR 0016.
10
- # Elle déduit le repo GitHub de l'image, or les images valent
11
- # ghcr.io/captive-studio/captive-platform/<app> : la déduction rend trois segments, qui
12
- # ne désignent aucun repo. Rien dans le service ni sur le cluster n'enregistre le repo
13
- # d'origine, donc la commande n'est pas portable en l'état — voir la note de l'étape 9.
14
- # Elle exige donc un clone de captive-platform (PlatformRoot), jusqu'à passer par l'API
15
- # du dashboard.
16
- def initialize(gh:, repo_root: nil)
17
- @gh = gh
18
- @repo_root = repo_root
19
- end
20
-
21
- def run(app)
22
- repo = extract_repo(app)
23
- version = current_version(app)
24
- fields = version ? { version: version } : {}
25
- @gh.trigger_workflow(repo, ref: "main", fields: fields)
26
- end
27
-
28
- private
29
-
30
- def repo_root = @repo_root ||= PlatformRoot.find
31
-
32
- def extract_repo(app)
33
- image_file = File.join(repo_root, "services", app, "image.yaml")
34
- raise ImageYamlNotFound, "image.yaml introuvable pour '#{app}'" unless File.exist?(image_file)
35
-
36
- content = File.read(image_file)
37
- image = content.match(/image:\s+ghcr\.io\/([^\s:]+)/)&.captures&.first
38
- raise ImageYamlNotFound, "image introuvable dans image.yaml de '#{app}'" unless image
39
-
40
- image
41
- end
42
-
43
- def current_version(app)
44
- deploy_file = File.join(repo_root, "services", app, "#{app}.yaml")
45
- return nil unless File.exist?(deploy_file)
46
-
47
- tag = File.read(deploy_file).match(/:(v\d+\.\d+\.\d+(?:-\d{14})?)/)&.captures&.first
48
- tag&.sub(/-\d{14}$/, "")
49
- end
50
- end
51
- end
52
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "env_list_kube"
4
-
5
- module Captive
6
- module Commands
7
- class EnvKubeCommands < 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 depuis K8s"
13
- long_desc <<~DESC
14
- Lit directement le secret <app>-env depuis le cluster Kubernetes.
15
-
16
- \x05Exemple :
17
- \x05 captive env list my-app-staging
18
- DESC
19
- def list(app)
20
- vars = EnvListKube.new(kubectl: kubectl).run(app)
21
- vars.sort.each { |v| puts v }
22
- end
23
- end
24
- end
25
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Captive
4
- module Commands
5
- class EnvListKube
6
- def initialize(kubectl:)
7
- @kubectl = kubectl
8
- end
9
-
10
- def run(app)
11
- data = @kubectl.get_secret_data(app, "#{app}-env")
12
- data.map { |k, v| "#{k}=#{v}" }
13
- end
14
- end
15
- end
16
- end