-
Notifications
You must be signed in to change notification settings - Fork 0
Postgres Economy DB support #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
11f289d
1b3e46b
9eb5158
2ff7519
05b7859
db850a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,74 +1,95 @@ | ||
| require "time" | ||
|
|
||
| module Deploio | ||
| module Commands | ||
| class PostgreSQLBackups < Thor | ||
| include SharedOptions | ||
|
|
||
| namespace "pg:backups" | ||
|
|
||
| DEDICATED_KIND = "Postgres" | ||
| ECONOMY_KIND = "PostgresDatabase" | ||
|
|
||
| desc "capture NAME", "Capture a new backup for the specified PostgreSQL database" | ||
| def capture(name) | ||
| setup_options | ||
| resolver = PgDatabaseResolver.new(nctl_client: @nctl) | ||
| db_ref = resolver.resolve(database_name: name) | ||
| data = @nctl.get_pg_database(db_ref) | ||
| kind = data["kind"] || "" | ||
| backup_service_for(name).capture | ||
| rescue Deploio::Error => e | ||
| Output.error(e.message) | ||
| exit 1 | ||
| end | ||
|
|
||
| unless kind == "Postgres" || @nctl.dry_run | ||
| Output.error("Backups can only be captured for PostgreSQL databases. (shared dbs are not supported)") | ||
| exit 1 | ||
| desc "list NAME", "List the available backups for the specified PostgreSQL database" | ||
| def list(name) | ||
| backups = backup_service_for(name).backups | ||
| if backups.empty? | ||
| Output.warning("No backups found for '#{name}'") | ||
| return | ||
| end | ||
|
|
||
| fqdn = data.dig("status", "atProvider", "fqdn") | ||
| if fqdn.nil? || fqdn.empty? | ||
| Output.error("Database FQDN not found; cannot capture backup.") | ||
| exit 1 | ||
| rows = backups.map do |backup| | ||
| [format_time(backup["ModTime"]), format_size(backup["Size"]), backup["Name"]] | ||
| end | ||
|
|
||
| cmd = ["ssh", "dbadmin@#{fqdn}", "sudo nine-postgresql-backup"] | ||
| Output.command(cmd.join(" ")) | ||
| system(*cmd) unless @nctl.dry_run | ||
| Output.table(rows, headers: ["DATE", "SIZE", "NAME"]) | ||
| rescue Deploio::Error => e | ||
| Output.error(e.message) | ||
| exit 1 | ||
| end | ||
|
Comment on lines
+22
to
36
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sneaky new feature: Implemented a list command for economy dbs as it's fairly cheap (we have to list the backups anyway). |
||
|
|
||
| desc "download NAME [--output destination_path]", "Download the latest backup for the specified PostgreSQL database instance" | ||
| method_option :output, type: :string, desc: "Output file path (defaults to current directory with auto-generated name)" | ||
| method_option :db_name, type: :string, desc: "If there are multiple DBs, specify which one to download the backup for", default: nil | ||
| def download(name) | ||
| destination = options[:output] || "./#{name}-latest-backup.zst" | ||
| service = backup_service_for(name) | ||
| destination = merged_options[:output] || service.default_destination | ||
| backup = service.download(destination: destination, db_name: merged_options[:db_name]) | ||
|
|
||
| # Only the economy tier knows when its backup was taken. | ||
| if backup | ||
| Output.success("Downloaded backup from #{format_time(backup["ModTime"])} to #{destination}") | ||
| else | ||
| Output.success("Downloaded backup to #{destination}") | ||
| end | ||
| rescue Deploio::Error => e | ||
| Output.error(e.message) | ||
| exit 1 | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def backup_service_for(name) | ||
| setup_options | ||
| resolver = PgDatabaseResolver.new(nctl_client: @nctl) | ||
| db_ref = resolver.resolve(database_name: name) | ||
| data = @nctl.get_pg_database(db_ref) | ||
| kind = data["kind"] || "" | ||
|
|
||
| unless kind == "Postgres" || @nctl.dry_run | ||
| Output.error("Backups can only be downloaded for PostgreSQL databases. (shared dbs are not supported)") | ||
| exit 1 | ||
| end | ||
| raise Deploio::Error, "Could not read database '#{db_ref.full_name}'." if data.nil? | ||
|
|
||
| databases = data.dig("status", "atProvider", "databases")&.keys || [] | ||
| databases.reject! { |db| db.strip.empty? } | ||
| if databases.empty? | ||
| Output.error("No databases found in PostgreSQL instance; cannot download backup.") | ||
| exit 1 | ||
| elsif databases.size > 1 && options[:db_name].nil? | ||
| Output.error("Multiple databases found in PostgreSQL instance") | ||
| Output.error("Databases: #{databases.join(", ")}") | ||
| Output.error("Please specify the database name using the --db_name option.") | ||
| exit 1 | ||
| case data["kind"] | ||
| when DEDICATED_KIND | ||
| PostgresBackupService.new(data: data, name: name, dry_run: @nctl.dry_run) | ||
| when ECONOMY_KIND | ||
| PostgresDatabaseBackupService.new(db_ref: db_ref, data: data, nctl_client: @nctl, name: name) | ||
|
Comment on lines
+67
to
+70
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted the logic that previously was inlined here into two services, one for managing dedicated dbs and one for the shared dbs |
||
| else | ||
| raise Deploio::UnsupportedBackupOperationError, | ||
| "Backups are not supported for databases of kind '#{data["kind"]}'." | ||
| end | ||
| end | ||
|
|
||
| db_name = options[:db_name] || databases.first | ||
| def format_time(value) | ||
| Time.parse(value.to_s).localtime.strftime("%Y-%m-%d %H:%M") | ||
| rescue ArgumentError, TypeError | ||
| value.to_s | ||
| end | ||
|
|
||
| fqdn = data.dig("status", "atProvider", "fqdn") | ||
| if fqdn.nil? || fqdn.empty? | ||
| Output.error("Database FQDN not found; cannot download backup.") | ||
| exit 1 | ||
| def format_size(bytes) | ||
| bytes = bytes.to_i | ||
| units = ["B", "KiB", "MiB", "GiB", "TiB"] | ||
| index = 0 | ||
| size = bytes.to_f | ||
| while size >= 1024 && index < units.size - 1 | ||
| size /= 1024 | ||
| index += 1 | ||
| end | ||
|
|
||
| cmd = ["rsync", "-avz", "dbadmin@#{fqdn}:~/backup/postgresql/latest/customer/#{db_name}/#{db_name}.zst", destination] | ||
| Output.command(cmd.join(" ")) | ||
| system(*cmd) unless @nctl.dry_run | ||
| (index.zero? ? "#{bytes} B" : format("%.1f %s", size, units[index])) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Deploio | ||
| # Backups for the dedicated tier (kind: Postgres), where we own the whole | ||
| # database server and reach it over SSH | ||
| # The naming is confusing, but this is how Nine names them and how the resources appear, so prefer to stay | ||
| # consistent with that | ||
| class PostgresBackupService | ||
| DEFAULT_EXTENSION = ".zst" | ||
|
|
||
| # @param name [String] the name the user typed, used for hints in messages | ||
| def initialize(data:, name: nil, dry_run: false) | ||
| @data = data || {} | ||
| @name = name | ||
| @dry_run = dry_run | ||
| end | ||
|
|
||
| def default_destination = "./#{@name}-latest-backup#{DEFAULT_EXTENSION}" | ||
|
|
||
| def backups | ||
| raise Deploio::UnsupportedBackupOperationError, | ||
| "Listing backups is not yet supported for dedicated PostgreSQL instances; Feel free to implement it!\n" \ | ||
| "Use 'deploio pg backups download #{@name}' to fetch it." | ||
| end | ||
|
|
||
| def capture | ||
| cmd = ["ssh", "dbadmin@#{fqdn}", "sudo nine-postgresql-backup"] | ||
| Output.command(cmd.join(" ")) | ||
| system(*cmd) unless @dry_run | ||
| end | ||
|
|
||
| def download(destination:, db_name: nil) | ||
| name = resolve_db_name(db_name) | ||
|
|
||
| cmd = ["rsync", "-av", "dbadmin@#{fqdn}:~/backup/postgresql/latest/customer/#{name}/#{name}.zst", destination] | ||
| Output.command(cmd.join(" ")) | ||
| system(*cmd) unless @dry_run | ||
|
|
||
| nil | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def resolve_db_name(db_name) | ||
| if databases.empty? | ||
| raise Deploio::Error, "No databases found in PostgreSQL instance; cannot download backup." | ||
| elsif databases.size > 1 && db_name.nil? | ||
| raise Deploio::Error, | ||
| "Multiple databases found in PostgreSQL instance\n" \ | ||
| "Databases: #{databases.join(", ")}\n" \ | ||
| "Please specify the database name using the --db_name option." | ||
| end | ||
|
|
||
| db_name || databases.first | ||
| end | ||
|
|
||
| def databases | ||
| @databases ||= (@data.dig("status", "atProvider", "databases")&.keys || []).reject { |db| db.strip.empty? } | ||
| end | ||
|
|
||
| def fqdn | ||
| value = @data.dig("status", "atProvider", "fqdn") | ||
| raise Deploio::Error, "Database FQDN not found; cannot reach the database server." if value.nil? || value.empty? | ||
|
|
||
| value | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to shared_options