renuo-cli 4.20.0 → 4.21.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: 96e87891f76ac2c03e7d49276a00ec9f28b6d51530636c9fe18861b517202d56
4
- data.tar.gz: 58a5b66274436e087f13ba451b252d2fc16a3e96c873e1abc43e6901e47dbc6c
3
+ metadata.gz: de68476aa8e37811fd54ca8248bc8a30ddeb8594ada90207201a968e1e106b59
4
+ data.tar.gz: d7d2a77ec3fb9e520053a82be66f4ac2dd782179cf002c75ba7fd7b277d22e32
5
5
  SHA512:
6
- metadata.gz: 3f8ead33a76b828827e9ffeb699d9b4005d84bb3eb92bc984fae844c9d11acf3008d60cb980d75ee52707f98c68ad675a13d75d5d34c3bdc95b3ebab3a6fbef9
7
- data.tar.gz: 682b8f5e3c1ce078db1bca379297a29c174f382d1a7ce882ee0a52db2056f6b54a6b3e330c6e790d0cb926130535b1e8982768886b1847fca5e7bd51b7f0a9ac
6
+ metadata.gz: 9a1ad1c501f0df98ee51a8ed38e08358ae6b03e020a2ee3e9d598ce98026f0db612c4323385dbe188f142a1b5d5b163a894c99f89b6429dd54abf9db4d428e88
7
+ data.tar.gz: a4618b96d73466267c395546f8a4d918fbb8de282046c471f86023bbabf25db349f9357433d1acff722120c0548aaa8b4c5fa0db6cdd3f782d210e83c4316493
@@ -160,7 +160,7 @@ class Renuo::Cli::Commands::CreateDeploioApp # rubocop:disable Metrics/ClassLeng
160
160
  --project=#{@project_name_with_org_prefix} \\
161
161
  --postgres-database-version=#{@postgres_version} \\
162
162
  --location=nine-es34 \\
163
- --backup-schedule=daily
163
+ --backup-schedule=daily \\
164
164
  --collation=C.UTF-8
165
165
  OUTPUT
166
166
  end
@@ -3,22 +3,26 @@
3
3
  require_relative "../services/cache"
4
4
  require_relative "../services/deploio"
5
5
  require_relative "../services/heroku"
6
+ require_relative "../services/hetzner"
7
+ require "shellwords"
6
8
 
7
- class Renuo::Cli::Commands::Debug
9
+ class Renuo::Cli::Commands::Debug # rubocop:disable Metrics/ClassLength
8
10
  Cache = Renuo::Cli::Services::Cache
9
11
  Deploio = Renuo::Cli::Services::Deploio
10
12
  Heroku = Renuo::Cli::Services::Heroku
13
+ Hetzner = Renuo::Cli::Services::Hetzner
11
14
 
12
15
  command "debug" do |c|
13
16
  c.syntax = "renuo debug <app-name or domain>"
14
17
  c.summary = "Shortcut to debug apps"
15
18
  c.description = <<~DESC
16
19
  Enter the container or show logs of the available apps on Heroku or Deploio.
17
- The command will show a menu with all found targets.
20
+ The command will show a menu with all found targets across all organizations.
18
21
  It will only use a cache. You must manually update the cache with option 1).
19
22
  DESC
20
23
  c.option "-H", "--heroku", "Show only Heroku apps"
21
24
  c.option "-d", "--deploio", "Show only Deploio apps"
25
+ c.option "-z", "--hetzner", "Show only Hetzner vms"
22
26
  c.action do |args, options|
23
27
  new.run(args, options)
24
28
  rescue Interrupt
@@ -33,46 +37,56 @@ class Renuo::Cli::Commands::Debug
33
37
  query = args[0]
34
38
  abort(">> Please provide an app name or domain.") if query.blank?
35
39
 
36
- cloud_unspecified = !options.heroku && !options.deploio
40
+ cloud_unspecified = !options.heroku && !options.deploio && !options.hetzner
37
41
  should_scan_heroku = options.heroku || cloud_unspecified
38
42
  should_scan_deploio = options.deploio || cloud_unspecified
43
+ should_scan_hetzner = options.hetzner || cloud_unspecified
39
44
 
40
45
  choose do |menu| # rubocop:todo Metrics/BlockLength
41
46
  deploio_cache_info = "Deploio: #{Cache.stored_at("deploio_apps") || "never"}"
42
47
  heroku_cache_info = "Heroku: #{Cache.stored_at("heroku_apps") || "never"}"
48
+ hetzner_cache_info = "Hetzner: #{Cache.stored_at("hetzner_vms") || "never"}"
43
49
 
44
- menu.choice "Update caches (#{deploio_cache_info}, #{heroku_cache_info})" do
50
+ menu.choice "Update caches (#{deploio_cache_info}, #{heroku_cache_info}, #{hetzner_cache_info})" do
45
51
  if should_scan_deploio
46
52
  say "Updating Deploio cache"
47
53
  Cache.store("deploio_apps", Deploio.fetch_apps)
54
+ Cache.store("deploio_vms", Deploio.fetch_vms)
48
55
  end
49
56
  if should_scan_heroku
50
57
  say "Updating Heroku cache..."
51
58
  Cache.store("heroku_apps", Heroku.fetch_apps)
52
59
  end
60
+ if should_scan_hetzner
61
+ say "Updating Hetzner cache..."
62
+ Cache.store("hetzner_vms", Hetzner.fetch_vms)
63
+ end
53
64
  say "Caches updated"
54
65
  end
55
66
 
56
67
  open_cmds = []
57
68
 
58
- # rubocop:disable Layout/LineLength
59
69
  if should_scan_deploio && Cache.stored_at("deploio_apps")
60
70
  select_deploio_targets(query).each do |app|
61
- bash_cmd = "nctl #{bold("exec")} app #{bold(app[:name])} bash --project #{bold(app[:namespace])}"
62
- menu.choice(bash_cmd) { exec bash_cmd.squish }
71
+ display_cmd, exec_cmd = nctl_command(app, "exec", "bash")
72
+ menu.choice(display_cmd) { exec exec_cmd.squish }
63
73
 
64
- rails_console_cmd = "nctl #{bold("exec")} app #{bold(app[:name])} bundle #{bold("exec")} rails console --project #{bold(app[:namespace])}"
65
- menu.choice(rails_console_cmd) { exec rails_console_cmd.squish }
74
+ display_cmd, exec_cmd = nctl_command(app, "exec", "bundle exec rails console")
75
+ menu.choice(display_cmd) { exec exec_cmd.squish }
66
76
 
67
- log_cmd = "nctl #{bold("logs")} app #{bold(app[:name])} -f --project #{bold(app[:namespace])}"
68
- menu.choice(log_cmd) { exec log_cmd.squish }
77
+ display_cmd, exec_cmd = nctl_command(app, "logs", "-f")
78
+ menu.choice(display_cmd) { exec exec_cmd.squish }
69
79
 
70
- app[:hosts].each do |host|
71
- open_cmds << "open https://#{host}"
72
- end
80
+ app[:hosts].each { |host| open_cmds << "open https://#{host}" }
81
+ end
82
+ end
83
+
84
+ if should_scan_deploio && Cache.stored_at("deploio_vms")
85
+ select_deploio_vm_targets(query).each do |vm|
86
+ bash_cmd = "ssh root@#{vm[:fqdn]} docker ps"
87
+ menu.choice(bash_cmd) { exec bash_cmd.squish }
73
88
  end
74
89
  end
75
- # rubocop:enable Layout/LineLength
76
90
 
77
91
  if should_scan_heroku && Cache.stored_at("heroku_apps")
78
92
  select_heroku_targets(query).each do |app|
@@ -91,6 +105,14 @@ class Renuo::Cli::Commands::Debug
91
105
  end
92
106
  end
93
107
 
108
+ if should_scan_hetzner && Cache.stored_at("hetzner_vms")
109
+ select_hetzner_vm_targets(query).each do |vm|
110
+ bash_cmd = "ssh root@#{vm[:fqdn] || vm[:public_ip]} docker ps"
111
+ menu.choice(bash_cmd) { exec bash_cmd.squish }
112
+ open_cmds << "open https://console.hetzner.com/projects/#{vm[:project_id]}/servers/#{vm[:id]}"
113
+ end
114
+ end
115
+
94
116
  open_cmds.uniq.each do |cmd|
95
117
  menu.choice(cmd) { exec cmd.squish }
96
118
  end
@@ -104,7 +126,38 @@ class Renuo::Cli::Commands::Debug
104
126
 
105
127
  def select_deploio_targets(query)
106
128
  Cache.restore("deploio_apps").select do |app|
107
- app[:name].include?(query) || app[:namespace].include?(query) || app[:hosts].any? { |host| host.include?(query) }
129
+ app[:name].include?(query) ||
130
+ app[:namespace].include?(query) ||
131
+ app[:hosts].any? { |host| host.include?(query) }
132
+ end
133
+ end
134
+
135
+ def nctl_command(app, subcommand, trailing_args)
136
+ plain = "nctl #{subcommand} app #{app[:name]} #{trailing_args} --project #{app[:namespace]}"
137
+ display = "nctl #{bold(subcommand)} app #{bold(app[:name])} #{trailing_args} --project #{bold(app[:namespace])}"
138
+ with_deploio_org(app, display, plain)
139
+ end
140
+
141
+ def with_deploio_org(app, display_command, exec_command)
142
+ organization = app[:organization].to_s
143
+ return [display_command, exec_command] if organization.empty?
144
+
145
+ display_cmd = "nctl auth set-org #{bold(Shellwords.escape(organization))} && #{display_command}"
146
+ exec_cmd = "nctl auth set-org #{Shellwords.escape(organization)} && #{exec_command}"
147
+ [display_cmd, exec_cmd]
148
+ end
149
+
150
+ def select_deploio_vm_targets(query)
151
+ Cache.restore("deploio_vms").select do |vm|
152
+ vm[:name].include?(query) || vm[:namespace].include?(query) ||
153
+ vm[:hostname].include?(query) || vm[:ip].include?(query) ||
154
+ vm[:reverse_dns].include?(query) || vm[:fqdn].include?(query)
155
+ end
156
+ end
157
+
158
+ def select_hetzner_vm_targets(query)
159
+ Cache.restore("hetzner_vms").select do |vm|
160
+ vm[:name].include?(query) || vm[:public_ip].include?(query) || vm[:fqdn]&.include?(query)
108
161
  end
109
162
  end
110
163
 
@@ -1,19 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "shellwords"
4
+
3
5
  class Renuo::Cli::Services::Deploio
4
6
  class << self
5
7
  def fetch_apps
6
- fetch_apps_cmd = %(nctl get apps -A -o yaml)
7
- stdout, stderr, status = Open3.capture3 fetch_apps_cmd
8
- raise "Error fetching Deploio app list: #{stderr}" unless status.success?
8
+ active_org, available_orgs = fetch_whoami_orgs
9
9
 
10
- YAML.load_stream(stdout).map do |app|
10
+ available_orgs.flat_map do |organization|
11
+ set_organization(organization)
12
+ parse_apps(fetch_apps_yaml, organization)
13
+ end
14
+ ensure
15
+ set_organization(active_org)
16
+ end
17
+
18
+ def fetch_whoami_orgs
19
+ stdout, = run_nctl_command('nctl auth whoami -o "yaml"')
20
+ whoami = YAML.safe_load(stdout)
21
+
22
+ [whoami["organization"], whoami["orgs"]]
23
+ end
24
+
25
+ def parse_apps(yaml, organization)
26
+ YAML.load_stream(yaml).filter_map do |app|
11
27
  name = app.dig("metadata", "name")
12
28
  namespace = app.dig("metadata", "namespace")
13
29
  spec_hosts = app.dig("spec", "forProvider", "hosts") || []
14
30
 
15
- { name: name, namespace: namespace, hosts: spec_hosts }
31
+ { name: name, namespace: namespace, hosts: spec_hosts, organization: organization }
32
+ end
33
+ end
34
+
35
+ def fetch_vms
36
+ fetch_vms_cmd = %(nctl get cloudvm -A -o yaml)
37
+ stdout, stderr, status = Open3.capture3 fetch_vms_cmd
38
+ raise "Error fetching Deploio app list: #{stderr}" unless status.success?
39
+
40
+ YAML.load_stream(stdout).map do |app|
41
+ {
42
+ name: app.dig("metadata", "name"),
43
+ namespace: app.dig("metadata", "namespace"),
44
+ hostname: app.dig("spec", "forProvider", "hostname"),
45
+ ip: app.dig("status", "atProvider", "ipAddress"),
46
+ reverse_dns: app.dig("status", "atProvider", "reverseDNS"),
47
+ fqdn: app.dig("status", "atProvider", "fqdn")
48
+ }
16
49
  end
17
50
  end
51
+
52
+ private
53
+
54
+ def set_organization(organization) # rubocop:disable Naming/AccessorMethodName
55
+ return if organization.strip.empty?
56
+
57
+ _, stderr, status = run_nctl_command("nctl auth set-org #{Shellwords.escape(organization)}")
58
+ return if status.success?
59
+
60
+ raise "Error switching Deploio organization to #{organization}: #{stderr}"
61
+ end
62
+
63
+ def run_nctl_command(cmd)
64
+ stdout, stderr, status = Open3.capture3(cmd)
65
+ raise "Error fetching Deploio app list: #{stderr}" unless status.success?
66
+
67
+ [stdout, stderr, status]
68
+ end
69
+
70
+ def fetch_apps_yaml
71
+ stdout, = run_nctl_command("nctl get apps -A -o yaml")
72
+ stdout
73
+ end
18
74
  end
19
75
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ module Renuo::Cli::Services::Hetzner
6
+ class << self
7
+ LIST_TOKENS_CMD = "op item list --tags renuo-cli-debug-hetzner-token --format=json"
8
+ EXTRACT_CREDENTIALS_CMD = "op item get --reveal --field credential,project_id"
9
+ def fetch_vms
10
+ tokens = read_tokens
11
+ tokens.flat_map do |token_and_project|
12
+ token, project_id = token_and_project.split(",")
13
+ client = Client.new(token)
14
+ client.list_servers.map { |s| s.merge(project_id: project_id.to_i) }
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def read_tokens
21
+ stdout, stderr, status = Open3.capture3("#{LIST_TOKENS_CMD} | #{EXTRACT_CREDENTIALS_CMD}")
22
+ raise "Failed to fetch Hetzner tokens: #{stderr}" unless status.success?
23
+
24
+ stdout.split("\n").map(&:strip).reject(&:empty?)
25
+ end
26
+ end
27
+
28
+ class Client
29
+ def initialize(token)
30
+ @token = token
31
+ end
32
+
33
+ BASE_URL = "https://api.hetzner.cloud/v1/"
34
+
35
+ def list_servers
36
+ response = get("servers")
37
+ raise "Error fetching Hetzner servers: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
38
+
39
+ data = JSON.parse(response.body)
40
+ data["servers"].map do |server|
41
+ {
42
+ id: server["id"],
43
+ name: server["name"],
44
+ status: server["status"],
45
+ public_ip: server.dig("public_net", "ipv4", "ip"),
46
+ private_ip: server.dig("private_net", 0, "ip"),
47
+ fqdn: server.dig("public_net", "ipv4", "dns_ptr")
48
+ }
49
+ end
50
+ end
51
+
52
+ def get(resource)
53
+ uri = URI("#{BASE_URL}#{resource}")
54
+ req = Net::HTTP::Get.new(uri)
55
+ req["Authorization"] = "Bearer #{@token}"
56
+
57
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
58
+ http.request(req)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -3,7 +3,7 @@
3
3
  # :nocov:
4
4
  module Renuo
5
5
  class Cli
6
- VERSION = "4.20.0"
6
+ VERSION = "4.21.0"
7
7
  NAME = "renuo-cli"
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: renuo-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.20.0
4
+ version: 4.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renuo AG
@@ -96,6 +96,7 @@ files:
96
96
  - lib/renuo/cli/services/cloudfront_config_service.rb
97
97
  - lib/renuo/cli/services/deploio.rb
98
98
  - lib/renuo/cli/services/heroku.rb
99
+ - lib/renuo/cli/services/hetzner.rb
99
100
  - lib/renuo/cli/services/renuo_cli_config.rb
100
101
  - lib/renuo/cli/templates/semaphore/bin/cache_restore.erb
101
102
  - lib/renuo/cli/templates/semaphore/bin/cache_store.erb