Files
ubicloud/routes/project/location/postgres.rb
Jeremy Evans d4c3eadc85 Add Clover#check_found_object to DRY up code
This DRYs up code in many routes.  Additionally, this fixes cases
where Clover would previous return 204 results for DELETE requests
with garbage at the end of the path.  204 should only be used if
the path would actually be valid.  This checks that the path has
been fully routed before using a 204 response.
2025-05-02 08:25:45 +09:00

325 lines
11 KiB
Ruby

# frozen_string_literal: true
class Clover
hash_branch(:project_location_prefix, "postgres") do |r|
r.get api? do
postgres_list
end
r.on POSTGRES_RESOURCE_NAME_OR_UBID do |pg_name, pg_ubid|
if pg_name
r.post api? do
check_visible_location
postgres_post(pg_name)
end
filter = {Sequel[:postgres_resource][:name] => pg_name}
else
filter = {Sequel[:postgres_resource][:id] => UBID.to_uuid(pg_ubid)}
end
filter[:location_id] = @location.id
pg = @project.postgres_resources_dataset.first(filter)
check_found_object(pg)
r.get true do
authorize("Postgres:view", pg.id)
response.headers["Cache-Control"] = "no-store"
if api?
Serializers::Postgres.serialize(pg, {detailed: true})
else
@pg = Serializers::Postgres.serialize(pg, {detailed: true, include_path: true})
@family = Validation.validate_vm_size(pg.target_vm_size, "x64").family
@option_tree, @option_parents = generate_postgres_configure_options(flavor: @pg[:flavor], location: @location)
view "postgres/show"
end
end
r.delete true do
authorize("Postgres:delete", pg.id)
pg.incr_destroy
204
end
r.patch do
authorize("Postgres:edit", pg.id)
allowed_optional_parameters = ["size", "storage_size", "ha_type"]
ignored_parameters = ["family"]
request_body_params = validate_request_params([], allowed_optional_parameters, ignored_parameters)
target_vm_size = Validation.validate_postgres_size(pg.location, request_body_params["size"] || pg.target_vm_size, @project.id)
target_storage_size_gib = Validation.validate_postgres_storage_size(pg.location, target_vm_size.vm_size, request_body_params["storage_size"] || pg.target_storage_size_gib, @project.id)
ha_type = request_body_params["ha_type"] || PostgresResource::HaType::NONE
Validation.validate_postgres_ha_type(ha_type)
if pg.representative_server.nil? || target_storage_size_gib < pg.representative_server.storage_size_gib
begin
current_disk_usage = pg.representative_server.vm.sshable.cmd("df --output=used /dev/vdb | tail -n 1").strip.to_i / (1024 * 1024)
rescue
fail CloverError.new(400, "InvalidRequest", "Database is not ready for update", {})
end
if target_storage_size_gib * 0.8 < current_disk_usage
fail Validation::ValidationFailed.new({storage_size: "Insufficient storage size is requested. It is only possible to reduce the storage size if the current usage is less than 80% of the requested size."})
end
end
current_postgres_vcpu_count = (PostgresResource::TARGET_STANDBY_COUNT_MAP[pg.ha_type] + 1) * pg.representative_server.vm.vcpus
requested_postgres_vcpu_count = (PostgresResource::TARGET_STANDBY_COUNT_MAP[ha_type] + 1) * target_vm_size.vcpu
Validation.validate_vcpu_quota(@project, "PostgresVCpu", requested_postgres_vcpu_count - current_postgres_vcpu_count)
DB.transaction do
pg.update(target_vm_size: target_vm_size.vm_size, target_storage_size_gib:, ha_type:)
pg.read_replicas.map { it.update(target_vm_size: target_vm_size.vm_size, target_storage_size_gib:) }
end
if api?
Serializers::Postgres.serialize(pg, {detailed: true})
else
flash["notice"] = "'#{pg.name}' will be updated according to requested configuration"
response["Location"] = "#{@project.path}#{pg.path}"
200
end
end
r.post "restart" do
authorize("Postgres:edit", pg.id)
pg.servers.each do |s|
s.incr_restart
rescue Sequel::ForeignKeyConstraintViolation
end
if api?
Serializers::Postgres.serialize(pg, {detailed: true})
else
flash["notice"] = "'#{pg.name}' will be restarted in a few seconds"
r.redirect "#{@project.path}#{pg.path}"
end
end
r.on "firewall-rule" do
r.get api?, true do
authorize("Postgres:view", pg.id)
{
items: Serializers::PostgresFirewallRule.serialize(pg.firewall_rules),
count: pg.firewall_rules.count
}
end
r.post true do
authorize("Postgres:edit", pg.id)
required_parameters = ["cidr"]
request_body_params = validate_request_params(required_parameters)
parsed_cidr = Validation.validate_cidr(request_body_params["cidr"])
firewall_rule = DB.transaction do
pg.incr_update_firewall_rules
PostgresFirewallRule.create_with_id(
postgres_resource_id: pg.id,
cidr: parsed_cidr.to_s
)
end
if api?
Serializers::PostgresFirewallRule.serialize(firewall_rule)
else
flash["notice"] = "Firewall rule is created"
r.redirect "#{@project.path}#{pg.path}"
end
end
r.delete String do |firewall_rule_ubid|
authorize("Postgres:edit", pg.id)
if (fwr = PostgresFirewallRule.from_ubid(firewall_rule_ubid))
DB.transaction do
fwr.destroy
pg.incr_update_firewall_rules
end
end
204
end
end
r.on "metric-destination" do
r.post true do
authorize("Postgres:edit", pg.id)
password_param = (api? ? "password" : "metric-destination-password")
required_parameters = ["url", "username", password_param]
request_body_params = validate_request_params(required_parameters)
Validation.validate_url(request_body_params["url"])
DB.transaction do
PostgresMetricDestination.create_with_id(
postgres_resource_id: pg.id,
url: request_body_params["url"],
username: request_body_params["username"],
password: request_body_params[password_param]
)
pg.servers.each(&:incr_configure_prometheus)
end
if api?
Serializers::Postgres.serialize(pg, {detailed: true})
else
flash["notice"] = "Metric destination is created"
r.redirect "#{@project.path}#{pg.path}"
end
end
r.delete String do |metric_destination_ubid|
authorize("Postgres:edit", pg.id)
if (md = PostgresMetricDestination.from_ubid(metric_destination_ubid))
DB.transaction do
md.destroy
pg.servers.each(&:incr_configure_prometheus)
end
end
204
end
end
r.on "read-replica" do
r.post true do
authorize("Postgres:edit", pg.id)
required_parameters = ["name"]
request_body_params = validate_request_params(required_parameters, [], [])
st = Prog::Postgres::PostgresResourceNexus.assemble(
project_id: @project.id,
location_id: pg.location_id,
name: request_body_params["name"],
target_vm_size: pg.target_vm_size,
target_storage_size_gib: pg.target_storage_size_gib,
ha_type: PostgresResource::HaType::NONE,
version: pg.version,
flavor: pg.flavor,
parent_id: pg.id,
restore_target: nil
)
send_notification_mail_to_partners(st.subject, current_account.email)
if api?
Serializers::Postgres.serialize(st.subject, {detailed: true})
else
flash["notice"] = "'#{request_body_params["name"]}' will be ready in a few minutes"
r.redirect "#{@project.path}#{st.subject.path}"
end
end
end
r.post "promote" do
authorize("Postgres:edit", pg.id)
unless pg.read_replica?
error_msg = "Non read replica servers cannot be promoted."
if api?
fail CloverError.new(400, "InvalidRequest", error_msg)
else
flash["error"] = error_msg
redirect_back_with_inputs
end
end
pg.incr_promote
if api?
Serializers::Postgres.serialize(pg)
else
flash["notice"] = "'#{pg.name}' will be promoted in a few minutes, please refresh the page"
r.redirect "#{@project.path}#{pg.path}"
end
end
r.post "restore" do
authorize("Postgres:create", @project.id)
authorize("Postgres:view", pg.id)
required_parameters = ["name", "restore_target"]
request_body_params = validate_request_params(required_parameters)
st = Prog::Postgres::PostgresResourceNexus.assemble(
project_id: @project.id,
location_id: pg.location_id,
name: request_body_params["name"],
target_vm_size: pg.target_vm_size,
target_storage_size_gib: pg.target_storage_size_gib,
version: pg.version,
flavor: pg.flavor,
parent_id: pg.id,
restore_target: request_body_params["restore_target"]
)
send_notification_mail_to_partners(st.subject, current_account.email)
if api?
Serializers::Postgres.serialize(st.subject, {detailed: true})
else
flash["notice"] = "'#{request_body_params["name"]}' will be ready in a few minutes"
r.redirect "#{@project.path}#{st.subject.path}"
end
end
r.post "reset-superuser-password" do
authorize("Postgres:view", pg.id)
unless pg.representative_server.primary?
if api?
fail CloverError.new(400, "InvalidRequest", "Superuser password cannot be updated during restore!")
else
flash["error"] = "Superuser password cannot be updated during restore!"
redirect_back_with_inputs
end
end
required_parameters = api? ? ["password"] : ["password", "repeat_password"]
request_body_params = validate_request_params(required_parameters)
Validation.validate_postgres_superuser_password(request_body_params["password"], request_body_params["repeat_password"])
DB.transaction do
pg.update(superuser_password: request_body_params["password"])
pg.representative_server.incr_update_superuser_password
end
if api?
Serializers::Postgres.serialize(pg, {detailed: true})
else
flash["notice"] = "The superuser password will be updated in a few seconds"
r.redirect "#{@project.path}#{pg.path}"
end
end
r.post "set-maintenance-window" do
authorize("Postgres:edit", pg.id)
allowed_optional_parameters = ["maintenance_window_start_at"]
request_body_params = validate_request_params([], allowed_optional_parameters)
pg.update(maintenance_window_start_at: request_body_params["maintenance_window_start_at"])
if api?
Serializers::Postgres.serialize(pg, {detailed: true})
else
flash["notice"] = "Maintenance window is set"
r.redirect "#{@project.path}#{pg.path}"
end
end
r.get "ca-certificates" do
authorize("Postgres:view", pg.id)
return 404 unless (certs = pg.ca_certificates)
response.headers["Content-Disposition"] = "attachment; filename=\"#{pg.name}.pem\""
response.headers["Content-Type"] = "application/x-pem-file"
certs
end
end
end
end