jatai 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b9131e1a51d23a7120a320f01c24d31b332e636467c048bbfddea25bd49b643
4
- data.tar.gz: 5306e6ed76d3076ebe707d08f399d9071d63814fe92cd930239a2143113ba87a
3
+ metadata.gz: cce550fef38fbddcb460eaa4d1d5e2631dfc8b43e33d035c607680e24ea6fee6
4
+ data.tar.gz: 75eaffc25c616af78578cd43b077ef62604852b90a4319d961bc50b4190f067e
5
5
  SHA512:
6
- metadata.gz: ed82a595b2b854268d89902e495963b548b6b6a0057d9e846aa01345157fe5c02ea76935901aede74a668be8a8c6ed1c9a7f509702fd27e8f45b37cbf2f382c4
7
- data.tar.gz: 17fb609ef8532e4da8d49c8bd1cb8ada50fc3052772db9b4b625de6f6516fb830ab8de27264c9166c30eff5080b2adb0d07c827bda32c7184682a0488746e235
6
+ metadata.gz: b16035861874a14cadbafff211bfb8517d30652187a0d428e377b5e3fff59a2f98aa0a04588bb8819e98aa1114429ea856e89432c7ef40657a987019eb63f4dc
7
+ data.tar.gz: 8a50d74a29189c5f21f62c1d16a73030fad4031be48a70957c6350c35d4935b14f6e76fd8b0fdb54365cc2f4a667519699e534e66913855293dfa6ac61fb1b29
data/lib/jatai/cli.rb CHANGED
@@ -88,7 +88,7 @@ module Jatai
88
88
 
89
89
  desc "scale", "Escalar a aplicação"
90
90
  option :replicas, type: :numeric, desc: "Número de réplicas"
91
- option :preset, type: :string, desc: "Preset (micro, hobby, pro)"
91
+ option :preset, type: :string, desc: "Hex (base, escala_1, escala_2)"
92
92
  def scale
93
93
  Commands::Scale.new.execute(options)
94
94
  end
@@ -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!
@@ -31,6 +48,22 @@ module Jatai
31
48
  abort pastel.red("Não autenticado. Execute `jatai login` primeiro.")
32
49
  end
33
50
 
51
+ # Rótulo do cliente para a unidade computacional. O slug é interno
52
+ # (`base`, `escala_1`); quem lê a tela lê "Hex Escala 1" — o mesmo texto
53
+ # que o painel e a fatura mostram (AppsHelper#hex_label no servidor).
54
+ def hex_label(tier)
55
+ return "-" if tier.to_s.empty?
56
+
57
+ "Hex #{tier.to_s.tr("_", " ").capitalize}"
58
+ end
59
+
60
+ # O banco é um addon; `database_plan` vem nulo no app que não tem nenhum.
61
+ def pg_label(plan)
62
+ return "sem banco" if plan.to_s.empty?
63
+
64
+ "PG #{plan.to_s.tr("_", " ").capitalize}"
65
+ end
66
+
34
67
  def success(msg)
35
68
  puts pastel.green(msg)
36
69
  end
@@ -42,6 +75,12 @@ module Jatai
42
75
  def info(msg)
43
76
  puts pastel.cyan(msg)
44
77
  end
78
+
79
+ # Nem verde nem vermelho: o deploy parcial subiu o site e deixou processo
80
+ # para trás — chamar de sucesso ou de falha mentiria nas duas direções.
81
+ def warning(msg)
82
+ puts pastel.yellow(msg)
83
+ end
45
84
  end
46
85
  end
47
86
  end
@@ -14,7 +14,7 @@ module Jatai
14
14
  db = helper.api.get("/api/v1/apps/#{slug}/database")
15
15
 
16
16
  puts helper.pastel.bold("Banco de dados de #{slug}")
17
- puts " Tier: #{db["database_tier"] || db["mode"]}"
17
+ puts " Modo: #{db["mode"]}"
18
18
  puts " Host: #{db["host"]}"
19
19
  puts " Porta: #{db["port"]}"
20
20
  puts " Nome: #{db["name"]}"
@@ -44,12 +44,15 @@ module Jatai
44
44
 
45
45
  status = ws.wait_for("DeployStatusChannel",
46
46
  params: { deploy_id: deploy_id },
47
- until_status: %w[live failed],
47
+ until_status: %w[live failed partially_live],
48
48
  timeout: DEPLOY_TIMEOUT)
49
49
 
50
50
  elapsed = (Time.now - started).to_i
51
- if status == "live"
51
+ case status
52
+ when "live"
52
53
  success("\nDeploy concluído com sucesso! (#{elapsed}s)")
54
+ when "partially_live"
55
+ print_partial_summary(slug, deploy_id, elapsed)
53
56
  else
54
57
  error("\nDeploy falhou.")
55
58
  end
@@ -74,6 +77,9 @@ module Jatai
74
77
  when "live"
75
78
  success("\nDeploy concluído com sucesso! (#{(Time.now - started).to_i}s)")
76
79
  return
80
+ when "partially_live"
81
+ print_partial_summary(slug, deploy_id, (Time.now - started).to_i)
82
+ return
77
83
  when "failed"
78
84
  error("\nDeploy falhou.")
79
85
  return
@@ -92,6 +98,20 @@ module Jatai
92
98
  error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: jatai status")
93
99
  end
94
100
 
101
+ # O deploy parcial é terminal: o site subiu, mas processo(s) declarados
102
+ # ficaram no release anterior. Sem este ramo a CLI penduraria 20min
103
+ # esperando um "live" que não vem.
104
+ def print_partial_summary(slug, deploy_id, elapsed)
105
+ warning("\nDeploy parcialmente concluído (#{elapsed}s)")
106
+ deploy = api.get("/api/v1/apps/#{slug}/deploys/#{deploy_id}")
107
+ Array(deploy["process_failures"]).each do |failure|
108
+ warning(" #{failure['name']}: #{failure['reason']}")
109
+ end
110
+ warning("O site está no ar na versão nova; os processos acima seguem na anterior.")
111
+ rescue StandardError
112
+ warning(" (detalhe indisponível — ver o deploy no painel)")
113
+ end
114
+
95
115
  def status_label(status)
96
116
  { "queued" => "Na fila...", "building" => "Construindo imagem...",
97
117
  "deploying" => "Deployando..." }[status] || status
@@ -5,24 +5,27 @@ require "yaml"
5
5
  module Jatai
6
6
  module Commands
7
7
  class Init < Base
8
- PRESETS = {
9
- "micro" => "Micro — 256MB RAM, 1 réplica (Grátis)",
10
- "hobby" => "Hobby — 1GB RAM, réplicas ilimitadas (R$ 19/mês)",
11
- "pro" => "Pro — 2GB RAM, réplicas ilimitadas (R$ 69/mês)"
8
+ # O Hex é a unidade computacional da plataforma — web e worker custam o
9
+ # mesmo degrau. Os preços aqui são os que a fatura cobra (Pricing::HEX):
10
+ # esta lista já anunciou "hobby R$ 19" enquanto a plataforma debitava
11
+ # R$ 29, e o cliente só descobria na primeira fatura.
12
+ #
13
+ # O `Hex Dedicado` (R$ 299) fica de fora: ele reserva 2Gi, 51% do nó k3s
14
+ # atual, e só fica vendável após o retype da VM.
15
+ HEX = {
16
+ "base" => "Hex Base — R$ 29/mês (1 grátis por time)",
17
+ "escala_1" => "Hex Escala 1 — R$ 59/mês",
18
+ "escala_2" => "Hex Escala 2 — R$ 119/mês"
12
19
  }.freeze
13
20
 
14
- REGIONS = { "br-se1" => "São Paulo", "br-ne1" => "Nordeste" }.freeze
21
+ # Região única por enquanto (Sudeste). Quando houver multi-região, volta a ser um prompt.
22
+ DEFAULT_REGION = "br-se1".freeze
15
23
  STACKS = {
16
24
  "rails" => "Rails",
17
25
  "node" => "Node.js",
18
26
  "static" => "Estático (HTML/CSS/JS, sem build)",
19
27
  "docker" => "Docker (custom Dockerfile)"
20
28
  }.freeze
21
- DATABASE_TIERS = {
22
- "mini" => "Mini — 512MB, 20 conexões (Grátis)",
23
- "essencial" => "Essencial — 1GB, 50 conexões (R$ 19/mês)",
24
- "padrao" => "Padrão — 5GB, 100 conexões (R$ 49/mês)"
25
- }.freeze
26
29
 
27
30
  def execute(options = {})
28
31
  prompt = TTY::Prompt.new
@@ -43,30 +46,50 @@ module Jatai
43
46
  stack = prompt.select("Stack:", STACKS.map { |k, v| { name: v, value: k } })
44
47
  end
45
48
 
46
- name = prompt.ask("Nome do app:") { |q| q.required true }
47
- 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
- # Sites estáticos não têm banco — não pergunta o tier.
50
- db_tier = stack == "static" ? nil : prompt.select("Banco de dados:", DATABASE_TIERS.map { |k, v| { name: v, value: k } })
49
+ name, slug = ask_name_and_slug(prompt)
50
+ preset = prompt.select("Hex:", HEX.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
- config["database_tier"] = db_tier if db_tier
61
64
 
62
65
  File.write(config_path, YAML.dump(config))
63
66
  success("Arquivo .jatai.yml criado com sucesso!")
67
+ info("Identificador: #{slug}") if slug != name
64
68
  info("Branch: #{branch}")
69
+ # O banco deixou de ser uma pergunta: o Provision::Orchestrator dá um
70
+ # Postgres a todo app não-estático, e o PG Base do app de cortesia do
71
+ # time é isento. Escolher tier aqui só antecipava uma decisão que a aba
72
+ # "Banco de dados" refaz melhor, com o app já rodando.
73
+ info("Banco: PG Base provisionado automaticamente no `jatai up`.") unless stack == "static"
65
74
  info("Execute `jatai up` para criar e fazer deploy do app.")
66
75
  end
67
76
 
68
77
  private
69
78
 
79
+ # O nome é livre; o slug sai dele. Repete a pergunta quando nada
80
+ # aproveitável sobra (nome só de emoji, por exemplo) em vez de gravar um
81
+ # `app` vazio que quebraria a primeira requisição.
82
+ def ask_name_and_slug(prompt)
83
+ loop do
84
+ name = prompt.ask("Nome do app:") { |q| q.required true }
85
+ slug = Slug.from(name)
86
+ return [ name, slug ] if slug
87
+
88
+ error("Não consegui gerar um identificador a partir de #{name.inspect}. " \
89
+ "Use letras ou números no nome.")
90
+ end
91
+ end
92
+
70
93
  def detect_stack
71
94
  if File.exist?(File.join(Dir.pwd, "Gemfile"))
72
95
  gemfile = File.read(File.join(Dir.pwd, "Gemfile"))
@@ -8,9 +8,9 @@ module Jatai
8
8
  namespace :redis
9
9
 
10
10
  MEMORY_TO_PLAN = {
11
- 256 => "starter",
12
- 512 => "pro",
13
- 1024 => "business"
11
+ 256 => "base",
12
+ 512 => "escala_1",
13
+ 1024 => "escala_2"
14
14
  }.freeze
15
15
 
16
16
  desc "status", "Ver status do Redis"
@@ -18,7 +18,7 @@ module Jatai
18
18
  end
19
19
 
20
20
  estimate = api.post("/api/v1/apps/#{slug}/scale/estimate", body: body)
21
- info("Preset: #{estimate["preset"]}")
21
+ info("Hex: #{hex_label(estimate["preset"])}")
22
22
  info("Réplicas: #{estimate["replicas"]}") if estimate["replicas"]
23
23
  info("CPU: #{estimate["cpu_limit"]} | RAM: #{estimate["memory_limit"]}") if estimate["cpu_limit"]
24
24
  info("Custo estimado: R$ #{estimate["monthly_cost"]}/mês")
@@ -14,10 +14,16 @@ module Jatai
14
14
  puts " Stack: #{app["stack"]}"
15
15
  puts " Status: #{colorize_status(app["status"])}"
16
16
  puts " Região: #{app["region"]}"
17
- puts " Preset: #{app["preset"]}"
18
- puts " DB: #{app["database_tier"]}"
19
- subdomain = app["subdomain"] || app["slug"]
20
- puts " URL: https://#{subdomain}.usebrasa.com.br"
17
+ puts " Hex: #{hex_label(app["preset"])}"
18
+ # `database_tier` deixou de existir: o tier do banco vive no plano do
19
+ # addon, e é `database_plan` que a API devolve. Enquanto a CLI lia a
20
+ # chave antiga, esta linha saía sempre vazia.
21
+ puts " Banco: #{pg_label(app["database_plan"])}"
22
+ # A URL vem da API (ClientAppSerializer#url), como no `jatai up`: montá-la
23
+ # aqui hardcodando usebrasa.com.br fazia a CLI anunciar a marca antiga
24
+ # enquanto a documentação e o dashboard já mostram jataiapp.com.br — e
25
+ # deixava a CLI errada de novo no próximo passo do rebrand.
26
+ puts " URL: #{app["url"]}" if app["url"]
21
27
 
22
28
  if app["last_deploy"]
23
29
  deploy = app["last_deploy"]
@@ -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,10 +92,9 @@ 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
- app_params[:database_tier] = config[:database_tier] if config[:database_tier]
77
98
  app_params[:release_command] = config.dig(:release, :command) if config.dig(:release, :command)
78
99
  api.post("/api/v1/apps", body: { app: app_params })
79
100
  end
@@ -145,11 +166,14 @@ module Jatai
145
166
 
146
167
  status = ws.wait_for("DeployStatusChannel",
147
168
  params: { deploy_id: deploy_id },
148
- until_status: %w[live failed],
169
+ until_status: %w[live failed partially_live],
149
170
  timeout: DEPLOY_TIMEOUT)
150
171
 
151
- if status == "live"
172
+ case status
173
+ when "live"
152
174
  true
175
+ when "partially_live"
176
+ partial_deploy_notice
153
177
  else
154
178
  error("\nDeploy falhou.")
155
179
  false
@@ -166,6 +190,14 @@ module Jatai
166
190
  Jatai::Websocket::CableClient.new.tap(&:connect!)
167
191
  end
168
192
 
193
+ # `true` porque, para o `up`, o app ESTÁ no ar: o fluxo seguinte (mostrar
194
+ # a URL) continua válido. O que ficou fora do release sai no painel e no
195
+ # `jatai deploy`, que tem o resumo completo.
196
+ def partial_deploy_notice
197
+ warning("\nDeploy parcialmente concluído — processo(s) fora do release; ver o deploy no painel.")
198
+ true
199
+ end
200
+
169
201
  def warn_fallback(err)
170
202
  $stderr.puts "\n [WebSocket indisponível: #{err.message}. Usando polling...]"
171
203
  end
@@ -205,6 +237,7 @@ module Jatai
205
237
  DEPLOY_MAX_POLLS.times do |i|
206
238
  deploy = api.get("/api/v1/apps/#{slug}/deploys/#{deploy_id}")
207
239
  return true if deploy["status"] == "live"
240
+ return partial_deploy_notice if deploy["status"] == "partially_live"
208
241
  if deploy["status"] == "failed"
209
242
  error("\nDeploy falhou.")
210
243
  return false
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.3.0"
2
+ VERSION = "1.5.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.3.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jataí Cloud
@@ -181,6 +181,7 @@ files:
181
181
  - lib/jatai/commands/status.rb
182
182
  - lib/jatai/commands/up.rb
183
183
  - lib/jatai/config.rb
184
+ - lib/jatai/slug.rb
184
185
  - lib/jatai/source_packer.rb
185
186
  - lib/jatai/version.rb
186
187
  - lib/jatai/websocket/cable_client.rb