deploio-cli 0.2.0 → 0.3.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/README.md +9 -1
- data/lib/deploio/cli.rb +2 -0
- data/lib/deploio/commands/postgresql_backups.rb +63 -42
- data/lib/deploio/completion_generator.rb +1 -0
- data/lib/deploio/nctl_client.rb +8 -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/version.rb +1 -1
- data/lib/deploio.rb +5 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ef7a08d44a816dd88331bf7e007a536ef399b10afbf05229bef8e1e3137629e
|
|
4
|
+
data.tar.gz: d042f89dc44e64df49d599f1291f88151af45461132156af5c36486fa6408baa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 603238b92b6b23a32bf92f81a0a89a4cc0edbf8daad40519f1f165c095629ccb5f83775f01f0aae67541f6177f845ebc9797584fdcdb977425fa2307c2d8f98e
|
|
7
|
+
data.tar.gz: 2ac16607435cc562bcf3d3e94066002315bb06f198122fe6d088ee25782f9153106ab065e217f34a86d5aa99a4cb2d7d154876eabcc958266f2e74cc6771ef09
|
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
|
|
|
@@ -18,7 +19,7 @@ bundle install
|
|
|
18
19
|
bundle exec bin/deploio --help
|
|
19
20
|
```
|
|
20
21
|
|
|
21
|
-
### As a gem
|
|
22
|
+
### As a gem
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
25
|
gem install deploio-cli
|
|
@@ -90,6 +91,13 @@ SERVICES
|
|
|
90
91
|
deploio services -p PROJECT --connected-apps Show which apps use each service (requires -p)
|
|
91
92
|
deploio services --chf Show estimated monthly price (CHF) for each service
|
|
92
93
|
|
|
94
|
+
POSTGRESQL
|
|
95
|
+
deploio pg List all PostgreSQL databases
|
|
96
|
+
deploio pg:info NAME Show database details
|
|
97
|
+
deploio pg backups list NAME List available backups
|
|
98
|
+
deploio pg backups download NAME Download the latest backup
|
|
99
|
+
deploio pg backups capture NAME Capture a new backup
|
|
100
|
+
|
|
93
101
|
LOGS
|
|
94
102
|
deploio logs -a APP Show recent logs
|
|
95
103
|
deploio logs -a APP --tail Stream logs continuously
|
data/lib/deploio/cli.rb
CHANGED
|
@@ -100,6 +100,8 @@ module Deploio
|
|
|
100
100
|
|
|
101
101
|
private
|
|
102
102
|
|
|
103
|
+
# The aliases above hand off with Thor.start, which starts from a fresh
|
|
104
|
+
# option parse, so the shared class options have to be passed as arguments.
|
|
103
105
|
def build_option_args
|
|
104
106
|
args = []
|
|
105
107
|
args << "--dry-run" if options[:dry_run]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
1
3
|
module Deploio
|
|
2
4
|
module Commands
|
|
3
5
|
class PostgreSQLBackups < Thor
|
|
@@ -5,70 +7,89 @@ module Deploio
|
|
|
5
7
|
|
|
6
8
|
namespace "pg:backups"
|
|
7
9
|
|
|
10
|
+
DEDICATED_KIND = "Postgres"
|
|
11
|
+
ECONOMY_KIND = "PostgresDatabase"
|
|
12
|
+
|
|
8
13
|
desc "capture NAME", "Capture a new backup for the specified PostgreSQL database"
|
|
9
14
|
def capture(name)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
backup_service_for(name).capture
|
|
16
|
+
rescue Deploio::Error => e
|
|
17
|
+
Output.error(e.message)
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
desc "list NAME", "List the available backups for the specified PostgreSQL database"
|
|
22
|
+
def list(name)
|
|
23
|
+
backups = backup_service_for(name).backups
|
|
24
|
+
if backups.empty?
|
|
25
|
+
Output.warning("No backups found for '#{name}'")
|
|
26
|
+
return
|
|
19
27
|
end
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Output.error("Database FQDN not found; cannot capture backup.")
|
|
24
|
-
exit 1
|
|
29
|
+
rows = backups.map do |backup|
|
|
30
|
+
[format_time(backup["ModTime"]), format_size(backup["Size"]), backup["Name"]]
|
|
25
31
|
end
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Output.
|
|
29
|
-
|
|
32
|
+
Output.table(rows, headers: ["DATE", "SIZE", "NAME"])
|
|
33
|
+
rescue Deploio::Error => e
|
|
34
|
+
Output.error(e.message)
|
|
35
|
+
exit 1
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
desc "download NAME [--output destination_path]", "Download the latest backup for the specified PostgreSQL database instance"
|
|
33
39
|
method_option :output, type: :string, desc: "Output file path (defaults to current directory with auto-generated name)"
|
|
34
40
|
method_option :db_name, type: :string, desc: "If there are multiple DBs, specify which one to download the backup for", default: nil
|
|
35
41
|
def download(name)
|
|
36
|
-
|
|
42
|
+
service = backup_service_for(name)
|
|
43
|
+
destination = merged_options[:output] || service.default_destination
|
|
44
|
+
backup = service.download(destination: destination, db_name: merged_options[:db_name])
|
|
37
45
|
|
|
46
|
+
# Only the economy tier knows when its backup was taken.
|
|
47
|
+
if backup
|
|
48
|
+
Output.success("Downloaded backup from #{format_time(backup["ModTime"])} to #{destination}")
|
|
49
|
+
else
|
|
50
|
+
Output.success("Downloaded backup to #{destination}")
|
|
51
|
+
end
|
|
52
|
+
rescue Deploio::Error => e
|
|
53
|
+
Output.error(e.message)
|
|
54
|
+
exit 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def backup_service_for(name)
|
|
38
60
|
setup_options
|
|
39
61
|
resolver = PgDatabaseResolver.new(nctl_client: @nctl)
|
|
40
62
|
db_ref = resolver.resolve(database_name: name)
|
|
41
63
|
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
|
|
64
|
+
raise Deploio::Error, "Could not read database '#{db_ref.full_name}'." if data.nil?
|
|
48
65
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Output.error("Please specify the database name using the --db_name option.")
|
|
58
|
-
exit 1
|
|
66
|
+
case data["kind"]
|
|
67
|
+
when DEDICATED_KIND
|
|
68
|
+
PostgresBackupService.new(data: data, name: name, dry_run: @nctl.dry_run)
|
|
69
|
+
when ECONOMY_KIND
|
|
70
|
+
PostgresDatabaseBackupService.new(db_ref: db_ref, data: data, nctl_client: @nctl, name: name)
|
|
71
|
+
else
|
|
72
|
+
raise Deploio::UnsupportedBackupOperationError,
|
|
73
|
+
"Backups are not supported for databases of kind '#{data["kind"]}'."
|
|
59
74
|
end
|
|
75
|
+
end
|
|
60
76
|
|
|
61
|
-
|
|
77
|
+
def format_time(value)
|
|
78
|
+
Time.parse(value.to_s).localtime.strftime("%Y-%m-%d %H:%M")
|
|
79
|
+
rescue ArgumentError, TypeError
|
|
80
|
+
value.to_s
|
|
81
|
+
end
|
|
62
82
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
83
|
+
def format_size(bytes)
|
|
84
|
+
bytes = bytes.to_i
|
|
85
|
+
units = ["B", "KiB", "MiB", "GiB", "TiB"]
|
|
86
|
+
index = 0
|
|
87
|
+
size = bytes.to_f
|
|
88
|
+
while size >= 1024 && index < units.size - 1
|
|
89
|
+
size /= 1024
|
|
90
|
+
index += 1
|
|
67
91
|
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
|
|
92
|
+
(index.zero? ? "#{bytes} B" : format("%.1f %s", size, units[index]))
|
|
72
93
|
end
|
|
73
94
|
end
|
|
74
95
|
end
|
|
@@ -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
|
data/lib/deploio/nctl_client.rb
CHANGED
|
@@ -197,6 +197,14 @@ module Deploio
|
|
|
197
197
|
nil
|
|
198
198
|
end
|
|
199
199
|
|
|
200
|
+
def get_bucket_user_access_key(name, project:)
|
|
201
|
+
capture("get", "bucketuser", name, "--project", project, "--print-access-key").strip
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def get_bucket_user_secret_key(name, project:)
|
|
205
|
+
capture("get", "bucketuser", name, "--project", project, "--print-secret-key").strip
|
|
206
|
+
end
|
|
207
|
+
|
|
200
208
|
def get_projects
|
|
201
209
|
output = capture("get", "projects", "-o", "json")
|
|
202
210
|
return [] if output.nil? || output.empty?
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deploio
|
|
4
|
+
# Backups for the dedicated tier (kind: Postgres), where we own the whole
|
|
5
|
+
# database server and reach it over SSH
|
|
6
|
+
# The naming is confusing, but this is how Nine names them and how the resources appear, so prefer to stay
|
|
7
|
+
# consistent with that
|
|
8
|
+
class PostgresBackupService
|
|
9
|
+
DEFAULT_EXTENSION = ".zst"
|
|
10
|
+
|
|
11
|
+
# @param name [String] the name the user typed, used for hints in messages
|
|
12
|
+
def initialize(data:, name: nil, dry_run: false)
|
|
13
|
+
@data = data || {}
|
|
14
|
+
@name = name
|
|
15
|
+
@dry_run = dry_run
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def default_destination = "./#{@name}-latest-backup#{DEFAULT_EXTENSION}"
|
|
19
|
+
|
|
20
|
+
def backups
|
|
21
|
+
raise Deploio::UnsupportedBackupOperationError,
|
|
22
|
+
"Listing backups is not yet supported for dedicated PostgreSQL instances; Feel free to implement it!\n" \
|
|
23
|
+
"Use 'deploio pg backups download #{@name}' to fetch it."
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def capture
|
|
27
|
+
cmd = ["ssh", "dbadmin@#{fqdn}", "sudo nine-postgresql-backup"]
|
|
28
|
+
Output.command(cmd.join(" "))
|
|
29
|
+
system(*cmd) unless @dry_run
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def download(destination:, db_name: nil)
|
|
33
|
+
name = resolve_db_name(db_name)
|
|
34
|
+
|
|
35
|
+
cmd = ["rsync", "-av", "dbadmin@#{fqdn}:~/backup/postgresql/latest/customer/#{name}/#{name}.zst", destination]
|
|
36
|
+
Output.command(cmd.join(" "))
|
|
37
|
+
system(*cmd) unless @dry_run
|
|
38
|
+
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def resolve_db_name(db_name)
|
|
45
|
+
if databases.empty?
|
|
46
|
+
raise Deploio::Error, "No databases found in PostgreSQL instance; cannot download backup."
|
|
47
|
+
elsif databases.size > 1 && db_name.nil?
|
|
48
|
+
raise Deploio::Error,
|
|
49
|
+
"Multiple databases found in PostgreSQL instance\n" \
|
|
50
|
+
"Databases: #{databases.join(", ")}\n" \
|
|
51
|
+
"Please specify the database name using the --db_name option."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
db_name || databases.first
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def databases
|
|
58
|
+
@databases ||= (@data.dig("status", "atProvider", "databases")&.keys || []).reject { |db| db.strip.empty? }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def fqdn
|
|
62
|
+
value = @data.dig("status", "atProvider", "fqdn")
|
|
63
|
+
raise Deploio::Error, "Database FQDN not found; cannot reach the database server." if value.nil? || value.empty?
|
|
64
|
+
|
|
65
|
+
value
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deploio
|
|
4
|
+
# Backups for the economy tier (kind: PostgresDatabase), where the database
|
|
5
|
+
# lives on a shared server we have no access to.
|
|
6
|
+
# The naming is confusing, but this is how Nine names them and how the resources appear, so prefer to stay
|
|
7
|
+
# consistent with that
|
|
8
|
+
class PostgresDatabaseBackupService
|
|
9
|
+
DEFAULT_EXTENSION = ".sql.zst"
|
|
10
|
+
|
|
11
|
+
BACKUP_SCHEDULE_LABEL = "DatabaseBackupSchedule"
|
|
12
|
+
|
|
13
|
+
def initialize(db_ref:, data:, nctl_client:, name: nil, rclone_client_factory: nil)
|
|
14
|
+
@db_ref = db_ref
|
|
15
|
+
@data = data || {}
|
|
16
|
+
@nctl = nctl_client
|
|
17
|
+
@name = name || db_ref.full_name
|
|
18
|
+
@rclone_client_factory = rclone_client_factory || method(:build_rclone_client)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def default_destination
|
|
22
|
+
"./#{@name}-latest-backup#{DEFAULT_EXTENSION}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Nine takes these backups on a schedule
|
|
26
|
+
# (you can even see them using `kubectl get databasebackupschedules.storage.nine.ch -n renuo-chess-tracker -o json`)
|
|
27
|
+
# => there is no way to trigger one.
|
|
28
|
+
def capture
|
|
29
|
+
raise Deploio::UnsupportedBackupOperationError,
|
|
30
|
+
"'#{@db_ref.full_name}' is an economy-tier database. Those are backed up automatically " \
|
|
31
|
+
"on their configured schedule and cannot be captured manually.\n" \
|
|
32
|
+
"Use 'deploio pg backups list #{@name}' to see the available backups."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def backups
|
|
36
|
+
@backups ||= begin
|
|
37
|
+
entries = rclone.list(bucket_name)
|
|
38
|
+
entries = entries.select { |e| own_backup?(e["Name"].to_s) }
|
|
39
|
+
entries.sort_by { |e| e["ModTime"].to_s }.reverse
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def download(destination:, db_name: nil)
|
|
44
|
+
backup = backups.first
|
|
45
|
+
unless backup
|
|
46
|
+
raise Deploio::Error, "No backups found for '#{@db_ref.full_name}' in bucket '#{bucket_name}'."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
rclone.download(bucket_name, backup["Name"], destination)
|
|
50
|
+
backup
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def own_backup?(object_name)
|
|
56
|
+
return true if instance_name.empty?
|
|
57
|
+
|
|
58
|
+
object_name.start_with?("PostgresDatabase-#{instance_name}-")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def instance_name
|
|
62
|
+
@instance_name ||= @data.dig("status", "atProvider", "name").to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def bucket_name
|
|
66
|
+
bucket.dig("metadata", "name")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The PostgresDatabase resource holds no reference to its backup bucket
|
|
70
|
+
# Changes with service connections (I assume), but we're not there yet as we still have projects with the old setup.
|
|
71
|
+
# Therefore, we match by name
|
|
72
|
+
def bucket
|
|
73
|
+
@bucket ||= begin
|
|
74
|
+
candidates = @nctl.get_services_by_type("bucket", project: @db_ref.project_name).select do |bucket|
|
|
75
|
+
backup_bucket_for_database?(bucket)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if candidates.empty?
|
|
79
|
+
raise Deploio::Error,
|
|
80
|
+
"No backup bucket found for '#{@db_ref.full_name}'. " \
|
|
81
|
+
"Check that backups are enabled for this database (spec.forProvider.backupSchedule)."
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
candidates.first
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def backup_bucket_for_database?(bucket)
|
|
89
|
+
metadata = bucket["metadata"] || {}
|
|
90
|
+
labels = metadata["labels"] || {}
|
|
91
|
+
return false unless labels["nine.ch/controllerKind"] == BACKUP_SCHEDULE_LABEL
|
|
92
|
+
|
|
93
|
+
metadata["name"].to_s.match?(/\Apostgresdatabase-#{Regexp.escape(@db_ref.database_name)}-[0-9a-f]{7}\z/)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def rclone
|
|
97
|
+
@rclone ||= @rclone_client_factory.call
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_rclone_client
|
|
101
|
+
endpoint = bucket.dig("status", "atProvider", "endpoint")
|
|
102
|
+
if endpoint.nil? || endpoint.to_s.empty?
|
|
103
|
+
raise Deploio::Error, "Backup bucket '#{bucket_name}' has no endpoint; cannot access backups."
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
client = RcloneClient.new(
|
|
107
|
+
endpoint: "https://#{endpoint}",
|
|
108
|
+
access_key: @nctl.get_bucket_user_access_key(bucket_name, project: @db_ref.project_name),
|
|
109
|
+
secret_key: @nctl.get_bucket_user_secret_key(bucket_name, project: @db_ref.project_name),
|
|
110
|
+
dry_run: @nctl.dry_run
|
|
111
|
+
)
|
|
112
|
+
client.check_requirements unless @nctl.dry_run
|
|
113
|
+
client
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Deploio
|
|
7
|
+
class RcloneClient
|
|
8
|
+
REMOTE = "DEPLOIO"
|
|
9
|
+
|
|
10
|
+
attr_reader :dry_run
|
|
11
|
+
|
|
12
|
+
def initialize(endpoint:, access_key:, secret_key:, dry_run: false)
|
|
13
|
+
@endpoint = endpoint
|
|
14
|
+
@access_key = access_key
|
|
15
|
+
@secret_key = secret_key
|
|
16
|
+
@dry_run = dry_run
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def check_requirements
|
|
20
|
+
check_rclone_installed
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def list(bucket)
|
|
24
|
+
output = capture("lsjson", remote_path(bucket))
|
|
25
|
+
return [] if output.nil? || output.empty?
|
|
26
|
+
|
|
27
|
+
data = JSON.parse(output)
|
|
28
|
+
data.is_a?(Array) ? data : []
|
|
29
|
+
rescue JSON::ParserError
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def download(bucket, object, destination)
|
|
34
|
+
run("copyto", remote_path(bucket, object), destination, "--progress", "--stats-one-line")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# --s3-no-check-bucket skips the HeadBucket call, which the read-only bucket
|
|
40
|
+
# user is not permitted to make.
|
|
41
|
+
# See also https://docs.nine.ch/docs/object-storage/object-storage-client-tools#rclone
|
|
42
|
+
def build_command(args)
|
|
43
|
+
["rclone", *args.map(&:to_s), "--s3-no-check-bucket"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def remote_path(bucket, object = nil)
|
|
47
|
+
object ? "#{REMOTE}:#{bucket}/#{object}" : "#{REMOTE}:#{bucket}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def env
|
|
51
|
+
{
|
|
52
|
+
"RCLONE_CONFIG_#{REMOTE}_TYPE" => "s3",
|
|
53
|
+
"RCLONE_CONFIG_#{REMOTE}_PROVIDER" => "Other",
|
|
54
|
+
"RCLONE_CONFIG_#{REMOTE}_ENDPOINT" => @endpoint,
|
|
55
|
+
"RCLONE_CONFIG_#{REMOTE}_ACCESS_KEY_ID" => @access_key,
|
|
56
|
+
"RCLONE_CONFIG_#{REMOTE}_SECRET_ACCESS_KEY" => @secret_key
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def capture(*args)
|
|
61
|
+
cmd = build_command(args)
|
|
62
|
+
if dry_run
|
|
63
|
+
Output.command(cmd.join(" "))
|
|
64
|
+
return ""
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
puts "> #{cmd.join(" ")}" if ENV["DEPLOIO_DEBUG"]
|
|
68
|
+
stdout, stderr, status = Open3.capture3(env, *cmd)
|
|
69
|
+
unless status.success?
|
|
70
|
+
raise Deploio::RcloneError, "rclone command failed: #{stderr}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
stdout
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def run(*args)
|
|
77
|
+
cmd = build_command(args)
|
|
78
|
+
Output.command(cmd.join(" "))
|
|
79
|
+
return true if dry_run
|
|
80
|
+
|
|
81
|
+
unless system(env, *cmd)
|
|
82
|
+
raise Deploio::RcloneError, "rclone command failed: #{cmd.join(" ")}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def check_rclone_installed
|
|
89
|
+
_stdout, _stderr, status = Open3.capture3("rclone", "version")
|
|
90
|
+
return if status.success?
|
|
91
|
+
|
|
92
|
+
raise Deploio::RcloneError,
|
|
93
|
+
"rclone not found. Please install it: brew install rclone"
|
|
94
|
+
rescue Errno::ENOENT
|
|
95
|
+
raise Deploio::RcloneError,
|
|
96
|
+
"rclone not found. Please install it: brew install rclone"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -7,8 +7,8 @@ module Deploio
|
|
|
7
7
|
def self.included(base)
|
|
8
8
|
base.class_option :app, aliases: "-a", type: :string, desc: "App in <project>-<app> format"
|
|
9
9
|
base.class_option :org, aliases: "-o", type: :string, desc: "Organization"
|
|
10
|
-
base.class_option :dry_run, type: :boolean,
|
|
11
|
-
base.class_option :no_color, type: :boolean,
|
|
10
|
+
base.class_option :dry_run, type: :boolean, desc: "Print commands without executing"
|
|
11
|
+
base.class_option :no_color, type: :boolean, desc: "Disable colored output"
|
|
12
12
|
|
|
13
13
|
base.define_singleton_method(:exit_on_failure?) { true }
|
|
14
14
|
end
|
data/lib/deploio/version.rb
CHANGED
data/lib/deploio.rb
CHANGED
|
@@ -8,8 +8,11 @@ require_relative "deploio/output"
|
|
|
8
8
|
require_relative "deploio/app_ref"
|
|
9
9
|
require_relative "deploio/pg_database_ref"
|
|
10
10
|
require_relative "deploio/nctl_client"
|
|
11
|
+
require_relative "deploio/rclone_client"
|
|
11
12
|
require_relative "deploio/app_resolver"
|
|
12
13
|
require_relative "deploio/pg_database_resolver"
|
|
14
|
+
require_relative "deploio/postgres_backup_service"
|
|
15
|
+
require_relative "deploio/postgres_database_backup_service"
|
|
13
16
|
require_relative "deploio/price_fetcher"
|
|
14
17
|
require_relative "deploio/shared_options"
|
|
15
18
|
require_relative "deploio/cli"
|
|
@@ -19,4 +22,6 @@ module Deploio
|
|
|
19
22
|
class AppNotFoundError < Error; end
|
|
20
23
|
class PgDatabaseNotFoundError < Error; end
|
|
21
24
|
class NctlError < Error; end
|
|
25
|
+
class RcloneError < Error; end
|
|
26
|
+
class UnsupportedBackupOperationError < Error; end
|
|
22
27
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: deploio-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Renuo AG
|
|
@@ -67,7 +67,10 @@ files:
|
|
|
67
67
|
- lib/deploio/output.rb
|
|
68
68
|
- lib/deploio/pg_database_ref.rb
|
|
69
69
|
- lib/deploio/pg_database_resolver.rb
|
|
70
|
+
- lib/deploio/postgres_backup_service.rb
|
|
71
|
+
- lib/deploio/postgres_database_backup_service.rb
|
|
70
72
|
- lib/deploio/price_fetcher.rb
|
|
73
|
+
- lib/deploio/rclone_client.rb
|
|
71
74
|
- lib/deploio/shared_options.rb
|
|
72
75
|
- lib/deploio/templates/completion.zsh.erb
|
|
73
76
|
- lib/deploio/utils.rb
|