jatai 1.0.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +65 -0
- data/exe/jatai +4 -0
- data/lib/jatai/api/client.rb +108 -0
- data/lib/jatai/cli.rb +106 -0
- data/lib/jatai/commands/base.rb +47 -0
- data/lib/jatai/commands/database.rb +75 -0
- data/lib/jatai/commands/deploy.rb +106 -0
- data/lib/jatai/commands/destroy.rb +32 -0
- data/lib/jatai/commands/domains.rb +98 -0
- data/lib/jatai/commands/env.rb +66 -0
- data/lib/jatai/commands/init.rb +83 -0
- data/lib/jatai/commands/login.rb +25 -0
- data/lib/jatai/commands/logs.rb +40 -0
- data/lib/jatai/commands/logs_export.rb +33 -0
- data/lib/jatai/commands/logs_search.rb +47 -0
- data/lib/jatai/commands/redis.rb +79 -0
- data/lib/jatai/commands/scale.rb +39 -0
- data/lib/jatai/commands/status.rb +53 -0
- data/lib/jatai/commands/up.rb +220 -0
- data/lib/jatai/config.rb +63 -0
- data/lib/jatai/source_packer.rb +52 -0
- data/lib/jatai/version.rb +3 -0
- data/lib/jatai/websocket/cable_client.rb +146 -0
- data/lib/jatai.rb +7 -0
- metadata +207 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7061adcfd38cb77a83a6807ac0295ac0699b515f496bb62e508e1eb024ba8779
|
|
4
|
+
data.tar.gz: f445036041493a7e51e316eba1258fab565abd8d3df444e9ec5ad88f28a8c22e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a0a35dfe41431ef623bc0e2ff937304682e0237417105054df3d82deabd5df3d8808d007f03753dae54f11f9a6e7255555fca44b26119b95fae780679e5f88e0
|
|
7
|
+
data.tar.gz: 58b87cf0501262214add03bae6dd12f1b778cba3f43d8666db18049ffc0fee728298759207e3bf2a41b47cb8939f4afa37448d15dfa8f4bc2a1e51703b83e639
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brasa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Jatai CLI
|
|
2
|
+
|
|
3
|
+
CLI para deploy de aplicações na nuvem brasileira (Magalu Cloud). Soberania de dados, faturamento em reais.
|
|
4
|
+
|
|
5
|
+
## Instalacao
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install jatai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Autenticar na plataforma
|
|
15
|
+
jatai login
|
|
16
|
+
|
|
17
|
+
# Inicializar projeto (cria .jatai.yml)
|
|
18
|
+
jatai init
|
|
19
|
+
|
|
20
|
+
# Criar app e fazer primeiro deploy
|
|
21
|
+
jatai up
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Comandos
|
|
25
|
+
|
|
26
|
+
| Comando | Descricao |
|
|
27
|
+
|---------|-----------|
|
|
28
|
+
| `jatai login` | Autenticar na plataforma |
|
|
29
|
+
| `jatai init` | Inicializar projeto no diretorio atual |
|
|
30
|
+
| `jatai up` | Criar app e fazer primeiro deploy |
|
|
31
|
+
| `jatai deploy` | Fazer deploy da aplicacao |
|
|
32
|
+
| `jatai status` | Ver status da aplicacao |
|
|
33
|
+
| `jatai logs` | Ver logs da aplicacao |
|
|
34
|
+
| `jatai env list` | Listar variaveis de ambiente |
|
|
35
|
+
| `jatai env set KEY=VALUE` | Definir variavel de ambiente |
|
|
36
|
+
| `jatai env unset KEY` | Remover variavel de ambiente |
|
|
37
|
+
| `jatai db info` | Informacoes do banco de dados |
|
|
38
|
+
| `jatai db backup` | Criar backup do banco |
|
|
39
|
+
| `jatai db restore BACKUP_ID` | Restaurar backup |
|
|
40
|
+
| `jatai domains list` | Listar dominios customizados |
|
|
41
|
+
| `jatai domains add HOSTNAME` | Adicionar dominio |
|
|
42
|
+
| `jatai domains remove HOSTNAME` | Remover dominio |
|
|
43
|
+
| `jatai domains verify HOSTNAME` | Verificar dominio |
|
|
44
|
+
| `jatai scale` | Escalar a aplicacao |
|
|
45
|
+
| `jatai destroy` | Destruir aplicacao e recursos |
|
|
46
|
+
| `jatai version` | Exibir versao da CLI |
|
|
47
|
+
|
|
48
|
+
## Configuracao
|
|
49
|
+
|
|
50
|
+
O comando `jatai init` cria um arquivo `.jatai.yml` na raiz do projeto:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
app: meu-app
|
|
54
|
+
stack: rails
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Variaveis de Ambiente
|
|
58
|
+
|
|
59
|
+
| Variavel | Descricao | Padrao |
|
|
60
|
+
|----------|-----------|--------|
|
|
61
|
+
| `JATAI_API_URL` | URL da API | `https://api.usebrasa.com.br` |
|
|
62
|
+
|
|
63
|
+
## Licenca
|
|
64
|
+
|
|
65
|
+
MIT License - veja [LICENSE.txt](LICENSE.txt).
|
data/exe/jatai
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "faraday/multipart"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Jatai
|
|
6
|
+
module Api
|
|
7
|
+
class Client
|
|
8
|
+
class ApiError < StandardError
|
|
9
|
+
attr_reader :status, :body
|
|
10
|
+
|
|
11
|
+
def initialize(message, status: nil, body: nil)
|
|
12
|
+
@status = status
|
|
13
|
+
@body = body
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class AuthenticationError < ApiError; end
|
|
19
|
+
class NotFoundError < ApiError; end
|
|
20
|
+
class ValidationError < ApiError; end
|
|
21
|
+
|
|
22
|
+
def initialize(token: nil, api_url: nil)
|
|
23
|
+
@token = token || Config.token
|
|
24
|
+
@api_url = api_url || Config.api_url
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get(path, params: {})
|
|
28
|
+
request(:get, path, params: params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def post(path, body: {})
|
|
32
|
+
request(:post, path, body: body)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def put(path, body: {})
|
|
36
|
+
request(:put, path, body: body)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def patch(path, body: {})
|
|
40
|
+
request(:patch, path, body: body)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete(path)
|
|
44
|
+
request(:delete, path)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def upload(path, file_path:, params: {})
|
|
48
|
+
response = upload_connection.post(path) do |req|
|
|
49
|
+
req.body = params.merge(source: Faraday::Multipart::FilePart.new(file_path, "application/gzip"))
|
|
50
|
+
end
|
|
51
|
+
handle_response(response)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def upload_connection
|
|
57
|
+
@upload_connection ||= Faraday.new(url: @api_url) do |conn|
|
|
58
|
+
conn.request :multipart
|
|
59
|
+
conn.request :url_encoded
|
|
60
|
+
conn.response :json
|
|
61
|
+
conn.headers["Authorization"] = "Bearer #{@token}" if @token
|
|
62
|
+
conn.adapter Faraday.default_adapter
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def request(method, path, body: nil, params: nil)
|
|
67
|
+
response = connection.public_send(method, path) do |req|
|
|
68
|
+
req.params = params if params && !params.empty?
|
|
69
|
+
req.body = body.to_json if body
|
|
70
|
+
end
|
|
71
|
+
handle_response(response)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def connection
|
|
75
|
+
@connection ||= Faraday.new(url: @api_url) do |conn|
|
|
76
|
+
conn.request :json
|
|
77
|
+
conn.response :json
|
|
78
|
+
conn.headers["Content-Type"] = "application/json"
|
|
79
|
+
conn.headers["Authorization"] = "Bearer #{@token}" if @token
|
|
80
|
+
conn.adapter Faraday.default_adapter
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def handle_response(response)
|
|
85
|
+
case response.status
|
|
86
|
+
when 200..299
|
|
87
|
+
response.body
|
|
88
|
+
when 401
|
|
89
|
+
raise AuthenticationError.new("Não autenticado. Execute `jatai login`.", status: 401, body: response.body)
|
|
90
|
+
when 404
|
|
91
|
+
raise NotFoundError.new("Recurso não encontrado.", status: 404, body: response.body)
|
|
92
|
+
when 403
|
|
93
|
+
msg = response.body&.dig("error") || "Sem permissão para esta ação"
|
|
94
|
+
raise ApiError.new(msg, status: 403, body: response.body)
|
|
95
|
+
when 409
|
|
96
|
+
msg = response.body&.dig("error") || "Conflito — recurso já existe ou operação em andamento"
|
|
97
|
+
raise ApiError.new(msg, status: 409, body: response.body)
|
|
98
|
+
when 422
|
|
99
|
+
msg = response.body&.dig("errors")&.join(", ") || response.body&.dig("error") || "Dados inválidos"
|
|
100
|
+
raise ValidationError.new(msg, status: 422, body: response.body)
|
|
101
|
+
else
|
|
102
|
+
msg = response.body&.dig("error") || "Erro na API (#{response.status})"
|
|
103
|
+
raise ApiError.new(msg, status: response.status, body: response.body)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/jatai/cli.rb
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "jatai/commands/login"
|
|
3
|
+
require "jatai/commands/init"
|
|
4
|
+
require "jatai/commands/up"
|
|
5
|
+
require "jatai/commands/deploy"
|
|
6
|
+
require "jatai/commands/logs"
|
|
7
|
+
require "jatai/commands/logs_search"
|
|
8
|
+
require "jatai/commands/logs_export"
|
|
9
|
+
require "jatai/commands/env"
|
|
10
|
+
require "jatai/commands/database"
|
|
11
|
+
require "jatai/commands/scale"
|
|
12
|
+
require "jatai/commands/status"
|
|
13
|
+
require "jatai/commands/destroy"
|
|
14
|
+
require "jatai/commands/domains"
|
|
15
|
+
require "jatai/commands/redis"
|
|
16
|
+
|
|
17
|
+
module Jatai
|
|
18
|
+
class CLI < Thor
|
|
19
|
+
def self.exit_on_failure?
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "login", "Autenticar na plataforma Jatai"
|
|
24
|
+
def login
|
|
25
|
+
Commands::Login.new.execute
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "init", "Inicializar projeto no diretório atual"
|
|
29
|
+
def init
|
|
30
|
+
Commands::Init.new.execute
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "up", "Criar app e fazer primeiro deploy"
|
|
34
|
+
def up
|
|
35
|
+
Commands::Up.new.execute
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
desc "deploy", "Fazer deploy da aplicação"
|
|
39
|
+
option :branch, type: :string, desc: "Branch para deploy"
|
|
40
|
+
def deploy
|
|
41
|
+
Commands::Deploy.new.execute(options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
desc "logs", "Ver logs da aplicação em tempo real"
|
|
45
|
+
option :tail, type: :numeric, default: 100, desc: "Número de linhas"
|
|
46
|
+
def logs
|
|
47
|
+
Commands::Logs.new.execute(options)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
desc "logs:search", "Buscar nos logs da aplicação"
|
|
51
|
+
option :query, type: :string, required: true, desc: "Termo de busca"
|
|
52
|
+
option :level, type: :string, desc: "Filtrar por nível (error, warn, info)"
|
|
53
|
+
option :source, type: :string, desc: "Filtrar por source"
|
|
54
|
+
option :since, type: :string, desc: "Período (ex: 1h, 24h, 7d)"
|
|
55
|
+
option :limit, type: :string, default: "100", desc: "Limite de resultados"
|
|
56
|
+
define_method("logs:search") do
|
|
57
|
+
Commands::LogsSearch.new.execute(options)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
desc "logs:export", "Exportar logs da aplicação"
|
|
61
|
+
option :format, type: :string, default: "json", desc: "Formato de exportação (json, csv)"
|
|
62
|
+
option :since, type: :string, desc: "Data/hora inicial"
|
|
63
|
+
option :until, type: :string, desc: "Data/hora final"
|
|
64
|
+
option :level, type: :string, desc: "Filtrar por nível (error, warn, info)"
|
|
65
|
+
define_method("logs:export") do
|
|
66
|
+
Commands::LogsExport.new.execute(options)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
desc "env SUBCOMMAND", "Gerenciar variáveis de ambiente"
|
|
70
|
+
subcommand "env", Commands::Env
|
|
71
|
+
|
|
72
|
+
desc "db SUBCOMMAND", "Gerenciar banco de dados"
|
|
73
|
+
subcommand "db", Commands::Database
|
|
74
|
+
|
|
75
|
+
desc "domains SUBCOMMAND", "Gerenciar domínios customizados"
|
|
76
|
+
subcommand "domains", Commands::Domains
|
|
77
|
+
|
|
78
|
+
desc "redis SUBCOMMAND", "Gerenciar Redis cache"
|
|
79
|
+
subcommand "redis", Commands::Redis
|
|
80
|
+
|
|
81
|
+
desc "scale", "Escalar a aplicação"
|
|
82
|
+
option :replicas, type: :numeric, desc: "Número de réplicas"
|
|
83
|
+
option :preset, type: :string, desc: "Preset (micro, hobby, pro)"
|
|
84
|
+
def scale
|
|
85
|
+
Commands::Scale.new.execute(options)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
desc "status", "Ver status da aplicação"
|
|
89
|
+
def status
|
|
90
|
+
Commands::Status.new.execute
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
desc "destroy", "Destruir aplicação e recursos"
|
|
94
|
+
def destroy
|
|
95
|
+
Commands::Destroy.new.execute
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
desc "version", "Exibir versão da CLI"
|
|
99
|
+
def version
|
|
100
|
+
puts "jatai #{Jatai::VERSION}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
map "-v" => :version
|
|
104
|
+
map "--version" => :version
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "pastel"
|
|
3
|
+
|
|
4
|
+
module Jatai
|
|
5
|
+
module Commands
|
|
6
|
+
class Base
|
|
7
|
+
def pastel
|
|
8
|
+
@pastel ||= Pastel.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def api
|
|
12
|
+
@api ||= Api::Client.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def project_config
|
|
16
|
+
@project_config ||= begin
|
|
17
|
+
config_path = Config.project_file_path
|
|
18
|
+
unless File.exist?(config_path)
|
|
19
|
+
abort pastel.red("Arquivo .jatai.yml não encontrado. Execute `jatai init` primeiro.")
|
|
20
|
+
end
|
|
21
|
+
YAML.safe_load(File.read(config_path), symbolize_names: true)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def app_slug
|
|
26
|
+
project_config[:app]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def require_auth!
|
|
30
|
+
return if Config.token
|
|
31
|
+
abort pastel.red("Não autenticado. Execute `jatai login` primeiro.")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def success(msg)
|
|
35
|
+
puts pastel.green(msg)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def error(msg)
|
|
39
|
+
puts pastel.red(msg)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def info(msg)
|
|
43
|
+
puts pastel.cyan(msg)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "tty-prompt"
|
|
3
|
+
require "jatai/commands/base"
|
|
4
|
+
|
|
5
|
+
module Jatai
|
|
6
|
+
module Commands
|
|
7
|
+
class Database < Thor
|
|
8
|
+
namespace :db
|
|
9
|
+
|
|
10
|
+
desc "info", "Informações do banco de dados"
|
|
11
|
+
def info
|
|
12
|
+
slug = helper.app_slug
|
|
13
|
+
|
|
14
|
+
db = helper.api.get("/api/v1/apps/#{slug}/database")
|
|
15
|
+
|
|
16
|
+
puts helper.pastel.bold("Banco de dados de #{slug}")
|
|
17
|
+
puts " Tier: #{db["database_tier"] || db["mode"]}"
|
|
18
|
+
puts " Host: #{db["host"]}"
|
|
19
|
+
puts " Porta: #{db["port"]}"
|
|
20
|
+
puts " Nome: #{db["name"]}"
|
|
21
|
+
puts " Status: #{db["status"]}"
|
|
22
|
+
rescue Api::Client::ApiError => e
|
|
23
|
+
helper.error("Erro: #{e.message}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc "backups", "Listar backups disponíveis"
|
|
27
|
+
def backups
|
|
28
|
+
slug = helper.app_slug
|
|
29
|
+
|
|
30
|
+
result = helper.api.get("/api/v1/apps/#{slug}/database/backups")
|
|
31
|
+
backups_list = result["backups"] || []
|
|
32
|
+
|
|
33
|
+
if backups_list.empty?
|
|
34
|
+
helper.info("Nenhum backup disponível.")
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
backups_list.each do |b|
|
|
39
|
+
puts "#{helper.pastel.bold(b["id"])} — #{b["name"]} (#{b["created_at"]})"
|
|
40
|
+
end
|
|
41
|
+
rescue Api::Client::ApiError => e
|
|
42
|
+
helper.error("Erro: #{e.message}")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "backup", "Criar backup do banco de dados"
|
|
46
|
+
def backup
|
|
47
|
+
slug = helper.app_slug
|
|
48
|
+
|
|
49
|
+
name = "manual-#{Time.now.strftime("%Y%m%d-%H%M%S")}"
|
|
50
|
+
helper.info("Criando backup do banco de dados...")
|
|
51
|
+
result = helper.api.post("/api/v1/apps/#{slug}/database/backups", body: { name: name })
|
|
52
|
+
helper.success("Backup criado: #{result["id"]} (#{name})")
|
|
53
|
+
rescue Api::Client::ApiError => e
|
|
54
|
+
helper.error("Erro: #{e.message}")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc "restore BACKUP_ID", "Restaurar backup do banco de dados"
|
|
58
|
+
def restore(backup_id)
|
|
59
|
+
helper.error("Recurso em desenvolvimento. Restauração de backups ainda não está disponível.")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def helper
|
|
65
|
+
@helper ||= begin
|
|
66
|
+
h = DatabaseHelper.new
|
|
67
|
+
h.require_auth!
|
|
68
|
+
h
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class DatabaseHelper < Base; end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require "jatai/commands/base"
|
|
2
|
+
require "jatai/source_packer"
|
|
3
|
+
require "tty-spinner"
|
|
4
|
+
|
|
5
|
+
module Jatai
|
|
6
|
+
module Commands
|
|
7
|
+
class Deploy < Base
|
|
8
|
+
POLL_INTERVAL = 5
|
|
9
|
+
DEPLOY_TIMEOUT = 1800
|
|
10
|
+
DEPLOY_MAX_POLLS = 360
|
|
11
|
+
|
|
12
|
+
def execute(options = {})
|
|
13
|
+
require_auth!
|
|
14
|
+
slug = app_slug
|
|
15
|
+
|
|
16
|
+
info("Empacotando código...")
|
|
17
|
+
archive_path = SourcePacker.pack
|
|
18
|
+
size_kb = File.size(archive_path) / 1024
|
|
19
|
+
|
|
20
|
+
spinner = TTY::Spinner.new("#{pastel.cyan(" Enviando #{size_kb}KB...")} :spinner", format: :dots)
|
|
21
|
+
spinner.auto_spin
|
|
22
|
+
deploy = api.upload("/api/v1/apps/#{slug}/deploys",
|
|
23
|
+
file_path: archive_path,
|
|
24
|
+
params: { branch: options["branch"] || project_config[:branch] || "main" })
|
|
25
|
+
spinner.success(pastel.green("enviado!"))
|
|
26
|
+
info("Deploy ##{deploy["id"]} criado.")
|
|
27
|
+
info(" Na fila...")
|
|
28
|
+
|
|
29
|
+
wait_for_deploy_complete(slug, deploy["id"])
|
|
30
|
+
rescue Api::Client::ApiError => e
|
|
31
|
+
error("Erro: #{e.message}")
|
|
32
|
+
ensure
|
|
33
|
+
FileUtils.rm_f(archive_path) if archive_path
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def wait_for_deploy_complete(slug, deploy_id)
|
|
39
|
+
ws = connect_websocket
|
|
40
|
+
started = Time.now
|
|
41
|
+
|
|
42
|
+
ws.on_log { |msg| print msg["line"] + "\n" if msg.is_a?(Hash) && msg["line"] }
|
|
43
|
+
ws.subscribe("DeployLogChannel", deploy_id: deploy_id)
|
|
44
|
+
|
|
45
|
+
status = ws.wait_for("DeployStatusChannel",
|
|
46
|
+
params: { deploy_id: deploy_id },
|
|
47
|
+
until_status: %w[live failed],
|
|
48
|
+
timeout: DEPLOY_TIMEOUT)
|
|
49
|
+
|
|
50
|
+
elapsed = (Time.now - started).to_i
|
|
51
|
+
if status == "live"
|
|
52
|
+
success("\nDeploy concluído com sucesso! (#{elapsed}s)")
|
|
53
|
+
else
|
|
54
|
+
error("\nDeploy falhou.")
|
|
55
|
+
end
|
|
56
|
+
rescue Jatai::Websocket::CableClient::ConnectionError => e
|
|
57
|
+
$stderr.puts "\n [WebSocket indisponível: #{e.message}. Usando polling...]"
|
|
58
|
+
wait_for_deploy_polling(slug, deploy_id)
|
|
59
|
+
ensure
|
|
60
|
+
ws&.disconnect
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def connect_websocket
|
|
64
|
+
require "jatai/websocket/cable_client"
|
|
65
|
+
Jatai::Websocket::CableClient.new.tap(&:connect!)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def wait_for_deploy_polling(slug, deploy_id)
|
|
69
|
+
started = Time.now
|
|
70
|
+
last_status = nil
|
|
71
|
+
DEPLOY_MAX_POLLS.times do |i|
|
|
72
|
+
deploy = api.get("/api/v1/apps/#{slug}/deploys/#{deploy_id}")
|
|
73
|
+
case deploy["status"]
|
|
74
|
+
when "live"
|
|
75
|
+
success("\nDeploy concluído com sucesso! (#{(Time.now - started).to_i}s)")
|
|
76
|
+
return
|
|
77
|
+
when "failed"
|
|
78
|
+
error("\nDeploy falhou.")
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
if deploy["status"] != last_status
|
|
82
|
+
print "\n #{status_label(deploy["status"])}"
|
|
83
|
+
last_status = deploy["status"]
|
|
84
|
+
end
|
|
85
|
+
print "."
|
|
86
|
+
show_elapsed(started) if (i % 12).zero? && i > 0
|
|
87
|
+
sleep(POLL_INTERVAL)
|
|
88
|
+
rescue Faraday::Error, Errno::ECONNREFUSED, IO::TimeoutError
|
|
89
|
+
print "!"
|
|
90
|
+
sleep(POLL_INTERVAL)
|
|
91
|
+
end
|
|
92
|
+
error("\nTimeout (#{(Time.now - started).to_i}s). Verifique com: jatai status")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def status_label(status)
|
|
96
|
+
{ "queued" => "Na fila...", "building" => "Construindo imagem...",
|
|
97
|
+
"deploying" => "Deployando..." }[status] || status
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def show_elapsed(started)
|
|
101
|
+
elapsed = (Time.now - started).to_i
|
|
102
|
+
print " (#{elapsed / 60}m#{elapsed % 60}s)"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "jatai/commands/base"
|
|
2
|
+
require "tty-prompt"
|
|
3
|
+
|
|
4
|
+
module Jatai
|
|
5
|
+
module Commands
|
|
6
|
+
class Destroy < Base
|
|
7
|
+
def execute(options = {})
|
|
8
|
+
require_auth!
|
|
9
|
+
slug = app_slug
|
|
10
|
+
|
|
11
|
+
prompt = TTY::Prompt.new
|
|
12
|
+
unless prompt.yes?("Tem certeza que deseja destruir #{slug}? Esta ação é irreversível.")
|
|
13
|
+
info("Operação cancelada.")
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
confirmation = prompt.ask("Digite o nome do app para confirmar (#{slug}):")
|
|
18
|
+
unless confirmation == slug
|
|
19
|
+
error("Nome não confere. Operação cancelada.")
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
info("Destruindo #{slug}...")
|
|
24
|
+
result = api.delete("/api/v1/apps/#{slug}")
|
|
25
|
+
success("App #{slug} marcado para destruição (status: #{result["status"]}).")
|
|
26
|
+
info("Os recursos serão removidos em segundo plano.")
|
|
27
|
+
rescue Api::Client::ApiError => e
|
|
28
|
+
error("Erro: #{e.message}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "jatai/commands/base"
|
|
3
|
+
|
|
4
|
+
module Jatai
|
|
5
|
+
module Commands
|
|
6
|
+
class Domains < Thor
|
|
7
|
+
namespace :domains
|
|
8
|
+
|
|
9
|
+
desc "list", "Listar domínios customizados"
|
|
10
|
+
def list
|
|
11
|
+
slug = helper.app_slug
|
|
12
|
+
|
|
13
|
+
domains = helper.api.get("/api/v1/apps/#{slug}/domains")
|
|
14
|
+
|
|
15
|
+
if domains.nil? || domains.empty?
|
|
16
|
+
helper.info("Nenhum domínio configurado.")
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
domains.each do |domain|
|
|
21
|
+
status_icon = domain["status"] == "active" ? helper.pastel.green("●") : helper.pastel.yellow("○")
|
|
22
|
+
puts "#{status_icon} #{helper.pastel.bold(domain["hostname"])} (#{domain["status"]})"
|
|
23
|
+
puts " Token: #{domain["verification_token"]}" if domain["status"] == "pending"
|
|
24
|
+
end
|
|
25
|
+
rescue Api::Client::ApiError => e
|
|
26
|
+
helper.error("Erro: #{e.message}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "add HOSTNAME", "Adicionar domínio customizado"
|
|
30
|
+
def add(hostname)
|
|
31
|
+
slug = helper.app_slug
|
|
32
|
+
|
|
33
|
+
result = helper.api.post("/api/v1/apps/#{slug}/domains", body: { domain: { hostname: hostname } })
|
|
34
|
+
app = helper.api.get("/api/v1/apps/#{slug}")
|
|
35
|
+
helper.success("Domínio #{hostname} adicionado.")
|
|
36
|
+
helper.info("Token de verificação: #{result["verification_token"]}")
|
|
37
|
+
helper.info("Adicione um registro CNAME apontando para #{app["subdomain"]}.usebrasa.com.br")
|
|
38
|
+
rescue Api::Client::ValidationError => e
|
|
39
|
+
helper.error("Erro: #{e.message}")
|
|
40
|
+
rescue Api::Client::ApiError => e
|
|
41
|
+
helper.error("Erro: #{e.message}")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
desc "remove HOSTNAME", "Remover domínio customizado"
|
|
45
|
+
def remove(hostname)
|
|
46
|
+
slug = helper.app_slug
|
|
47
|
+
|
|
48
|
+
domains = helper.api.get("/api/v1/apps/#{slug}/domains")
|
|
49
|
+
domain = domains&.find { |d| d["hostname"] == hostname }
|
|
50
|
+
|
|
51
|
+
unless domain
|
|
52
|
+
helper.error("Domínio #{hostname} não encontrado.")
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
helper.api.delete("/api/v1/apps/#{slug}/domains/#{domain["id"]}")
|
|
57
|
+
helper.success("Domínio #{hostname} removido.")
|
|
58
|
+
rescue Api::Client::ApiError => e
|
|
59
|
+
helper.error("Erro: #{e.message}")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc "verify HOSTNAME", "Verificar domínio customizado"
|
|
63
|
+
def verify(hostname)
|
|
64
|
+
slug = helper.app_slug
|
|
65
|
+
|
|
66
|
+
domains = helper.api.get("/api/v1/apps/#{slug}/domains")
|
|
67
|
+
domain = domains&.find { |d| d["hostname"] == hostname }
|
|
68
|
+
|
|
69
|
+
unless domain
|
|
70
|
+
helper.error("Domínio #{hostname} não encontrado.")
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
result = helper.api.post("/api/v1/apps/#{slug}/domains/#{domain["id"]}/verify")
|
|
75
|
+
|
|
76
|
+
if result["status"] == "active"
|
|
77
|
+
helper.success("Domínio #{hostname} verificado com sucesso!")
|
|
78
|
+
else
|
|
79
|
+
helper.error("Verificação falhou. Certifique-se de que o registro DNS está configurado.")
|
|
80
|
+
end
|
|
81
|
+
rescue Api::Client::ApiError => e
|
|
82
|
+
helper.error("Erro: #{e.message}")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def helper
|
|
88
|
+
@helper ||= begin
|
|
89
|
+
h = DomainsHelper.new
|
|
90
|
+
h.require_auth!
|
|
91
|
+
h
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class DomainsHelper < Base; end
|
|
97
|
+
end
|
|
98
|
+
end
|