It hasn't been necessary to use create_with_id since
ebc79622df
, in December 2024.
I have plans to introduce:
```ruby
def create_with_id(id, values)
obj = new(values)
obj.id = id
obj.save_changes
end
```
This will make it easier to use the same id when creating
multiple objects. The first step is removing the existing
uses of create_with_id.
238 lines
12 KiB
Ruby
238 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "../spec_helper"
|
|
|
|
RSpec.describe PostgresTimeline do
|
|
subject(:postgres_timeline) { described_class.create(access_key: "dummy-access-key", secret_key: "dummy-secret-key", location_id: Location::HETZNER_FSN1_ID) }
|
|
|
|
it "returns ubid as bucket name" do
|
|
expect(postgres_timeline.bucket_name).to eq(postgres_timeline.ubid)
|
|
end
|
|
|
|
it "returns walg config" do
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, url: "https://blob-endpoint"))
|
|
|
|
walg_config = <<-WALG_CONF
|
|
WALG_S3_PREFIX=s3://#{postgres_timeline.ubid}
|
|
AWS_ENDPOINT=https://blob-endpoint
|
|
AWS_ACCESS_KEY_ID=dummy-access-key
|
|
AWS_SECRET_ACCESS_KEY=dummy-secret-key
|
|
AWS_REGION: us-east-1
|
|
AWS_S3_FORCE_PATH_STYLE=true
|
|
PGHOST=/var/run/postgresql
|
|
WALG_CONF
|
|
|
|
expect(postgres_timeline.generate_walg_config).to eq(walg_config)
|
|
expect(postgres_timeline).to receive(:aws?).and_return(true)
|
|
expect(postgres_timeline).to receive(:location).and_return(instance_double(Location, name: "us-east-2"))
|
|
expect(postgres_timeline.generate_walg_config).to eq(walg_config.sub("us-east-1", "us-east-2"))
|
|
end
|
|
|
|
describe "#need_backup?" do
|
|
let(:sshable) { instance_double(Sshable) }
|
|
let(:leader) {
|
|
instance_double(
|
|
PostgresServer,
|
|
strand: instance_double(Strand, label: "wait"),
|
|
vm: instance_double(Vm, sshable: sshable)
|
|
)
|
|
}
|
|
|
|
before do
|
|
allow(postgres_timeline).to receive(:leader).and_return(leader).at_least(:once)
|
|
end
|
|
|
|
it "returns false as backup needed if there is no backup endpoint is set" do
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return(nil)
|
|
expect(postgres_timeline.need_backup?).to be(false)
|
|
end
|
|
|
|
it "returns false as backup needed if there is no leader" do
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return("dummy-blob-storage")
|
|
expect(postgres_timeline).to receive(:leader).and_return(nil)
|
|
expect(postgres_timeline.need_backup?).to be(false)
|
|
end
|
|
|
|
it "returns true as backup needed if there is no backup process or the last backup failed" do
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return("dummy-blob-storage").twice
|
|
expect(sshable).to receive(:cmd).and_return("NotStarted", "Failed")
|
|
expect(postgres_timeline.need_backup?).to be(true)
|
|
expect(postgres_timeline.need_backup?).to be(true)
|
|
end
|
|
|
|
it "returns true as backup needed if previous backup started more than a day ago and is succeeded" do
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return("dummy-blob-storage")
|
|
expect(postgres_timeline).to receive(:latest_backup_started_at).and_return(Time.now - 60 * 60 * 25).twice
|
|
expect(sshable).to receive(:cmd).and_return("Succeeded")
|
|
expect(postgres_timeline.need_backup?).to be(true)
|
|
end
|
|
|
|
it "returns false as backup needed if previous backup started less than a day ago" do
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return("dummy-blob-storage")
|
|
expect(postgres_timeline).to receive(:latest_backup_started_at).and_return(Time.now - 60 * 60 * 23).twice
|
|
expect(sshable).to receive(:cmd).and_return("Succeeded")
|
|
expect(postgres_timeline.need_backup?).to be(false)
|
|
end
|
|
|
|
it "returns false as backup needed if previous backup started is in progress" do
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return("dummy-blob-storage")
|
|
expect(sshable).to receive(:cmd).and_return("InProgress")
|
|
expect(postgres_timeline.need_backup?).to be(false)
|
|
end
|
|
end
|
|
|
|
describe "#latest_backup_label_before_target" do
|
|
it "returns most recent backup before given target" do
|
|
most_recent_backup_time = Time.now
|
|
expect(postgres_timeline).to receive(:backups).and_return(
|
|
[
|
|
instance_double(Minio::Client::Blob, key: "basebackups_005/0001_backup_stop_sentinel.json", last_modified: most_recent_backup_time - 200),
|
|
instance_double(Minio::Client::Blob, key: "basebackups_005/0002_backup_stop_sentinel.json", last_modified: most_recent_backup_time - 100),
|
|
instance_double(Minio::Client::Blob, key: "basebackups_005/0003_backup_stop_sentinel.json", last_modified: most_recent_backup_time)
|
|
]
|
|
)
|
|
|
|
expect(postgres_timeline.latest_backup_label_before_target(target: most_recent_backup_time - 50)).to eq("0002")
|
|
end
|
|
|
|
it "raises error if no backups before given target" do
|
|
expect(postgres_timeline).to receive(:backups).and_return([])
|
|
|
|
expect { postgres_timeline.latest_backup_label_before_target(target: Time.now) }.to raise_error RuntimeError, "BUG: no backup found"
|
|
end
|
|
end
|
|
|
|
it "returns empty array if blob storage is not configured" do
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return(nil)
|
|
expect(postgres_timeline.backups).to eq([])
|
|
end
|
|
|
|
it "returns empty array if user is not created yet" do
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, url: "https://blob-endpoint", root_certs: "certs")).at_least(:once)
|
|
minio_client = instance_double(Minio::Client)
|
|
expect(minio_client).to receive(:list_objects).and_raise(RuntimeError.new("The AWS Access Key Id you provided does not exist in our records."))
|
|
expect(Minio::Client).to receive(:new).and_return(minio_client)
|
|
expect(postgres_timeline.backups).to eq([])
|
|
end
|
|
|
|
it "re-raises exceptions other than missin access key" do
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, url: "https://blob-endpoint", root_certs: "certs")).at_least(:once)
|
|
minio_client = instance_double(Minio::Client)
|
|
expect(minio_client).to receive(:list_objects).and_raise(RuntimeError.new("some error"))
|
|
expect(Minio::Client).to receive(:new).and_return(minio_client)
|
|
expect { postgres_timeline.backups }.to raise_error(RuntimeError)
|
|
end
|
|
|
|
it "returns list of backups" do
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, url: "https://blob-endpoint", root_certs: "certs")).at_least(:once)
|
|
|
|
minio_client = Minio::Client.new(endpoint: "https://blob-endpoint", access_key: "access_key", secret_key: "secret_key", ssl_ca_data: "data")
|
|
expect(minio_client).to receive(:list_objects).with(postgres_timeline.ubid, "basebackups_005/").and_return([instance_double(Minio::Client::Blob, key: "backup_stop_sentinel.json"), instance_double(Minio::Client::Blob, key: "unrelated_file.txt")])
|
|
expect(Minio::Client).to receive(:new).and_return(minio_client)
|
|
|
|
expect(postgres_timeline.backups.map(&:key)).to eq(["backup_stop_sentinel.json"])
|
|
end
|
|
|
|
it "returns list of backups for AWS regions" do
|
|
expect(postgres_timeline).to receive(:location).and_return(instance_double(Location, aws?: true, name: "us-west-2")).at_least(:once)
|
|
|
|
s3_client = Aws::S3::Client.new(stub_responses: true)
|
|
s3_client.stub_responses(:list_objects_v2, {contents: [{key: "backup_stop_sentinel.json"}, {key: "unrelated_file.txt"}], is_truncated: false})
|
|
expect(s3_client).to receive(:list_objects_v2).with(bucket: postgres_timeline.ubid, prefix: "basebackups_005/").and_call_original
|
|
expect(Aws::S3::Client).to receive(:new).and_return(s3_client)
|
|
expect(postgres_timeline.backups.map(&:key)).to eq(["backup_stop_sentinel.json"])
|
|
end
|
|
|
|
it "returns list of backups with enumeration for AWS regions" do
|
|
expect(postgres_timeline).to receive(:location).and_return(instance_double(Location, aws?: true, name: "us-west-2")).at_least(:once)
|
|
|
|
s3_client = Aws::S3::Client.new(stub_responses: true)
|
|
s3_client.stub_responses(:list_objects_v2, {contents: [{key: "backup_stop_sentinel.json"}, {key: "unrelated_file.txt"}], is_truncated: true, next_continuation_token: "token"}, {contents: [{key: "backup_stop_sentinel.json"}, {key: "unrelated_file.txt"}], is_truncated: false})
|
|
expect(s3_client).to receive(:list_objects_v2).with(bucket: postgres_timeline.ubid, prefix: "basebackups_005/").and_call_original
|
|
expect(s3_client).to receive(:list_objects_v2).with(bucket: postgres_timeline.ubid, prefix: "basebackups_005/", continuation_token: "token").and_call_original
|
|
expect(Aws::S3::Client).to receive(:new).and_return(s3_client)
|
|
expect(postgres_timeline.backups.map(&:key)).to eq(["backup_stop_sentinel.json", "backup_stop_sentinel.json"])
|
|
end
|
|
|
|
it "returns blob storage endpoint" do
|
|
expect(MinioCluster).to receive(:[]).and_return(instance_double(MinioCluster, url: "https://blob-endpoint"))
|
|
expect(postgres_timeline.blob_storage_endpoint).to eq("https://blob-endpoint")
|
|
end
|
|
|
|
it "returns blob storage client from cache" do
|
|
expect(postgres_timeline).to receive(:blob_storage_endpoint).and_return("https://blob-endpoint")
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, root_certs: "certs")).once
|
|
expect(Minio::Client).to receive(:new).and_return("dummy-client").once
|
|
expect(postgres_timeline.blob_storage_client).to eq("dummy-client")
|
|
expect(postgres_timeline.blob_storage_client).to eq("dummy-client")
|
|
end
|
|
|
|
it "returns blob storage client when aws properly" do
|
|
expect(postgres_timeline).to receive(:location).and_return(nil)
|
|
expect(postgres_timeline).to receive(:blob_storage_endpoint).and_return("https://blob-endpoint")
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, root_certs: "certs")).once
|
|
expect(Minio::Client).to receive(:new).and_return("dummy-client").once
|
|
expect(postgres_timeline.blob_storage_client).to eq("dummy-client")
|
|
end
|
|
|
|
it "returns blob storage policy" do
|
|
policy = {Version: "2012-10-17", Statement: [{Effect: "Allow", Action: ["s3:*"], Resource: ["arn:aws:s3:::dummy-ubid*"]}]}
|
|
expect(postgres_timeline).to receive(:ubid).and_return("dummy-ubid")
|
|
expect(postgres_timeline.blob_storage_policy).to eq(policy)
|
|
end
|
|
|
|
it "returns earliest restore time" do
|
|
expect(postgres_timeline).to receive(:backups).and_return([instance_double(Minio::Client::Blob, last_modified: Time.now - 60 * 60 * 24 * 5)])
|
|
expect(postgres_timeline.earliest_restore_time.to_i).to be_within(5 * 60).of(Time.now.to_i - 60 * 60 * 24 * 5 + 5 * 60)
|
|
end
|
|
|
|
describe "aws" do
|
|
let(:s3_client) { Aws::S3::Client.new(stub_responses: true) }
|
|
|
|
before do
|
|
expect(postgres_timeline).to receive(:aws?).and_return(true).at_least(:once)
|
|
expect(Aws::S3::Client).to receive(:new).and_return(s3_client).at_least(:once)
|
|
end
|
|
|
|
it "creates bucket" do
|
|
expect(postgres_timeline).to receive(:location).and_return(instance_double(Location, name: "us-east-2")).at_least(:once)
|
|
s3_client.stub_responses(:create_bucket)
|
|
expect(s3_client).to receive(:create_bucket).with({bucket: postgres_timeline.ubid, create_bucket_configuration: {location_constraint: "us-east-2"}}).and_return(true)
|
|
expect(postgres_timeline.create_bucket).to be(true)
|
|
end
|
|
|
|
it "creates bucket in us-east-1" do
|
|
expect(postgres_timeline).to receive(:location).and_return(instance_double(Location, name: "us-east-1")).at_least(:once)
|
|
s3_client.stub_responses(:create_bucket)
|
|
expect(s3_client).to receive(:create_bucket).with({bucket: postgres_timeline.ubid, create_bucket_configuration: nil}).and_return(true)
|
|
expect(postgres_timeline.create_bucket).to be(true)
|
|
end
|
|
|
|
it "sets lifecycle policy" do
|
|
s3_client.stub_responses(:put_bucket_lifecycle_configuration)
|
|
expect(s3_client).to receive(:put_bucket_lifecycle_configuration).with({bucket: postgres_timeline.ubid, lifecycle_configuration: {rules: [{id: "DeleteOldBackups", status: "Enabled", expiration: {days: 8}, filter: {}}]}}).and_return(true)
|
|
expect(postgres_timeline.set_lifecycle_policy).to be(true)
|
|
end
|
|
end
|
|
|
|
describe "minio" do
|
|
let(:minio_client) { instance_double(Minio::Client) }
|
|
|
|
before do
|
|
expect(postgres_timeline).to receive(:aws?).and_return(false).at_least(:once)
|
|
expect(postgres_timeline).to receive(:blob_storage).and_return(instance_double(MinioCluster, url: "https://blob-endpoint", root_certs: "certs")).at_least(:once)
|
|
expect(Minio::Client).to receive(:new).and_return(minio_client).at_least(:once)
|
|
end
|
|
|
|
it "creates bucket" do
|
|
expect(minio_client).to receive(:create_bucket).with(postgres_timeline.ubid).and_return(true)
|
|
expect(postgres_timeline.create_bucket).to be(true)
|
|
end
|
|
|
|
it "sets lifecycle policy" do
|
|
expect(minio_client).to receive(:set_lifecycle_policy).with(postgres_timeline.ubid, postgres_timeline.ubid, 8).and_return(true)
|
|
expect(postgres_timeline.set_lifecycle_policy).to be(true)
|
|
end
|
|
end
|
|
end
|