deploio-cli 0.2.0 → 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: eae3786b2b0c98a4d02b1ac9037e9536da6cfc90ab7120ce80d4e98a76df90d7
4
- data.tar.gz: ac86804058048327a006cd3343839299408425dd4e3702fe99aafc571e8e372c
3
+ metadata.gz: 6111410969c71b44f6dc2296de35ca89d8f5435909b52c7c300a94bd1d71b558
4
+ data.tar.gz: a255219edc6349570f4e159f77461284d1feb08df1070a418ffaa827bacfc9fe
5
5
  SHA512:
6
- metadata.gz: 9a6a68ed578e07a9d0026f1dd61f3971ddd5f5e6b2ec590e2960644bef88de65cf054c04c2255051e0b550061eaa965a521b5700bfaf139ae5e3d76bf1f1aa19
7
- data.tar.gz: 36614043d839a6a59c7ecc761f6d47ce8b9ff05cf149a630f33b17cb10b45ed8e374c2d1293c3ef7b9d98324290eeeb9332c2c530aea90642e18878faa08b9c4
6
+ metadata.gz: b6734d4ec34d7021bfeba37492c64b9d4e7a7684739052698715c98e4bd7a587bed1c1027cd7d15c5d18e1a01285145963055db395245ba120fcaedb64dbd2a4
7
+ data.tar.gz: ad7acd6d33369947979936e9ff940683a495ad988619f08f95e38ebe0b23596e88f5fc9c499c9fcfaf3c0c0e384150761266a3b20a76b4b375829013768e9e84
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0 - 2026-11-04
4
+
5
+ - Add `mysql` commands, including MySQL database backups ([#5](https://github.com/renuo/deploio-cli/pull/5))
6
+ - Add bash completions ([#7](https://github.com/renuo/deploio-cli/pull/7))
7
+ - Replace the fork installer with `bin/setup`, so the checkout runs from any directory
8
+ ([#8](https://github.com/renuo/deploio-cli/pull/8))
data/README.md CHANGED
@@ -6,6 +6,7 @@ A CLI for [Deploio](https://www.deplo.io/) that wraps [`nctl`](https://github.co
6
6
 
7
7
  - Ruby 3.3+
8
8
  - nctl version 1.10.0 or higher
9
+ - [rclone](https://rclone.org/) (`brew install rclone`) — only needed for `deploio pg backups` on economy-tier databases
9
10
 
10
11
  ## Installation
11
12
 
@@ -14,11 +15,13 @@ A CLI for [Deploio](https://www.deplo.io/) that wraps [`nctl`](https://github.co
14
15
  ```bash
15
16
  git clone https://github.com/renuo/deploio-cli.git
16
17
  cd deploio-cli
17
- bundle install
18
- bundle exec bin/deploio --help
18
+ bin/setup
19
19
  ```
20
20
 
21
- ### As a gem (coming soon)
21
+ `bin/setup` installs the dependencies and puts `deploio` and `depl` binstubs into `~/.local/bin` (override with `INSTALL_DIR`), so this checkout runs from any directory.
22
+ The binstubs are pinned to the Ruby that ran `bin/setup`; run it again after changing the Ruby version.
23
+
24
+ ### As a gem
22
25
 
23
26
  ```bash
24
27
  gem install deploio-cli
@@ -26,10 +29,14 @@ gem install deploio-cli
26
29
 
27
30
  ## Shell Completion
28
31
 
29
- Enable autocompletion by adding this to your `~/.zshrc` or `~/.bashrc`, or whatever you use:
32
+ Enable autocompletion by adding the line for your shell to its rc file:
30
33
 
31
34
  ```bash
35
+ # ~/.zshrc
32
36
  eval "$(deploio completion)"
37
+
38
+ # ~/.bashrc
39
+ eval "$(deploio completion --shell bash)"
33
40
  ```
34
41
 
35
42
  After setup, completions work for:
@@ -90,6 +97,13 @@ SERVICES
90
97
  deploio services -p PROJECT --connected-apps Show which apps use each service (requires -p)
91
98
  deploio services --chf Show estimated monthly price (CHF) for each service
92
99
 
100
+ POSTGRESQL
101
+ deploio pg List all PostgreSQL databases
102
+ deploio pg:info NAME Show database details
103
+ deploio pg backups list NAME List available backups
104
+ deploio pg backups download NAME Download the latest backup
105
+ deploio pg backups capture NAME Capture a new backup
106
+
93
107
  LOGS
94
108
  deploio logs -a APP Show recent logs
95
109
  deploio logs -a APP --tail Stream logs continuously
@@ -100,7 +114,8 @@ EXECUTION
100
114
  deploio run -a APP -- CMD Alias for exec
101
115
 
102
116
  OTHER
103
- deploio completion Generate shell completion script
117
+ deploio completion Generate zsh completion script
118
+ deploio completion -s bash Generate bash completion script
104
119
  deploio version Show version
105
120
 
106
121
  FLAGS
@@ -214,20 +229,14 @@ gem build deploio-cli.gemspec
214
229
 
215
230
  ### Testing commands
216
231
 
217
- The best way to test the commands in the shell is to temporarly set:
232
+ After `bin/setup`, `deploio` runs this checkout, so changes take effect immediately.
233
+ After adding or changing commands or options, refresh the autocompletion options:
218
234
 
219
235
  ```shell
220
- export PATH="$PWD/bin:$PATH"
236
+ eval "$(deploio completion)" # zsh
237
+ eval "$(deploio completion --shell bash)" # bash
221
238
  ```
222
239
 
223
- to have the `deploio` command binded to the current one, and
224
-
225
- ```shell
226
- eval "$(deploio completion)"
227
- ```
228
-
229
- to refresh the autocompletion options.
230
-
231
240
  ## License
232
241
 
233
242
  MIT
data/deploio-cli.gemspec CHANGED
@@ -14,7 +14,6 @@ Gem::Specification.new do |spec|
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = ">= 3.0.0"
16
16
 
17
- spec.metadata["homepage_uri"] = spec.homepage
18
17
  spec.metadata["source_code_uri"] = spec.homepage
19
18
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
19
 
@@ -23,6 +22,10 @@ Gem::Specification.new do |spec|
23
22
 
24
23
  eval "$(deploio completion)"
25
24
 
25
+ or this to your ~/.bashrc:
26
+
27
+ eval "$(deploio completion --shell bash)"
28
+
26
29
  MSG
27
30
 
28
31
  spec.files = Dir.chdir(__dir__) do
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "completion_generator"
4
+
5
+ module Deploio
6
+ # Generates bash completion script from Thor command metadata.
7
+ # Completers are shell commands printing the candidates.
8
+ #
9
+ # @example With custom option completers
10
+ # BashCompletionGenerator.new(MyCLI, option_completers: {
11
+ # 'env' => 'echo production staging development'
12
+ # }).generate
13
+ #
14
+ class BashCompletionGenerator < CompletionGenerator
15
+ TEMPLATE_PATH = File.expand_path("templates/completion.bash.erb", __dir__)
16
+
17
+ ROOT_OPTIONS = {version: Thor::Option.new(:version, type: :boolean, aliases: "-v")}.freeze
18
+
19
+ # A command path ("" is the root, "pg:backups" a nested subcommand)
20
+ # with the candidates that can follow it.
21
+ Context = Struct.new(:path, :commands, :options, :positional, :passthrough, keyword_init: true)
22
+
23
+ private
24
+
25
+ def default_option_completers
26
+ {
27
+ "app" => "_#{program_name}_apps_list",
28
+ "size" => "echo micro mini standard",
29
+ "project" => "_#{program_name}_projects_list"
30
+ }
31
+ end
32
+
33
+ def default_positional_completers
34
+ {
35
+ "orgs:set" => "_#{program_name}_orgs_list",
36
+ "pg:info" => "_#{program_name}_pg_databases_list",
37
+ "pg:backups:capture" => "_#{program_name}_pg_databases_list",
38
+ "pg:backups:list" => "_#{program_name}_pg_databases_list",
39
+ "pg:backups:download" => "_#{program_name}_pg_databases_list"
40
+ }
41
+ end
42
+
43
+ def contexts
44
+ @contexts ||= [
45
+ Context.new(path: "", commands: main_commands.map(&:first), options: ROOT_OPTIONS),
46
+ *direct_commands.map { |name, options| leaf_context(name, all_options(options)) },
47
+ *passthrough_commands.map { |name, options| leaf_context(name, all_options(options), passthrough: true) },
48
+ *subcommands.flat_map do |name, commands, class_options, default_task, klass|
49
+ group_contexts(name, commands, class_options, default_task, klass.subcommand_classes)
50
+ end,
51
+ *nested_subcommands.flat_map do |name, commands, class_options|
52
+ group_contexts(name, commands, class_options)
53
+ end
54
+ ]
55
+ end
56
+
57
+ def group_contexts(name, commands, class_options, default_task = nil, nested = {})
58
+ default_options = commands.find { |cmd_name, _, _| cmd_name == default_task }&.last || {}
59
+ group = Context.new(path: name, commands: commands.map(&:first),
60
+ options: all_options(default_options, class_options))
61
+
62
+ leaves = commands.reject { |cmd_name, _, _| nested.key?(cmd_name) }.map do |cmd_name, _, options|
63
+ leaf_context("#{name}:#{cmd_name}", all_options(options, class_options))
64
+ end
65
+
66
+ [group, *leaves]
67
+ end
68
+
69
+ def leaf_context(path, options, passthrough: false)
70
+ Context.new(path: path, commands: [], options: options,
71
+ positional: positional_completers[path], passthrough: passthrough)
72
+ end
73
+
74
+ def all_options(method_options, class_options = {})
75
+ cli_class_options.merge(class_options).merge(method_options)
76
+ end
77
+
78
+ def flags(context)
79
+ context.options.flat_map { |name, opt| option_flags(name, opt) }.uniq
80
+ end
81
+
82
+ def value_flags(context)
83
+ context.options.reject { |_, opt| opt.type == :boolean }.flat_map { |name, opt| option_flags(name, opt) }.uniq
84
+ end
85
+
86
+ def option_value_completers
87
+ contexts.flat_map { |context| context.options.to_a }.uniq(&:first).filter_map do |name, opt|
88
+ completer = option_completers[name.to_s]
89
+ [option_flags(name, opt), completer] if completer
90
+ end
91
+ end
92
+
93
+ def option_flags(name, opt)
94
+ ["--#{name.to_s.tr("_", "-")}", *opt.aliases]
95
+ end
96
+ end
97
+ end
data/lib/deploio/cli.rb CHANGED
@@ -7,9 +7,13 @@ require_relative "commands/builds"
7
7
  require_relative "commands/orgs"
8
8
  require_relative "commands/projects"
9
9
  require_relative "commands/services"
10
+ require_relative "helpers/backups_helper"
10
11
  require_relative "commands/postgresql_backups"
11
12
  require_relative "commands/postgresql"
13
+ require_relative "commands/mysql_backups"
14
+ require_relative "commands/mysql"
12
15
  require_relative "completion_generator"
16
+ require_relative "bash_completion_generator"
13
17
 
14
18
  module Deploio
15
19
  class CLI < Thor
@@ -28,11 +32,13 @@ module Deploio
28
32
  case options[:shell].downcase
29
33
  when "zsh"
30
34
  puts CompletionGenerator.new.generate
31
- when "bash", "fish"
32
- Output.error("#{options[:shell]} completion is not yet supported. Only zsh is available.")
35
+ when "bash"
36
+ puts BashCompletionGenerator.new.generate
37
+ when "fish"
38
+ Output.error("#{options[:shell]} completion is not yet supported. Only zsh and bash are available.")
33
39
  exit 1
34
40
  else
35
- Output.error("Unknown shell: #{options[:shell]}. Supported: zsh")
41
+ Output.error("Unknown shell: #{options[:shell]}. Supported: zsh, bash")
36
42
  exit 1
37
43
  end
38
44
  end
@@ -57,6 +63,9 @@ module Deploio
57
63
  desc "pg COMMAND", "PostgreSQL database management commands"
58
64
  subcommand "pg", Commands::PostgreSQL
59
65
 
66
+ desc "mysql COMMAND", "MySQL database management commands"
67
+ subcommand "mysql", Commands::MySQL
68
+
60
69
  # Shortcut for auth:login
61
70
  desc "login", "Authenticate with nctl (alias for auth:login)"
62
71
  def login
@@ -100,6 +109,8 @@ module Deploio
100
109
 
101
110
  private
102
111
 
112
+ # The aliases above hand off with Thor.start, which starts from a fresh
113
+ # option parse, so the shared class options have to be passed as arguments.
103
114
  def build_option_args
104
115
  args = []
105
116
  args << "--dry-run" if options[:dry_run]
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deploio
4
+ module Commands
5
+ class MySQL < Thor
6
+ include SharedOptions
7
+
8
+ namespace "mysql"
9
+
10
+ class_option :json, type: :boolean, default: false, desc: "Output as JSON"
11
+
12
+ default_task :list
13
+
14
+ desc "list", "List all MySQL databases"
15
+ def list
16
+ setup_options
17
+ raw_dbs = @nctl.get_all_mysql_databases
18
+
19
+ if options[:json]
20
+ puts JSON.pretty_generate(raw_dbs)
21
+ return
22
+ end
23
+
24
+ if raw_dbs.empty?
25
+ Output.warning("No MySQL databases found") unless merged_options[:dry_run]
26
+ return
27
+ end
28
+
29
+ resolver = MysqlDatabaseResolver.new(nctl_client: @nctl)
30
+
31
+ rows = raw_dbs.map do |mysql_database|
32
+ kind = mysql_database["kind"] || ""
33
+ metadata = mysql_database["metadata"] || {}
34
+ spec = mysql_database["spec"] || {}
35
+ for_provider = spec["forProvider"] || {}
36
+ version = for_provider["version"]
37
+ namespace = metadata["namespace"] || ""
38
+ name = metadata["name"] || ""
39
+
40
+ [
41
+ resolver.short_name_for(namespace, name),
42
+ project_from_namespace(namespace, resolver.current_org),
43
+ presence(kind, default: "-"),
44
+ presence(version, default: "?")
45
+ ]
46
+ end
47
+
48
+ Output.table(rows, headers: ["NAME", "PROJECT", "KIND", "VERSION"])
49
+ end
50
+
51
+ desc "info NAME", "Show MySQL database details"
52
+ def info(name)
53
+ setup_options
54
+ resolver = MysqlDatabaseResolver.new(nctl_client: @nctl)
55
+ db_ref = resolver.resolve(database_name: name)
56
+ data = @nctl.get_mysql_database(db_ref)
57
+
58
+ if options[:json]
59
+ puts JSON.pretty_generate(data)
60
+ return
61
+ end
62
+
63
+ kind = data["kind"]
64
+ metadata = data["metadata"] || {}
65
+ spec = data["spec"] || {}
66
+ for_provider = spec["forProvider"] || {}
67
+ status = data["status"] || {}
68
+ at_provider = status["atProvider"] || {}
69
+
70
+ Output.header("MySQL Database: #{db_ref.full_name}")
71
+ puts
72
+
73
+ Output.header("General")
74
+ Output.table(
75
+ [
76
+ ["Name", presence(metadata["name"])],
77
+ ["Project", presence(metadata["namespace"])],
78
+ ["Kind", presence(kind, default: "-")],
79
+ ["Version", presence(for_provider["version"], default: "?")],
80
+ ["FQDN", presence(at_provider["fqdn"])],
81
+ ["Size", presence(at_provider["size"], default: "-")]
82
+ ]
83
+ )
84
+
85
+ puts
86
+
87
+ Output.header("Status")
88
+ conditions = status["conditions"] || []
89
+ ready_condition = conditions.find { |c| c["type"] == "Ready" }
90
+ synced_condition = conditions.find { |c| c["type"] == "Synced" }
91
+
92
+ Output.table(
93
+ [
94
+ ["Ready", presence(ready_condition&.dig("status"))],
95
+ ["Synced", presence(synced_condition&.dig("status"))]
96
+ ]
97
+ )
98
+
99
+ if for_provider["allowedCIDRs"].is_a?(Array)
100
+ puts
101
+
102
+ Output.header("Access")
103
+ Output.table(
104
+ [
105
+ ["Allowed CIDRs", for_provider["allowedCIDRs"].join(", ")]
106
+ ]
107
+ )
108
+
109
+ puts
110
+
111
+ Output.header("SSH Keys")
112
+ for_provider["sshKeys"].each do |key|
113
+ puts "- #{key}"
114
+ end
115
+ end
116
+ rescue Deploio::Error => e
117
+ Output.error(e.message)
118
+ exit 1
119
+ end
120
+
121
+ desc "backups COMMAND", "Manage MySQL database backups"
122
+ subcommand "backups", Commands::MySQLBackups
123
+
124
+ private
125
+
126
+ def presence(value, default: "-")
127
+ (value.nil? || value.to_s.empty?) ? default : value
128
+ end
129
+
130
+ def project_from_namespace(namespace, current_org)
131
+ if current_org && namespace.start_with?("#{current_org}-")
132
+ namespace.delete_prefix("#{current_org}-")
133
+ else
134
+ namespace
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,77 @@
1
+ module Deploio
2
+ module Commands
3
+ class MySQLBackups < Thor
4
+ include SharedOptions
5
+ include BackupsHelper
6
+
7
+ namespace "mysql:backups"
8
+
9
+ DEDICATED_KIND = "MySQL"
10
+ ECONOMY_KIND = "MySQLDatabase"
11
+
12
+ desc "capture NAME", "Capture a new backup for the specified MySQL database"
13
+ def capture(name)
14
+ backup_service_for(name).capture
15
+ rescue Deploio::Error => e
16
+ Output.error(e.message)
17
+ exit 1
18
+ end
19
+
20
+ desc "list NAME", "List the available backups for the specified MySQL database"
21
+ def list(name)
22
+ backups = backup_service_for(name).backups
23
+ if backups.empty?
24
+ Output.warning("No backups found for '#{name}'")
25
+ return
26
+ end
27
+
28
+ rows = backups.map do |backup|
29
+ [format_time(backup["ModTime"]), format_size(backup["Size"]), backup["Name"]]
30
+ end
31
+ Output.table(rows, headers: ["DATE", "SIZE", "NAME"])
32
+ rescue Deploio::Error => e
33
+ Output.error(e.message)
34
+ exit 1
35
+ end
36
+
37
+ desc "download NAME [--output destination_path]", "Download the latest backup for the specified MySQL database instance"
38
+ method_option :output, type: :string, desc: "Output file path (defaults to current directory with auto-generated name)"
39
+ method_option :db_name, type: :string, desc: "If there are multiple DBs, specify which one to download the backup for", default: nil
40
+ def download(name)
41
+ service = backup_service_for(name)
42
+ destination = merged_options[:output] || service.default_destination
43
+ backup = service.download(destination: destination, db_name: merged_options[:db_name])
44
+
45
+ # Only the economy tier knows when its backup was taken.
46
+ if backup
47
+ Output.success("Downloaded backup from #{format_time(backup["ModTime"])} to #{destination}")
48
+ else
49
+ Output.success("Downloaded backup to #{destination}")
50
+ end
51
+ rescue Deploio::Error => e
52
+ Output.error(e.message)
53
+ exit 1
54
+ end
55
+
56
+ private
57
+
58
+ def backup_service_for(name)
59
+ setup_options
60
+ resolver = MysqlDatabaseResolver.new(nctl_client: @nctl)
61
+ db_ref = resolver.resolve(database_name: name)
62
+ data = @nctl.get_mysql_database(db_ref)
63
+ raise Deploio::Error, "Could not read database '#{db_ref.full_name}'." if data.nil?
64
+
65
+ case data["kind"]
66
+ when DEDICATED_KIND
67
+ MysqlBackupService.new(data: data, name: name, dry_run: @nctl.dry_run)
68
+ when ECONOMY_KIND
69
+ MysqlDatabaseBackupService.new(db_ref: db_ref, data: data, nctl_client: @nctl, name: name)
70
+ else
71
+ raise Deploio::UnsupportedBackupOperationError,
72
+ "Backups are not supported for databases of kind '#{data["kind"]}'."
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -2,73 +2,75 @@ module Deploio
2
2
  module Commands
3
3
  class PostgreSQLBackups < Thor
4
4
  include SharedOptions
5
+ include BackupsHelper
5
6
 
6
7
  namespace "pg:backups"
7
8
 
9
+ DEDICATED_KIND = "Postgres"
10
+ ECONOMY_KIND = "PostgresDatabase"
11
+
8
12
  desc "capture NAME", "Capture a new backup for the specified PostgreSQL database"
9
13
  def capture(name)
10
- setup_options
11
- resolver = PgDatabaseResolver.new(nctl_client: @nctl)
12
- db_ref = resolver.resolve(database_name: name)
13
- data = @nctl.get_pg_database(db_ref)
14
- kind = data["kind"] || ""
14
+ backup_service_for(name).capture
15
+ rescue Deploio::Error => e
16
+ Output.error(e.message)
17
+ exit 1
18
+ end
15
19
 
16
- unless kind == "Postgres" || @nctl.dry_run
17
- Output.error("Backups can only be captured for PostgreSQL databases. (shared dbs are not supported)")
18
- exit 1
20
+ desc "list NAME", "List the available backups for the specified PostgreSQL database"
21
+ def list(name)
22
+ backups = backup_service_for(name).backups
23
+ if backups.empty?
24
+ Output.warning("No backups found for '#{name}'")
25
+ return
19
26
  end
20
27
 
21
- fqdn = data.dig("status", "atProvider", "fqdn")
22
- if fqdn.nil? || fqdn.empty?
23
- Output.error("Database FQDN not found; cannot capture backup.")
24
- exit 1
28
+ rows = backups.map do |backup|
29
+ [format_time(backup["ModTime"]), format_size(backup["Size"]), backup["Name"]]
25
30
  end
26
-
27
- cmd = ["ssh", "dbadmin@#{fqdn}", "sudo nine-postgresql-backup"]
28
- Output.command(cmd.join(" "))
29
- system(*cmd) unless @nctl.dry_run
31
+ Output.table(rows, headers: ["DATE", "SIZE", "NAME"])
32
+ rescue Deploio::Error => e
33
+ Output.error(e.message)
34
+ exit 1
30
35
  end
31
36
 
32
37
  desc "download NAME [--output destination_path]", "Download the latest backup for the specified PostgreSQL database instance"
33
38
  method_option :output, type: :string, desc: "Output file path (defaults to current directory with auto-generated name)"
34
39
  method_option :db_name, type: :string, desc: "If there are multiple DBs, specify which one to download the backup for", default: nil
35
40
  def download(name)
36
- destination = options[:output] || "./#{name}-latest-backup.zst"
41
+ service = backup_service_for(name)
42
+ destination = merged_options[:output] || service.default_destination
43
+ backup = service.download(destination: destination, db_name: merged_options[:db_name])
37
44
 
45
+ # Only the economy tier knows when its backup was taken.
46
+ if backup
47
+ Output.success("Downloaded backup from #{format_time(backup["ModTime"])} to #{destination}")
48
+ else
49
+ Output.success("Downloaded backup to #{destination}")
50
+ end
51
+ rescue Deploio::Error => e
52
+ Output.error(e.message)
53
+ exit 1
54
+ end
55
+
56
+ private
57
+
58
+ def backup_service_for(name)
38
59
  setup_options
39
60
  resolver = PgDatabaseResolver.new(nctl_client: @nctl)
40
61
  db_ref = resolver.resolve(database_name: name)
41
62
  data = @nctl.get_pg_database(db_ref)
42
- kind = data["kind"] || ""
43
-
44
- unless kind == "Postgres" || @nctl.dry_run
45
- Output.error("Backups can only be downloaded for PostgreSQL databases. (shared dbs are not supported)")
46
- exit 1
47
- end
63
+ raise Deploio::Error, "Could not read database '#{db_ref.full_name}'." if data.nil?
48
64
 
49
- databases = data.dig("status", "atProvider", "databases")&.keys || []
50
- databases.reject! { |db| db.strip.empty? }
51
- if databases.empty?
52
- Output.error("No databases found in PostgreSQL instance; cannot download backup.")
53
- exit 1
54
- elsif databases.size > 1 && options[:db_name].nil?
55
- Output.error("Multiple databases found in PostgreSQL instance")
56
- Output.error("Databases: #{databases.join(", ")}")
57
- Output.error("Please specify the database name using the --db_name option.")
58
- exit 1
65
+ case data["kind"]
66
+ when DEDICATED_KIND
67
+ PostgresBackupService.new(data: data, name: name, dry_run: @nctl.dry_run)
68
+ when ECONOMY_KIND
69
+ PostgresDatabaseBackupService.new(db_ref: db_ref, data: data, nctl_client: @nctl, name: name)
70
+ else
71
+ raise Deploio::UnsupportedBackupOperationError,
72
+ "Backups are not supported for databases of kind '#{data["kind"]}'."
59
73
  end
60
-
61
- db_name = options[:db_name] || databases.first
62
-
63
- fqdn = data.dig("status", "atProvider", "fqdn")
64
- if fqdn.nil? || fqdn.empty?
65
- Output.error("Database FQDN not found; cannot download backup.")
66
- exit 1
67
- end
68
-
69
- cmd = ["rsync", "-avz", "dbadmin@#{fqdn}:~/backup/postgresql/latest/customer/#{db_name}/#{db_name}.zst", destination]
70
- Output.command(cmd.join(" "))
71
- system(*cmd) unless @nctl.dry_run
72
74
  end
73
75
  end
74
76
  end
@@ -37,7 +37,7 @@ module Deploio
37
37
  end
38
38
 
39
39
  def generate
40
- template = File.read(TEMPLATE_PATH)
40
+ template = File.read(self.class::TEMPLATE_PATH)
41
41
  ERB.new(template, trim_mode: "-").result(binding)
42
42
  end
43
43
 
@@ -61,6 +61,7 @@ module Deploio
61
61
  "orgs:set" => "'1:organization:_#{program_name}_orgs_list'",
62
62
  "pg:info" => "'1:database:_#{program_name}_pg_databases_list'",
63
63
  "pg:backups:capture" => "'1:database:_#{program_name}_pg_databases_list'",
64
+ "pg:backups:list" => "'1:database:_#{program_name}_pg_databases_list'",
64
65
  "pg:backups:download" => "'1:database:_#{program_name}_pg_databases_list'"
65
66
  }
66
67
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Deploio
6
+ module BackupsHelper
7
+ def format_size(bytes)
8
+ bytes = bytes.to_i
9
+ units = ["B", "KiB", "MiB", "GiB", "TiB"]
10
+ index = 0
11
+ size = bytes.to_f
12
+ while size >= 1024 && index < units.size - 1
13
+ size /= 1024
14
+ index += 1
15
+ end
16
+ (index.zero? ? "#{bytes} B" : format("%.1f %s", size, units[index]))
17
+ end
18
+
19
+ def format_time(value)
20
+ Time.parse(value.to_s).localtime.strftime("%Y-%m-%d %H:%M")
21
+ rescue ArgumentError, TypeError
22
+ value.to_s
23
+ end
24
+ end
25
+ end