jatai 1.2.1 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bf8aed477b1220c2ffd5b71fc309ac1f407a51404b43603f9ce08ad7f29f78c
4
- data.tar.gz: 2514d15c7bcdc3460193c286ff6657b27aae26817524b66121cbc96ccc28a03c
3
+ metadata.gz: 648533180b943e80129355c3308d594f1867fde7946470361f3897ad3cdd06eb
4
+ data.tar.gz: 2cfe1d3481ef074b1c1e257f0cd2eea369193a6681694d5d2da6a8e6131f4c42
5
5
  SHA512:
6
- metadata.gz: 3d6f9195e30feeb5798276c198ffcddcd8ef429bbd65c4bd9444adf50089c0983ee124afe8a8bd72e342b7199248c46477241deb2083f3d7e9caf4438238a963
7
- data.tar.gz: a6d364146dd82c0321773da507e2eea74b40b637601c27b7cabe45d5cea9b4f61d7a007125b38d25fd346a20a00862733db5945c6af97dfcff1d9a88b41cd09b
6
+ metadata.gz: 82d898f647e4109c8842d35d203fd818a170f013f174865ef91e5934a6afc93083e2dd4aa7dd305ef2922d7ab2897c0257f9dbd9fba8c763a021b4707851a7be
7
+ data.tar.gz: 444b6824a85488f361916f126e34bc68e574c7248bef66d35c51430cdaa9056ad0b18d6880a17ea856d2ef98ca1483f7df71ad991d08729c19630f6b053b99a8
data/lib/jatai/cli.rb CHANGED
@@ -13,6 +13,7 @@ require "jatai/commands/status"
13
13
  require "jatai/commands/destroy"
14
14
  require "jatai/commands/domains"
15
15
  require "jatai/commands/redis"
16
+ require "jatai/commands/run"
16
17
 
17
18
  module Jatai
18
19
  class CLI < Thor
@@ -41,6 +42,13 @@ module Jatai
41
42
  Commands::Deploy.new.execute(options)
42
43
  end
43
44
 
45
+ # "run" é palavra reservada do Thor — mapeada p/ run_command.
46
+ desc "run COMANDO", "Executar comando one-off no app (pod efêmero do release atual)"
47
+ map "run" => :run_command
48
+ def run_command(*command_parts)
49
+ Commands::Run.new.execute(command_parts)
50
+ end
51
+
44
52
  desc "logs", "Ver logs da aplicação em tempo real"
45
53
  option :tail, type: :numeric, default: 100, desc: "Número de linhas"
46
54
  def logs
@@ -22,8 +22,25 @@ module Jatai
22
22
  end
23
23
  end
24
24
 
25
+ # SEMPRE slugifica o que veio do arquivo. Isso é o que dá
26
+ # retrocompatibilidade com os .jatai.yml gerados antes de `app:` passar a
27
+ # guardar o slug: lá dentro havia o nome de exibição cru ("Alves
28
+ # Barbearia"), que ia direto para a URL e estourava no URI antes mesmo de
29
+ # sair a requisição. Slugificar é idempotente, então arquivo novo — que já
30
+ # traz o slug — atravessa sem alteração.
25
31
  def app_slug
26
- project_config[:app]
32
+ raw = project_config[:app]
33
+ slug = Slug.from(raw)
34
+ return slug if slug
35
+
36
+ abort pastel.red("`app` inválido no .jatai.yml (#{raw.inspect}). " \
37
+ "Use letras, números e hífen, ou rode `jatai init` novamente.")
38
+ end
39
+
40
+ # Nome legível. Arquivo novo traz `name:`; no antigo, o próprio `app:` era
41
+ # o nome de exibição.
42
+ def app_name
43
+ project_config[:name] || project_config[:app]
27
44
  end
28
45
 
29
46
  def require_auth!
@@ -11,7 +11,8 @@ module Jatai
11
11
  "pro" => "Pro — 2GB RAM, réplicas ilimitadas (R$ 69/mês)"
12
12
  }.freeze
13
13
 
14
- REGIONS = { "br-se1" => "São Paulo", "br-ne1" => "Nordeste" }.freeze
14
+ # Região única por enquanto (Sudeste). Quando houver multi-região, volta a ser um prompt.
15
+ DEFAULT_REGION = "br-se1".freeze
15
16
  STACKS = {
16
17
  "rails" => "Rails",
17
18
  "node" => "Node.js",
@@ -43,30 +44,48 @@ module Jatai
43
44
  stack = prompt.select("Stack:", STACKS.map { |k, v| { name: v, value: k } })
44
45
  end
45
46
 
46
- name = prompt.ask("Nome do app:") { |q| q.required true }
47
+ name, slug = ask_name_and_slug(prompt)
47
48
  preset = prompt.select("Preset:", PRESETS.map { |k, v| { name: v, value: k } })
48
- region = prompt.select("Região:", REGIONS.map { |k, v| { name: "#{v} (#{k})", value: k } })
49
49
  # Sites estáticos não têm banco — não pergunta o tier.
50
50
  db_tier = stack == "static" ? nil : prompt.select("Banco de dados:", DATABASE_TIERS.map { |k, v| { name: v, value: k } })
51
51
  branch = detect_git_branch || "main"
52
52
 
53
+ # `app` é o IDENTIFICADOR (slug) usado nas URLs da API; `name` é o nome de
54
+ # exibição. Antes um único campo servia aos dois papéis, e um nome com
55
+ # espaço ("Alves Barbearia") ia direto para a URL e estourava no URI.
53
56
  config = {
54
- "app" => name,
57
+ "app" => slug,
58
+ "name" => name,
55
59
  "stack" => stack,
56
60
  "preset" => preset,
57
- "region" => region,
61
+ "region" => DEFAULT_REGION,
58
62
  "branch" => branch
59
63
  }
60
64
  config["database_tier"] = db_tier if db_tier
61
65
 
62
66
  File.write(config_path, YAML.dump(config))
63
67
  success("Arquivo .jatai.yml criado com sucesso!")
68
+ info("Identificador: #{slug}") if slug != name
64
69
  info("Branch: #{branch}")
65
70
  info("Execute `jatai up` para criar e fazer deploy do app.")
66
71
  end
67
72
 
68
73
  private
69
74
 
75
+ # O nome é livre; o slug sai dele. Repete a pergunta quando nada
76
+ # aproveitável sobra (nome só de emoji, por exemplo) em vez de gravar um
77
+ # `app` vazio que quebraria a primeira requisição.
78
+ def ask_name_and_slug(prompt)
79
+ loop do
80
+ name = prompt.ask("Nome do app:") { |q| q.required true }
81
+ slug = Slug.from(name)
82
+ return [ name, slug ] if slug
83
+
84
+ error("Não consegui gerar um identificador a partir de #{name.inspect}. " \
85
+ "Use letras ou números no nome.")
86
+ end
87
+ end
88
+
70
89
  def detect_stack
71
90
  if File.exist?(File.join(Dir.pwd, "Gemfile"))
72
91
  gemfile = File.read(File.join(Dir.pwd, "Gemfile"))
@@ -0,0 +1,79 @@
1
+ require "jatai/commands/base"
2
+
3
+ module Jatai
4
+ module Commands
5
+ # jatai run "<comando>" — executa um comando one-off num pod efêmero com a
6
+ # imagem do release atual, streama a saída e retorna o exit code real.
7
+ class Run < Base
8
+ RUN_TIMEOUT = 960 # activeDeadlineSeconds (900) + folga
9
+
10
+ def execute(command_parts)
11
+ require_auth!
12
+ command = Array(command_parts).join(" ").strip
13
+ return error("Informe o comando: jatai run \"bin/rails runner '...'\"") if command.empty?
14
+
15
+ slug = app_slug
16
+ run = api.post("/api/v1/apps/#{slug}/runs", body: { command: command })
17
+ info("Run ##{run["id"]} criado — subindo pod efêmero com o release atual...")
18
+
19
+ status = follow(run["id"])
20
+ finish(slug, run["id"], status)
21
+ rescue Api::Client::ValidationError => e
22
+ error("Erro: #{e.message}")
23
+ exit(1)
24
+ rescue Api::Client::ApiError => e
25
+ error("Erro: #{e.message}")
26
+ exit(1)
27
+ end
28
+
29
+ private
30
+
31
+ def follow(run_id)
32
+ ws = connect_websocket
33
+ ws.on_log { |msg| puts msg["line"] if msg.is_a?(Hash) && msg["line"] }
34
+ ws.wait_for("AppRunChannel",
35
+ params: { run_id: run_id },
36
+ until_status: %w[succeeded failed error],
37
+ timeout: RUN_TIMEOUT)
38
+ rescue Jatai::Websocket::CableClient::ConnectionError
39
+ info("[WebSocket indisponível — aguardando resultado via API...]")
40
+ poll_until_done(run_id)
41
+ ensure
42
+ ws&.disconnect
43
+ end
44
+
45
+ def poll_until_done(run_id)
46
+ (RUN_TIMEOUT / 5).times do
47
+ run = api.get("/api/v1/apps/#{app_slug}/runs/#{run_id}")
48
+ return run["status"] if %w[succeeded failed error].include?(run["status"])
49
+ sleep(5)
50
+ end
51
+ "error"
52
+ end
53
+
54
+ # O socket entrega o status; o exit code vem da API (fonte de verdade).
55
+ def finish(slug, run_id, status)
56
+ run = api.get("/api/v1/apps/#{slug}/runs/#{run_id}")
57
+ exit_code = run["exit_code"]
58
+
59
+ case status
60
+ when "succeeded"
61
+ success("Concluído (exit 0)")
62
+ exit(0)
63
+ when "failed"
64
+ hint = exit_code == 137 ? " — processo morto (SIGKILL), possivelmente falta de memória (OOM)" : ""
65
+ error("Comando terminou com exit #{exit_code}#{hint}")
66
+ exit(exit_code.to_i.clamp(1, 255))
67
+ else
68
+ error("Run falhou na infraestrutura. Tente novamente ou verifique com: jatai status")
69
+ exit(1)
70
+ end
71
+ end
72
+
73
+ def connect_websocket
74
+ require "jatai/websocket/cable_client"
75
+ Jatai::Websocket::CableClient.new.tap(&:connect!)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -46,14 +46,36 @@ module Jatai
46
46
 
47
47
  private
48
48
 
49
+ # O slug do .jatai.yml é só o PALPITE: o servidor pode ter desambiguado
50
+ # com sufixo (alves-barbearia-1) quando o nome já existia, e a partir daí
51
+ # o arquivo apontaria para sempre para um app que não é o do cliente.
52
+ # Depois de criar, grava de volta o slug que o servidor devolveu — daí em
53
+ # diante o arquivo carrega a verdade, não a dedução.
49
54
  def find_or_create_app(config)
50
- existing = api.get("/api/v1/apps/#{config[:app]}")
55
+ existing = api.get("/api/v1/apps/#{app_slug}")
51
56
  info("App #{existing["slug"]} encontrado (#{existing["status"]}).")
52
57
  ensure_repo_url(existing["slug"])
53
58
  existing
54
59
  rescue Api::Client::NotFoundError
55
- info("Criando app #{config[:app]}...")
56
- create_app(config)
60
+ info("Criando app #{app_name}...")
61
+ created = create_app(config)
62
+ persist_server_slug(created["slug"])
63
+ created
64
+ end
65
+
66
+ def persist_server_slug(server_slug)
67
+ return if server_slug.nil? || server_slug == project_config[:app]
68
+
69
+ path = Config.project_file_path
70
+ raw = YAML.safe_load(File.read(path))
71
+ raw["name"] ||= raw["app"] # arquivo antigo: `app` guardava o nome
72
+ raw["app"] = server_slug
73
+ File.write(path, YAML.dump(raw))
74
+ info("Identificador do app no .jatai.yml: #{server_slug}")
75
+ rescue StandardError => e
76
+ # Não derruba o deploy: o app foi criado e o deploy segue. Só o arquivo
77
+ # ficou desatualizado, e o próximo comando cai no palpite de novo.
78
+ warn(pastel.yellow(" Não foi possível gravar o slug no .jatai.yml: #{e.message}"))
57
79
  end
58
80
 
59
81
  def trigger_provision(slug)
@@ -70,7 +92,7 @@ module Jatai
70
92
 
71
93
  def create_app(config)
72
94
  app_params = {
73
- name: config[:app], stack: config[:stack], preset: config[:preset],
95
+ name: app_name, stack: config[:stack], preset: config[:preset],
74
96
  region: config[:region], repo_url: detect_git_remote, repo_branch: config[:branch]
75
97
  }
76
98
  app_params[:database_tier] = config[:database_tier] if config[:database_tier]
data/lib/jatai/slug.rb ADDED
@@ -0,0 +1,40 @@
1
+ module Jatai
2
+ # Converte texto livre no identificador que o servidor usa na URL.
3
+ #
4
+ # O servidor gera o slug com `name.parameterize` (ActiveSupport) e o valida
5
+ # com /\A[a-z0-9]([a-z0-9\-]*[a-z0-9])?\z/. A CLI não depende de
6
+ # ActiveSupport — carregá-la inteira por uma função encareceria o boot — então
7
+ # a transliteração é feita à mão para o conjunto que os clientes de fato usam
8
+ # (português). Caracteres fora dessa tabela são descartados, e é por isso que
9
+ # esta conversão é só o PALPITE da primeira requisição: quem manda no slug é o
10
+ # servidor, e o `jatai up` grava de volta no .jatai.yml o slug que ele retorna.
11
+ #
12
+ # Idempotente: aplicar sobre um slug válido devolve o mesmo slug. É o que dá
13
+ # retrocompatibilidade com os .jatai.yml antigos, em que `app:` guardava o
14
+ # nome de exibição — "Alves Barbearia" vira "alves-barbearia", que é
15
+ # exatamente o slug que o servidor gerou na criação daquele app.
16
+ module Slug
17
+ TRANSLITERATIONS = {
18
+ "á" => "a", "à" => "a", "â" => "a", "ã" => "a", "ä" => "a", "å" => "a",
19
+ "é" => "e", "è" => "e", "ê" => "e", "ë" => "e",
20
+ "í" => "i", "ì" => "i", "î" => "i", "ï" => "i",
21
+ "ó" => "o", "ò" => "o", "ô" => "o", "õ" => "o", "ö" => "o",
22
+ "ú" => "u", "ù" => "u", "û" => "u", "ü" => "u",
23
+ "ç" => "c", "ñ" => "n", "ý" => "y"
24
+ }.freeze
25
+
26
+ module_function
27
+
28
+ # nil quando não sobra nada utilizável — quem chama decide o que dizer ao
29
+ # usuário. Devolver "" mandaria uma URL vazia para a API.
30
+ def from(text)
31
+ return nil if text.nil?
32
+
33
+ slug = text.to_s.downcase
34
+ slug = slug.gsub(/[^a-z0-9\-]/) { |char| TRANSLITERATIONS.fetch(char, "-") }
35
+ slug = slug.gsub(/-+/, "-").gsub(/\A-|-\z/, "")
36
+
37
+ slug.empty? ? nil : slug
38
+ end
39
+ end
40
+ end
data/lib/jatai/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Jatai
2
- VERSION = "1.2.1"
2
+ VERSION = "1.4.0"
3
3
  end
data/lib/jatai.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "jatai/version"
2
2
  require "jatai/config"
3
+ require "jatai/slug"
3
4
  require "jatai/api/client"
4
5
  require "jatai/cli"
5
6
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jatai
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jataí Cloud
@@ -176,10 +176,12 @@ files:
176
176
  - lib/jatai/commands/logs_export.rb
177
177
  - lib/jatai/commands/logs_search.rb
178
178
  - lib/jatai/commands/redis.rb
179
+ - lib/jatai/commands/run.rb
179
180
  - lib/jatai/commands/scale.rb
180
181
  - lib/jatai/commands/status.rb
181
182
  - lib/jatai/commands/up.rb
182
183
  - lib/jatai/config.rb
184
+ - lib/jatai/slug.rb
183
185
  - lib/jatai/source_packer.rb
184
186
  - lib/jatai/version.rb
185
187
  - lib/jatai/websocket/cable_client.rb