Files
ubicloud/spec/model/postgres/postgres_timeline_spec.rb
Furkan Sahin c4256995d1 Fix S3 Endpoint related errors crashing UI
We use s3 api calls to server read replica and backup/restore pages.
When the endpoints fail, which happens when the server is freshly
provisioned, these pages cause 500. This commit fixes that by simply
swallowing some of the error messages.
2025-07-07 18:14:38 +02:00

220 lines
11 KiB
Ruby

# frozen_string_literal: true
require_relative "../spec_helper"
RSpec.describe PostgresTimeline do
subject(:postgres_timeline) { described_class.create_with_id(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"}]})
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 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 "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", prefix: "basebackups_005/", expiration: {days: 8}}]}}).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