From 47f9890bae75c63fc4b29c009cf9020ac2bcbafa Mon Sep 17 00:00:00 2001 From: Alexey Kondratov Date: Fri, 20 Jan 2023 15:37:24 +0100 Subject: [PATCH] [compute_ctl] Make role deletion spec processing idempotent (#3380) Previously, we were trying to re-assign owned objects of the already deleted role. This were causing a crash loop in the case when compute was restarted with a spec that includes delta operation for role deletion. To avoid such cases, check that role is still present before calling `reassign_owned_objects`. Resolves neondatabase/cloud#3553 --- compute_tools/src/compute.rs | 3 ++- compute_tools/src/pg_helpers.rs | 4 ++-- compute_tools/src/spec.rs | 14 +++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/compute_tools/src/compute.rs b/compute_tools/src/compute.rs index e229bb1cc2..c8af8822b7 100644 --- a/compute_tools/src/compute.rs +++ b/compute_tools/src/compute.rs @@ -252,7 +252,7 @@ impl ComputeNode { // If connection fails, // it may be the old node with `zenith_admin` superuser. // - // In this case we need to connect with old `zenith_admin`name + // In this case we need to connect with old `zenith_admin` name // and create new user. We cannot simply rename connected user, // but we can create a new one and grant it all privileges. let mut client = match Client::connect(self.connstr.as_str(), NoTls) { @@ -278,6 +278,7 @@ impl ComputeNode { Ok(client) => client, }; + // Proceed with post-startup configuration. Note, that order of operations is important. handle_roles(&self.spec, &mut client)?; handle_databases(&self.spec, &mut client)?; handle_role_deletions(self, &mut client)?; diff --git a/compute_tools/src/pg_helpers.rs b/compute_tools/src/pg_helpers.rs index 921289d7c2..6ab2864721 100644 --- a/compute_tools/src/pg_helpers.rs +++ b/compute_tools/src/pg_helpers.rs @@ -130,8 +130,8 @@ impl Role { /// Serialize a list of role parameters into a Postgres-acceptable /// string of arguments. pub fn to_pg_options(&self) -> String { - // XXX: consider putting LOGIN as a default option somewhere higher, e.g. in Rails. - // For now we do not use generic `options` for roles. Once used, add + // XXX: consider putting LOGIN as a default option somewhere higher, e.g. in control-plane. + // For now, we do not use generic `options` for roles. Once used, add // `self.options.as_pg_options()` somewhere here. let mut params: String = "LOGIN".to_string(); diff --git a/compute_tools/src/spec.rs b/compute_tools/src/spec.rs index 40c8366bf4..97cd623052 100644 --- a/compute_tools/src/spec.rs +++ b/compute_tools/src/spec.rs @@ -213,8 +213,20 @@ pub fn handle_role_deletions(node: &ComputeNode, client: &mut Client) -> Result< if let Some(ops) = &node.spec.delta_operations { // First, reassign all dependent objects to db owners. info!("reassigning dependent objects of to-be-deleted roles"); + + // Fetch existing roles. We could've exported and used `existing_roles` from + // `handle_roles()`, but we only make this list there before creating new roles. + // Which is probably fine as we never create to-be-deleted roles, but that'd + // just look a bit untidy. Anyway, the entire `pg_roles` should be in shared + // buffers already, so this shouldn't be a big deal. + let mut xact = client.transaction()?; + let existing_roles: Vec = get_existing_roles(&mut xact)?; + xact.commit()?; + for op in ops { - if op.action == "delete_role" { + // Check that role is still present in Postgres, as this could be a + // restart with the same spec after role deletion. + if op.action == "delete_role" && existing_roles.iter().any(|r| r.name == op.name) { reassign_owned_objects(node, &op.name)?; } }