brasa 0.3.1 → 0.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: e07ee00f7ac9e995d27c4e920a9408358f9d8d026682cac95fb43cd89b63289b
4
- data.tar.gz: 5ff8c1b1dfbacbdfecd63f4fb1a6bb06a6d438ad3a69b3bc7296d784a291b2b1
3
+ metadata.gz: 10c05491bcc0f0acb8baea99ebdf17b598c2d2306cddd27f37b20036649f7877
4
+ data.tar.gz: 4b5ef6aacd6da982766cafc26e0fbbcfaa06748fe843a25049101c6611bb7bcb
5
5
  SHA512:
6
- metadata.gz: 6411273f8dc5913936a36e4df53d71c1e582338ae59a43fa3cd1a90e8e1aeb48d89e678a0108af9fcf6d019c6eabf1548a3dd75e4aee821bc1eda7972df49078
7
- data.tar.gz: e9f02d023fc2a852d8ec826afa5801d89ebb133d278ccae56f59efc971e7a3c78ac9e8d449080e902a275543798d43c05259fa2be2e92fbf0e080f15f37edc20
6
+ metadata.gz: a3f40bb61b742e672444fad37d5ed851c3bef5b8b1acdffe8a06ec7aff3b21f9ba9499cda34d882416b5d0d1e8ea6455785e9c185d50b20a53315fbc2a334112
7
+ data.tar.gz: 9feffff14f3712c98cdb2070791183472425b378d055b733765802762421e95349d503e68bd2f91316b05f48db320a642ba38c61ed81b6e6715a1853f91456e1
@@ -9,53 +9,66 @@ module Brasa
9
9
 
10
10
  desc "info", "Informações do banco de dados"
11
11
  def info
12
- helper = DatabaseHelper.new
13
- helper.require_auth!
14
12
  slug = helper.app_slug
15
13
 
16
14
  db = helper.api.get("/api/v1/apps/#{slug}/database")
17
15
 
18
16
  puts helper.pastel.bold("Banco de dados de #{slug}")
17
+ puts " Engine: #{db["engine"]} #{db["engine_version"]}"
18
+ puts " Modo: #{db["mode"]}"
19
19
  puts " Host: #{db["host"]}"
20
20
  puts " Porta: #{db["port"]}"
21
21
  puts " Nome: #{db["name"]}"
22
- puts " Usuário: #{db["username"]}"
23
22
  puts " Status: #{db["status"]}"
24
- puts " Tamanho: #{db["size"]}" if db["size"]
23
+ puts " Machine: #{db["machine_type"]}" if db["machine_type"] && db["machine_type"] != "embedded"
25
24
  rescue Api::Client::ApiError => e
26
- DatabaseHelper.new.error("Erro: #{e.message}")
25
+ helper.error("Erro: #{e.message}")
26
+ end
27
+
28
+ desc "backups", "Listar backups disponíveis"
29
+ def backups
30
+ slug = helper.app_slug
31
+
32
+ result = helper.api.get("/api/v1/apps/#{slug}/database/backups")
33
+ backups_list = result["backups"] || []
34
+
35
+ if backups_list.empty?
36
+ helper.info("Nenhum backup disponível.")
37
+ return
38
+ end
39
+
40
+ backups_list.each do |b|
41
+ puts "#{helper.pastel.bold(b["id"])} — #{b["name"]} (#{b["created_at"]})"
42
+ end
43
+ rescue Api::Client::ApiError => e
44
+ helper.error("Erro: #{e.message}")
27
45
  end
28
46
 
29
47
  desc "backup", "Criar backup do banco de dados"
30
48
  def backup
31
- helper = DatabaseHelper.new
32
- helper.require_auth!
33
49
  slug = helper.app_slug
34
50
 
51
+ name = "manual-#{Time.now.strftime("%Y%m%d-%H%M%S")}"
35
52
  helper.info("Criando backup do banco de dados...")
36
- result = helper.api.post("/api/v1/apps/#{slug}/database/backups")
37
- helper.success("Backup criado: #{result["filename"]}")
53
+ result = helper.api.post("/api/v1/apps/#{slug}/database/backups", body: { name: name })
54
+ helper.success("Backup criado: #{result["id"]} (#{name})")
38
55
  rescue Api::Client::ApiError => e
39
- DatabaseHelper.new.error("Erro: #{e.message}")
56
+ helper.error("Erro: #{e.message}")
40
57
  end
41
58
 
42
59
  desc "restore BACKUP_ID", "Restaurar backup do banco de dados"
43
60
  def restore(backup_id)
44
- helper = DatabaseHelper.new
45
- helper.require_auth!
46
- slug = helper.app_slug
61
+ helper.error("Recurso em desenvolvimento. Restauração de backups ainda não está disponível.")
62
+ end
47
63
 
48
- prompt = TTY::Prompt.new
49
- unless prompt.yes?("Restaurar backup #{backup_id}? Isso substituirá os dados atuais.")
50
- helper.info("Restauração cancelada.")
51
- return
52
- end
64
+ private
53
65
 
54
- helper.info("Restaurando backup...")
55
- helper.api.post("/api/v1/apps/#{slug}/database/backups/#{backup_id}/restore")
56
- helper.success("Backup restaurado com sucesso!")
57
- rescue Api::Client::ApiError => e
58
- DatabaseHelper.new.error("Erro: #{e.message}")
66
+ def helper
67
+ @helper ||= begin
68
+ h = DatabaseHelper.new
69
+ h.require_auth!
70
+ h
71
+ end
59
72
  end
60
73
  end
61
74
 
@@ -20,7 +20,7 @@ module Brasa
20
20
  deploy = api.upload(
21
21
  "/api/v1/apps/#{slug}/deploys",
22
22
  file_path: archive_path,
23
- params: { branch: options["branch"] || "main" }
23
+ params: { branch: options["branch"] || project_config[:branch] || "main" }
24
24
  )
25
25
  info("Deploy ##{deploy["id"]} criado. Aguardando build e deploy...")
26
26
 
@@ -21,8 +21,9 @@ module Brasa
21
21
  end
22
22
 
23
23
  info("Destruindo #{slug}...")
24
- api.delete("/api/v1/apps/#{slug}")
25
- success("App #{slug} destruído com sucesso.")
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.")
26
27
  rescue Api::Client::ApiError => e
27
28
  error("Erro: #{e.message}")
28
29
  end
@@ -8,8 +8,6 @@ module Brasa
8
8
 
9
9
  desc "list", "Listar domínios customizados"
10
10
  def list
11
- helper = DomainsHelper.new
12
- helper.require_auth!
13
11
  slug = helper.app_slug
14
12
 
15
13
  domains = helper.api.get("/api/v1/apps/#{slug}/domains")
@@ -25,29 +23,26 @@ module Brasa
25
23
  puts " Token: #{domain["verification_token"]}" if domain["status"] == "pending"
26
24
  end
27
25
  rescue Api::Client::ApiError => e
28
- DomainsHelper.new.error("Erro: #{e.message}")
26
+ helper.error("Erro: #{e.message}")
29
27
  end
30
28
 
31
29
  desc "add HOSTNAME", "Adicionar domínio customizado"
32
30
  def add(hostname)
33
- helper = DomainsHelper.new
34
- helper.require_auth!
35
31
  slug = helper.app_slug
36
32
 
37
33
  result = helper.api.post("/api/v1/apps/#{slug}/domains", body: { domain: { hostname: hostname } })
34
+ app = helper.api.get("/api/v1/apps/#{slug}")
38
35
  helper.success("Domínio #{hostname} adicionado.")
39
36
  helper.info("Token de verificação: #{result["verification_token"]}")
40
- helper.info("Adicione um registro CNAME ou TXT apontando para #{slug}.usebrasa.com.br")
37
+ helper.info("Adicione um registro CNAME apontando para #{app["subdomain"]}.usebrasa.com.br")
41
38
  rescue Api::Client::ValidationError => e
42
- DomainsHelper.new.error("Erro: #{e.message}")
39
+ helper.error("Erro: #{e.message}")
43
40
  rescue Api::Client::ApiError => e
44
- DomainsHelper.new.error("Erro: #{e.message}")
41
+ helper.error("Erro: #{e.message}")
45
42
  end
46
43
 
47
44
  desc "remove HOSTNAME", "Remover domínio customizado"
48
45
  def remove(hostname)
49
- helper = DomainsHelper.new
50
- helper.require_auth!
51
46
  slug = helper.app_slug
52
47
 
53
48
  domains = helper.api.get("/api/v1/apps/#{slug}/domains")
@@ -61,13 +56,11 @@ module Brasa
61
56
  helper.api.delete("/api/v1/apps/#{slug}/domains/#{domain["id"]}")
62
57
  helper.success("Domínio #{hostname} removido.")
63
58
  rescue Api::Client::ApiError => e
64
- DomainsHelper.new.error("Erro: #{e.message}")
59
+ helper.error("Erro: #{e.message}")
65
60
  end
66
61
 
67
62
  desc "verify HOSTNAME", "Verificar domínio customizado"
68
63
  def verify(hostname)
69
- helper = DomainsHelper.new
70
- helper.require_auth!
71
64
  slug = helper.app_slug
72
65
 
73
66
  domains = helper.api.get("/api/v1/apps/#{slug}/domains")
@@ -86,7 +79,17 @@ module Brasa
86
79
  helper.error("Verificação falhou. Certifique-se de que o registro DNS está configurado.")
87
80
  end
88
81
  rescue Api::Client::ApiError => e
89
- DomainsHelper.new.error("Erro: #{e.message}")
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
90
93
  end
91
94
  end
92
95
 
@@ -5,8 +5,17 @@ require "yaml"
5
5
  module Brasa
6
6
  module Commands
7
7
  class Init < Base
8
- PRESETS = %w[hobby production scale].freeze
8
+ PRESETS = {
9
+ "nano" => "Nano — 1 vCPU, 1GB RAM, DB embedded (R$ 19/mês)",
10
+ "micro" => "Micro — 1 vCPU, 1GB RAM, DB embedded (R$ 29/mês)",
11
+ "hobby" => "Hobby — 1 vCPU, 1GB RAM, DB managed (R$ 99/mês)",
12
+ "production" => "Production — 4 vCPU, 4GB RAM, DB managed + storage (R$ 299/mês)",
13
+ "scale" => "Scale — 8 vCPU, 8GB RAM, DB managed + storage (R$ 599/mês)"
14
+ }.freeze
15
+
9
16
  REGIONS = { "br-se1" => "São Paulo", "br-ne1" => "Nordeste" }.freeze
17
+ STACKS = { "rails" => "Rails", "node" => "Node.js", "docker" => "Docker (custom Dockerfile)" }.freeze
18
+ DATABASE_ENGINES = { "postgresql" => "PostgreSQL", "mysql" => "MySQL" }.freeze
10
19
 
11
20
  def execute(options = {})
12
21
  prompt = TTY::Prompt.new
@@ -17,27 +26,35 @@ module Brasa
17
26
  end
18
27
 
19
28
  stack = detect_stack
20
- unless stack
21
- error("Não foi possível detectar o stack do projeto. Certifique-se de ter um Gemfile (Rails) ou package.json (Node.js).")
22
- return
29
+ if stack
30
+ info("Stack detectado: #{STACKS[stack]}")
31
+ unless prompt.yes?("Confirmar stack #{STACKS[stack]}?")
32
+ stack = prompt.select("Stack:", STACKS.map { |k, v| { name: v, value: k } })
33
+ end
34
+ else
35
+ info("Não foi possível detectar o stack automaticamente.")
36
+ stack = prompt.select("Stack:", STACKS.map { |k, v| { name: v, value: k } })
23
37
  end
24
38
 
25
- info("Stack detectado: #{stack == "rails" ? "Rails" : "Node.js"}")
26
-
27
- name = prompt.ask("Nome do app:")
28
- preset = prompt.select("Preset:", PRESETS)
39
+ name = prompt.ask("Nome do app:") { |q| q.required true }
40
+ preset = prompt.select("Preset:", PRESETS.map { |k, v| { name: v, value: k } })
29
41
  region = prompt.select("Região:", REGIONS.map { |k, v| { name: "#{v} (#{k})", value: k } })
42
+ engine = prompt.select("Banco de dados:", DATABASE_ENGINES.map { |k, v| { name: v, value: k } })
43
+ branch = detect_git_branch || "main"
30
44
 
31
45
  config = {
32
46
  "app" => name,
33
47
  "stack" => stack,
34
48
  "preset" => preset,
35
49
  "region" => region,
36
- "branch" => "main"
50
+ "branch" => branch,
51
+ "database_engine" => engine
37
52
  }
38
53
 
39
54
  File.write(config_path, YAML.dump(config))
40
55
  success("Arquivo .brasa.yml criado com sucesso!")
56
+ info("Branch: #{branch}")
57
+ info("Execute `brasa up` para criar e fazer deploy do app.")
41
58
  end
42
59
 
43
60
  private
@@ -48,12 +65,17 @@ module Brasa
48
65
  return "rails" if gemfile.include?("rails")
49
66
  end
50
67
 
51
- if File.exist?(File.join(Dir.pwd, "package.json"))
52
- return "node"
53
- end
68
+ return "node" if File.exist?(File.join(Dir.pwd, "package.json"))
69
+
70
+ return "docker" if File.exist?(File.join(Dir.pwd, "Dockerfile"))
54
71
 
55
72
  nil
56
73
  end
74
+
75
+ def detect_git_branch
76
+ branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
77
+ branch.empty? ? nil : branch
78
+ end
57
79
  end
58
80
  end
59
81
  end
@@ -20,9 +20,17 @@ module Brasa
20
20
  logs.each do |entry|
21
21
  timestamp = entry["timestamp"]
22
22
  source = entry["source"]
23
+ level = entry["level"]
23
24
  message = entry["message"]
24
25
 
25
- puts "#{pastel.dim(timestamp)} #{pastel.cyan(source)} #{message}"
26
+ level_str = case level
27
+ when "error" then pastel.red(level)
28
+ when "warn" then pastel.yellow(level)
29
+ when "info" then pastel.green(level)
30
+ else pastel.dim(level || "log")
31
+ end
32
+
33
+ puts "#{pastel.dim(timestamp)} #{pastel.cyan(source)} [#{level_str}] #{message}"
26
34
  end
27
35
  rescue Api::Client::ApiError => e
28
36
  error("Erro: #{e.message}")
@@ -15,10 +15,15 @@ module Brasa
15
15
 
16
16
  result = api.get("/api/v1/apps/#{slug}/logs/export", params: params)
17
17
 
18
- if result.is_a?(Array)
18
+ case result
19
+ when Array
19
20
  puts JSON.pretty_generate(result)
20
- else
21
+ when Hash
22
+ puts JSON.pretty_generate(result)
23
+ when String
21
24
  puts result
25
+ else
26
+ puts result.to_s
22
27
  end
23
28
  rescue Api::Client::ApiError => e
24
29
  error("Erro: #{e.message}")
@@ -1,4 +1,5 @@
1
1
  require "thor"
2
+ require "tty-prompt"
2
3
  require "brasa/commands/base"
3
4
 
4
5
  module Brasa
@@ -14,8 +15,6 @@ module Brasa
14
15
 
15
16
  desc "status", "Ver status do Redis"
16
17
  def status
17
- helper = RedisHelper.new
18
- helper.require_auth!
19
18
  slug = helper.app_slug
20
19
 
21
20
  addon = helper.api.get("/api/v1/apps/#{slug}/addons/cache")
@@ -26,14 +25,12 @@ module Brasa
26
25
  puts " Memória: #{addon.dig("config", "memory_mb")}MB"
27
26
  puts " Custo: R$ #{addon["monthly_cost_brl"]}/mês"
28
27
  rescue Api::Client::ApiError => e
29
- RedisHelper.new.error("Erro: #{e.message}")
28
+ helper.error("Erro: #{e.message}")
30
29
  end
31
30
 
32
31
  desc "enable", "Habilitar Redis para o app"
33
32
  option :memory, type: :numeric, default: 256, desc: "Memória em MB (256, 512, 1024)"
34
33
  def enable
35
- helper = RedisHelper.new
36
- helper.require_auth!
37
34
  slug = helper.app_slug
38
35
 
39
36
  plan = MEMORY_TO_PLAN[options[:memory]]
@@ -43,16 +40,14 @@ module Brasa
43
40
  end
44
41
 
45
42
  helper.info("Habilitando Redis com #{options[:memory]}MB (plano #{plan})...")
46
- result = helper.api.post("/api/v1/apps/#{slug}/addons", { addon: { slug: "cache", plan: plan } })
43
+ result = helper.api.post("/api/v1/apps/#{slug}/addons", body: { addon: { slug: "cache", plan: plan } })
47
44
  helper.success("Redis habilitado! Status: #{result["status"]}")
48
45
  rescue Api::Client::ApiError => e
49
- RedisHelper.new.error("Erro: #{e.message}")
46
+ helper.error("Erro: #{e.message}")
50
47
  end
51
48
 
52
49
  desc "disable", "Remover Redis do app"
53
50
  def disable
54
- helper = RedisHelper.new
55
- helper.require_auth!
56
51
  slug = helper.app_slug
57
52
 
58
53
  prompt = TTY::Prompt.new
@@ -65,7 +60,17 @@ module Brasa
65
60
  helper.api.delete("/api/v1/apps/#{slug}/addons/cache")
66
61
  helper.success("Redis removido com sucesso!")
67
62
  rescue Api::Client::ApiError => e
68
- RedisHelper.new.error("Erro: #{e.message}")
63
+ helper.error("Erro: #{e.message}")
64
+ end
65
+
66
+ private
67
+
68
+ def helper
69
+ @helper ||= begin
70
+ h = RedisHelper.new
71
+ h.require_auth!
72
+ h
73
+ end
69
74
  end
70
75
  end
71
76
 
@@ -15,7 +15,8 @@ module Brasa
15
15
  puts " Status: #{colorize_status(app["status"])}"
16
16
  puts " Região: #{app["region"]}"
17
17
  puts " Preset: #{app["preset"]}"
18
- puts " URL: https://#{app["slug"]}.usebrasa.com.br"
18
+ puts " DB: #{app["database_engine"]}"
19
+ puts " URL: https://#{app["subdomain"]}.usebrasa.com.br"
19
20
 
20
21
  if app["last_deploy"]
21
22
  deploy = app["last_deploy"]
@@ -11,10 +11,10 @@ module Brasa
11
11
  require_auth!
12
12
  config = project_config
13
13
 
14
- app = find_or_create_app(config)
15
- slug = app["slug"]
14
+ @app = find_or_create_app(config)
15
+ slug = @app["slug"]
16
16
 
17
- if app["status"] == "active"
17
+ if @app["status"] == "active"
18
18
  info("Iniciando deploy...")
19
19
  else
20
20
  info("Provisionando infraestrutura...")
@@ -28,7 +28,8 @@ module Brasa
28
28
  deploy = trigger_deploy(slug)
29
29
 
30
30
  if wait_for_deploy(slug, deploy["id"])
31
- success("Deploy concluído! App disponível em https://#{slug}.usebrasa.com.br")
31
+ subdomain = @app["subdomain"] || slug
32
+ success("Deploy concluído! App disponível em https://#{subdomain}.usebrasa.com.br")
32
33
  end
33
34
  rescue Api::Client::ValidationError => e
34
35
  error("Erro ao criar app: #{e.message}")
@@ -58,14 +59,16 @@ module Brasa
58
59
  end
59
60
 
60
61
  def create_app(config)
61
- api.post("/api/v1/apps", body: {
62
+ app_params = {
62
63
  name: config[:app],
63
64
  stack: config[:stack],
64
65
  preset: config[:preset],
65
66
  region: config[:region],
66
67
  repo_url: detect_git_remote,
67
68
  repo_branch: config[:branch]
68
- })
69
+ }
70
+ app_params[:database_engine] = config[:database_engine] if config[:database_engine]
71
+ api.post("/api/v1/apps", body: { app: app_params })
69
72
  end
70
73
 
71
74
  def trigger_deploy(slug)
data/lib/brasa/config.rb CHANGED
@@ -20,8 +20,9 @@ module Brasa
20
20
 
21
21
  def save_token(token)
22
22
  FileUtils.mkdir_p(CONFIG_DIR)
23
- File.write(CREDENTIALS_FILE, JSON.generate({ token: token }))
24
- File.chmod(0600, CREDENTIALS_FILE)
23
+ File.open(CREDENTIALS_FILE, "w", 0600) do |f|
24
+ f.write(JSON.generate({ token: token }))
25
+ end
25
26
  end
26
27
 
27
28
  def clear_token
@@ -1,11 +1,15 @@
1
1
  require "tmpdir"
2
2
  require "open3"
3
+ require "securerandom"
3
4
 
4
5
  module Brasa
5
6
  class SourcePacker
6
7
  IGNORE_PATTERNS = %w[
7
8
  .git node_modules tmp log .bundle vendor/bundle
8
9
  .brasa.yml .env .env.* *.log coverage
10
+ config/master.key config/credentials.yml.enc
11
+ config/credentials config/credentials/*.yml.enc
12
+ *.pem *.key storage/*.key
9
13
  ].freeze
10
14
 
11
15
  def self.pack(directory = Dir.pwd)
@@ -17,7 +21,7 @@ module Brasa
17
21
  end
18
22
 
19
23
  def pack
20
- archive_path = File.join(Dir.tmpdir, "brasa-source-#{Time.now.to_i}.tar.gz")
24
+ archive_path = File.join(Dir.tmpdir, "brasa-source-#{Time.now.to_i}-#{SecureRandom.hex(4)}.tar.gz")
21
25
 
22
26
  excludes = build_excludes
23
27
  cmd = [ "tar", "czf", archive_path ] + excludes + [ "-C", @directory, "." ]
data/lib/brasa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Brasa
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brasa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brasa