Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ as a flag to the `development` or `production` commands:
development restore-from production --parallelize
```

When restoring to development, you can specify a specific backup using either:

* `--backup-id` to download a specific backup by its Heroku backup ID:
```shell
development restore production --backup-id a1234
```

* `--backup-url` to download from a specific URL (as returned by `heroku pg:backups:url`):
```shell
development restore production --backup-url https://...
```

[2]: http://redis.io/commands

Convention
Expand Down
33 changes: 29 additions & 4 deletions lib/parity/backup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def initialize(args)
@from, @to = args.values_at(:from, :to)
@additional_args = args[:additional_args] || BLANK_ARGUMENTS
@parallelize = args[:parallelize] || false
@backup_id = args[:backup_id]
@backup_url = args[:backup_url]
end

def restore
Expand All @@ -25,7 +27,8 @@ def restore

private

attr_reader :additional_args, :from, :to, :parallelize
attr_reader :additional_args, :backup_id, :backup_url, :from, :to,
:parallelize

alias :parallelize? :parallelize

Expand Down Expand Up @@ -77,21 +80,43 @@ def ensure_temp_directory_exists
end

def download_remote_backup
if backup_url
download_from_url
elsif backup_id
download_backup_by_id
else
download_latest_backup
end
end

def download_from_url
Kernel.system("curl -o tmp/parity.backup \"#{backup_url}\"")
end

def download_backup_by_id
Kernel.system(
"curl -o tmp/parity.backup "\
"\"$(heroku pg:backups:url #{backup_id} --remote #{from})\"",
)
end

def download_latest_backup
Kernel.system(
"curl -o tmp/latest.backup \"$(heroku pg:backups:url --remote #{from})\"",
"curl -o tmp/parity.backup "\
"\"$(heroku pg:backups:url --remote #{from})\"",
)
end

def restore_from_local_temp_backup
Kernel.system(
"pg_restore tmp/latest.backup --verbose --no-acl --no-owner "\
"pg_restore tmp/parity.backup --verbose --no-acl --no-owner "\
"--dbname #{development_db} --jobs=#{processor_cores} "\
"#{additional_args}",
)
end

def delete_local_temp_backup
Kernel.system("rm tmp/latest.backup")
Kernel.system("rm tmp/parity.backup")
end

def delete_rails_production_environment_settings
Expand Down
31 changes: 27 additions & 4 deletions lib/parity/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@ def restore
$stdout.puts "Parity does not support restoring backups into your "\
"production environment. Use `--force` to override."
else
Backup.new(
Backup.new(**{
from: arguments.first,
to: environment,
parallelize: parallelize?,
backup_id: backup_id,
backup_url: backup_url,
additional_args: additional_restore_arguments,
).restore
}.compact).restore
end
end

Expand All @@ -90,9 +92,30 @@ def parallelize?
arguments.include?("--parallelize")
end

def backup_id
argument_value("--backup-id")
end

def backup_url
argument_value("--backup-url")
end

def argument_value(flag)
index = arguments.index(flag)
arguments[index + 1] if index
end

def additional_restore_arguments
(arguments.drop(1) - ["--force", "--parallelize"] +
[restore_confirmation_argument]).compact.join(" ")
args = arguments.dup
# Remove the remote environment name
args = args.drop(1)
# Remove toggle options --force and --parallelize
args -= ["--force", "--parallelize"]
# Remove key/value options
args -= ["--backup-id", backup_id] if backup_id
args -= ["--backup-url", backup_url] if backup_url

(args + [restore_confirmation_argument]).compact.join(" ")
end

def restore_confirmation_argument
Expand Down
116 changes: 113 additions & 3 deletions spec/parity/backup_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,103 @@
expect(Kernel).to have_received(:system).with(set_db_metadata_sql)
end

it "downloads backup from URL when backup_url is specified" do
allow(IO).to receive(:read).and_return(database_fixture)
allow(Kernel).to receive(:system)
allow(Etc).to receive(:nprocessors).and_return(number_of_processes)

Parity::Backup.new(
from: "production",
to: "development",
backup_url: "https://example.com/backup.dump",
parallelize: true,
).restore

expect(Kernel).
to have_received(:system).
with(make_temp_directory_command)
expect(Kernel).
to have_received(:system).
with(download_from_url_command("https://example.com/backup.dump"))
expect(Kernel).
to have_received(:system).
with(drop_development_database_drop_command)
expect(Kernel).
to have_received(:system).
with(create_heroku_ext_schema_command)
expect(Kernel).
to have_received(:system).
with(restore_from_local_temp_backup_command)
expect(Kernel).
to have_received(:system).
with(delete_local_temp_backup_command)
end

it "downloads specific backup by id when backup_id is specified" do
allow(IO).to receive(:read).and_return(database_fixture)
allow(Kernel).to receive(:system)
allow(Etc).to receive(:nprocessors).and_return(number_of_processes)

Parity::Backup.new(
from: "production",
to: "development",
backup_id: "a1234",
parallelize: true,
).restore

expect(Kernel).
to have_received(:system).
with(make_temp_directory_command)
expect(Kernel).
to have_received(:system).
with(download_backup_by_id_command(id: "a1234", remote: "production"))
expect(Kernel).
to have_received(:system).
with(drop_development_database_drop_command)
expect(Kernel).
to have_received(:system).
with(create_heroku_ext_schema_command)
expect(Kernel).
to have_received(:system).
with(restore_from_local_temp_backup_command)
expect(Kernel).
to have_received(:system).
with(delete_local_temp_backup_command)
end

it "prefers backup_url over backup_id when both are specified" do
allow(IO).to receive(:read).and_return(database_fixture)
allow(Kernel).to receive(:system)
allow(Etc).to receive(:nprocessors).and_return(number_of_processes)

Parity::Backup.new(
from: "production",
to: "development",
backup_url: "https://example.com/backup.dump",
backup_id: "a1234",
parallelize: true,
).restore

expect(Kernel).
to have_received(:system).
with(make_temp_directory_command)
expect(Kernel).
to have_received(:system).
with(download_from_url_command("https://example.com/backup.dump"))
expect(Kernel).
not_to have_received(:system).
with(download_backup_by_id_command(id: "a1234", remote: "production"))
expect(Kernel).
to have_received(:system).
with(drop_development_database_drop_command)
expect(Kernel).
to have_received(:system).
with(restore_from_local_temp_backup_command)
expect(Kernel).
to have_received(:system).
with(delete_local_temp_backup_command)
end

it "is able to load a database.yml file containing top-level ERB" do
allow(IO).to receive(:read).and_return(database_fixture_with_erb)
allow(Kernel).to receive(:system)
Expand Down Expand Up @@ -254,12 +351,25 @@ def make_temp_directory_command
"mkdir -p tmp"
end

def copy_local_backup_command(source:)
"cp \"#{source}\" tmp/parity.backup"
end

def download_remote_database_command
'curl -o tmp/latest.backup "$(heroku pg:backups:url --remote production)"'
'curl -o tmp/parity.backup "$(heroku pg:backups:url --remote production)"'
end

def download_from_url_command(url)
"curl -o tmp/parity.backup \"#{url}\""
end

def download_backup_by_id_command(id:, remote:)
"curl -o tmp/parity.backup "\
"\"$(heroku pg:backups:url #{id} --remote #{remote})\""
end

def restore_from_local_temp_backup_command(cores: number_of_processes)
"pg_restore tmp/latest.backup --verbose --no-acl --no-owner "\
"pg_restore tmp/parity.backup --verbose --no-acl --no-owner "\
"--dbname #{default_db_name} --jobs=#{cores} "
end

Expand All @@ -268,7 +378,7 @@ def number_of_processes
end

def delete_local_temp_backup_command
"rm tmp/latest.backup"
"rm tmp/parity.backup"
end

def heroku_development_to_staging_passthrough(db_name: default_db_name)
Expand Down
Loading