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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +24 -15
- data/deploio-cli.gemspec +4 -1
- data/lib/deploio/bash_completion_generator.rb +97 -0
- data/lib/deploio/cli.rb +14 -3
- data/lib/deploio/commands/mysql.rb +139 -0
- data/lib/deploio/commands/mysql_backups.rb +77 -0
- data/lib/deploio/commands/postgresql_backups.rb +47 -45
- data/lib/deploio/completion_generator.rb +2 -1
- data/lib/deploio/helpers/backups_helper.rb +25 -0
- data/lib/deploio/mysql_backup_service.rb +79 -0
- data/lib/deploio/mysql_database_backup_service.rb +129 -0
- data/lib/deploio/mysql_database_ref.rb +40 -0
- data/lib/deploio/mysql_database_resolver.rb +71 -0
- data/lib/deploio/nctl_client.rb +44 -0
- data/lib/deploio/postgres_backup_service.rb +68 -0
- data/lib/deploio/postgres_database_backup_service.rb +116 -0
- data/lib/deploio/rclone_client.rb +99 -0
- data/lib/deploio/shared_options.rb +2 -2
- data/lib/deploio/templates/completion.bash.erb +140 -0
- data/lib/deploio/version.rb +1 -1
- data/lib/deploio.rb +10 -0
- metadata +18 -3
- data/setup +0 -115
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6111410969c71b44f6dc2296de35ca89d8f5435909b52c7c300a94bd1d71b558
|
|
4
|
+
data.tar.gz: a255219edc6349570f4e159f77461284d1feb08df1070a418ffaa827bacfc9fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
18
|
-
bundle exec bin/deploio --help
|
|
18
|
+
bin/setup
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
32
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
backup_service_for(name).capture
|
|
15
|
+
rescue Deploio::Error => e
|
|
16
|
+
Output.error(e.message)
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
Output.
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|