This is an automated email from the ASF dual-hosted git repository.
sciabarracom pushed a commit to branch 0.9.0
in repository https://gitbox.apache.org/repos/asf/openserverless-operator.git
The following commit(s) were added to refs/heads/0.9.0 by this push:
new bd24b2b Fix FerretDB tenant provisioning on 0.9.0 (#142)
bd24b2b is described below
commit bd24b2bb6f593a95fa71aebfb70dc8387e7e8959
Author: miki3421 <[email protected]>
AuthorDate: Thu Oct 22 10:43:51 2026 +0200
Fix FerretDB tenant provisioning on 0.9.0 (#142)
* Fix FerretDB provisioning with valid namespace prefixes and primary
selection
* Verify FerretDB login grants while preserving existing database ownership
* Restrict baseline CI exceptions to unchanged integration test scripts
* Validate FerretDB namespaces before PostgreSQL rendering
* Keep FerretDB regressions compatible with protected SQL files
---------
Co-authored-by: Codex <[email protected]>
---
.github/workflows/check.yml | 24 ++++++---
openserverless/ferretdb.py | 34 ++++++++----
tests/kind/ferretdb_test.ipy | 16 ++++++
tests/test_ferretdb_provisioning.py | 105 ++++++++++++++++++++++++++++++++++++
4 files changed, 160 insertions(+), 19 deletions(-)
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 7f1d3c0..515e3c6 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -64,7 +64,7 @@ jobs:
run: bash .github/cisetup.sh
- name: Install
run: task setup
- - name: Security regression tests
+ - name: Regression tests
run: poetry run python3 -m unittest discover -s tests -p 'test_*.py' -v
- name: BuildAndLoad
run: task build-and-load
@@ -85,20 +85,28 @@ jobs:
continue-on-error: false
- name: Reject new integration failures
run: |
- # The legacy runner writes failures without returning nonzero.
- # These two tests also fail on unmodified 0.9.0 (run 38893750250).
+ # The legacy runner does not propagate failure. Only the exact
original
+ # 0.9.0 test scripts (3ee0a79; reproduced in run 38893750250) may
retain
+ # their known warning. An updated test must pass, so both exceptions
+ # automatically disappear once the two independent fixes are merged.
unexpected_failures=0
if [ -f _failed.txt ]; then
while IFS= read -r failed_test; do
case "$failed_test" in
-
tests/kind/ferretdb_test.ipy|tests/kind/runtimes_preloader_test.ipy)
- echo "::warning::Pre-existing 0.9.0 integration failure:
$failed_test"
+ tests/kind/ferretdb_test.ipy)
+ baseline_blob=90e70ffe1ae1b126e5bd651f32a6a6240b4def07
;;
- *)
- echo "::error::Integration regression: $failed_test"
- unexpected_failures=1
+ tests/kind/runtimes_preloader_test.ipy)
+ baseline_blob=afbee5e8f49945f4737c507de36c731e1da6faee
;;
+ *) baseline_blob= ;;
esac
+ if [ -n "$baseline_blob" ] && [ "$(git hash-object --
"$failed_test")" = "$baseline_blob" ]; then
+ echo "::warning::Pre-existing 0.9.0 integration failure in
unchanged baseline test: $failed_test"
+ else
+ echo "::error::Integration regression: $failed_test"
+ unexpected_failures=1
+ fi
done < _failed.txt
fi
exit "$unexpected_failures"
diff --git a/openserverless/ferretdb.py b/openserverless/ferretdb.py
index 6d911e6..df4f87b 100644
--- a/openserverless/ferretdb.py
+++ b/openserverless/ferretdb.py
@@ -160,10 +160,11 @@ def _add_mdb_user_metadata(user_metadata, data):
mdb_url =
f"mongodb://{auth}@{mdb_pod_name}.{mdb_service_name}.{mdb_ns}.svc.cluster.local:27017/{database}?connectTimeoutMS=60000&authMechanism=PLAIN"
user_metadata.add_metadata("MONGODB_URL",mdb_url)
- return None
+ return True
+ return False
except Exception as e:
logging.error(f"failed to build mongodb_url for {data['database']}:
{e}")
- return None
+ return False
def create_db_user(ucfg: UserConfig, user_metadata: UserMetadata):
database = ucfg.get('mongodb.database')
@@ -178,16 +179,23 @@ def create_db_user(ucfg: UserConfig, user_metadata:
UserMetadata):
data["password"]=ucfg.get('mongodb.password')
data["mode"]="create"
- path_to_pgpass =
postgres.render_postgres_script(f"{namespace}_ferretdb","pgpass_tpl.properties",data)
- path_to_mdb_script =
postgres.render_postgres_script(f"{namespace}_ferretdb","postgres_manage_user_tpl.sql",data)
- pod_name = util.get_pod_name("{.items[?(@.metadata.labels.app ==
'openserverless-postgres')].metadata.name}")
+ if not util.validate_namespace(namespace):
+ raise ValueError("Invalid FerretDB namespace")
+ if not util.validate_database_name(data["database"]) or not
util.validate_database_name(data["username"]):
+ raise ValueError("Invalid FerretDB database or username")
+ # The renderer needs the Kubernetes namespace as a safe file prefix;
+ # the PostgreSQL names retain their existing _ferretdb suffix.
+ path_to_pgpass =
postgres.render_postgres_script(namespace,"pgpass_tpl.properties",data)
+ path_to_mdb_script =
postgres.render_postgres_script(namespace,"postgres_manage_user_tpl.sql",data)
+ pod_name =
util.get_pod_name_by_selector("app=openserverless-postgres","{.items[?(@.metadata.labels.replicationRole
== 'primary')].metadata.name}")
if(pod_name):
res =
postgres.exec_psql_command(pod_name,path_to_mdb_script,path_to_pgpass)
if res:
- _add_mdb_user_metadata(user_metadata, data)
- return res
+ if _add_mdb_user_metadata(user_metadata, data):
+ return res
+ logging.error("FerretDB user created but connection metadata
is unavailable")
else:
logging.error("failed to execute SQL script on Postgres DB to
enable MongoDB (FerretDB) user")
@@ -205,9 +213,13 @@ def delete_db_user(namespace, database):
data["username"]=f"{namespace}_ferretdb"
data["mode"]="delete"
- path_to_pgpass =
postgres.render_postgres_script(f"{namespace}_ferretdb","pgpass_tpl.properties",data)
- path_to_mdb_script =
postgres.render_postgres_script(f"{namespace}_ferretdb","postgres_manage_user_tpl.sql",data)
- pod_name = util.get_pod_name("{.items[?(@.metadata.labels.app ==
'openserverless-postgres')].metadata.name}")
+ if not util.validate_namespace(namespace):
+ raise ValueError("Invalid FerretDB namespace")
+ if not util.validate_database_name(data["database"]) or not
util.validate_database_name(data["username"]):
+ raise ValueError("Invalid FerretDB database or username")
+ path_to_pgpass =
postgres.render_postgres_script(namespace,"pgpass_tpl.properties",data)
+ path_to_mdb_script =
postgres.render_postgres_script(namespace,"postgres_manage_user_tpl.sql",data)
+ pod_name =
util.get_pod_name_by_selector("app=openserverless-postgres","{.items[?(@.metadata.labels.replicationRole
== 'primary')].metadata.name}")
if(pod_name):
res =
postgres.exec_psql_command(pod_name,path_to_mdb_script,path_to_pgpass)
@@ -216,4 +228,4 @@ def delete_db_user(namespace, database):
return None
except Exception as e:
logging.error(f"failed to remove Ferretdb database {namespace}
authorization id and key: {e}")
- return None
\ No newline at end of file
+ return None
diff --git a/tests/kind/ferretdb_test.ipy b/tests/kind/ferretdb_test.ipy
index 90e70ff..6b34d4a 100644
--- a/tests/kind/ferretdb_test.ipy
+++ b/tests/kind/ferretdb_test.ipy
@@ -21,6 +21,7 @@
import openserverless.config as cfg
import openserverless.ferretdb as mdb
import openserverless.kube as kube
+import openserverless.util as util
import openserverless.postgres_operator as postgres
import openserverless.testutil as tu
from openserverless.user_config import UserConfig
@@ -46,7 +47,22 @@ udata = UserMetadata(ucfg)
assert(mdb.create_db_user(ucfg, udata))
assert(any(e['key'] == "MONGODB_URL" for e in
udata.get_metadata()['metadata']))
+
+# Prove provisioning created the intended database and login grants, rather
than merely
+# returning psql output. Keep credentials inside the database pod environment.
+primary = util.get_pod_name_by_selector("app=openserverless-postgres",
"{.items[?(@.metadata.labels.replicationRole == 'primary')].metadata.name}")
+assert(primary)
+
+def scalar(query):
+ return kube.kubectl("exec", primary, "--", "sh", "-c",
+ 'PGPASSWORD="$POSTGRES_PASSWORD" exec psql --no-psqlrc
--username=postgres --dbname=postgres --tuples-only --no-align
--set=ON_ERROR_STOP=1 --command "$1"',
+ "ferretdb-probe", query).strip()
+
+database = (ucfg.get('mongodb.database') + '_ferretdb').replace("'", "''")
+username = (ucfg.get('namespace') + '_ferretdb').replace("'", "''")
+assert(scalar(f"SELECT count(*) FROM pg_database d, pg_roles r WHERE
d.datname='{database}' AND r.rolname='{username}' AND r.rolcanlogin AND
has_database_privilege(r.oid,d.oid,'CONNECT') AND
has_database_privilege(r.oid,d.oid,'CREATE')") == '1')
assert(mdb.delete_db_user(ucfg.get('namespace'),ucfg.get('mongodb.database')))
+assert(scalar(f"SELECT ((EXISTS(SELECT 1 FROM pg_database WHERE
datname='{database}')) OR (EXISTS(SELECT 1 FROM pg_roles WHERE
rolname='{username}')))::int") == '0')
assert(mdb.delete())
assert(postgres.delete())
diff --git a/tests/test_ferretdb_provisioning.py
b/tests/test_ferretdb_provisioning.py
new file mode 100644
index 0000000..3bda0ee
--- /dev/null
+++ b/tests/test_ferretdb_provisioning.py
@@ -0,0 +1,105 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Exercise FerretDB provisioning with the real namespace validator and
renderer."""
+from contextlib import ExitStack
+from types import SimpleNamespace
+import unittest
+from unittest.mock import Mock, patch
+from urllib.parse import urlsplit, unquote
+
+from openserverless import ferretdb as mongo, postgres_operator as postgres,
util
+
+
+class FerretDBProvisioningTests(unittest.TestCase):
+ def setUp(self):
+ stack=ExitStack();self.addCleanup(stack.close)
+
self.values={'namespace':'fixtureuser','mongodb.database':'fixturedb','mongodb.password':'synthetic'}
+ self.user=SimpleNamespace(get=self.values.get)
+ self.metadata=Mock()
+
stack.enter_context(patch.object(util,'get_postgres_config_data',return_value={}))
+
self.primary=stack.enter_context(patch.object(util,'get_pod_name_by_selector',return_value='postgres-primary'))
+
stack.enter_context(patch.object(util,'get_pod_name',side_effect=AssertionError('ambiguous
pod lookup')))
+
self.service=stack.enter_context(patch.object(util,'get_service',return_value={'metadata':{'name':'mongo-svc','namespace':'openserverless'}}))
+
stack.enter_context(patch.object(mongo,'get_ferret_pod_name',return_value='mongo-0'))
+ # Keep the actual renderer and validator; mock only filesystem and
Kubernetes I/O.
+
self.spool=stack.enter_context(patch.object(postgres.ntp,'spool_template',side_effect=lambda
template,path,data:path))
+ self.chmod=stack.enter_context(patch.object(postgres.os,'chmod'))
+
self.render=stack.enter_context(patch.object(postgres,'render_postgres_script',wraps=postgres.render_postgres_script))
+
self.sql=stack.enter_context(patch.object(postgres,'exec_psql_command',return_value='SQL
completed'))
+
+ def assert_primary_and_names(self,mode):
+
self.primary.assert_called_once_with('app=openserverless-postgres',"{.items[?(@.metadata.labels.replicationRole
== 'primary')].metadata.name}")
+ self.sql.assert_called_once()
+ self.assertEqual(self.sql.call_args.args[0],'postgres-primary')
+ self.assertEqual(len(self.render.call_args_list),2)
+ for call in self.render.call_args_list:
+ namespace,template,data=call.args
+ self.assertEqual(namespace,'fixtureuser')
+ self.assertEqual(data['database'],'fixturedb_ferretdb')
+ self.assertEqual(data['username'],'fixtureuser_ferretdb')
+ self.assertEqual(data['mode'],mode)
+
+ def test_create_keeps_sql_suffix_and_uses_real_kubernetes_namespace(self):
+ self.assertTrue(mongo.create_db_user(self.user,self.metadata))
+ self.assert_primary_and_names('create')
+ name,uri=self.metadata.add_metadata.call_args.args
+ self.assertEqual(name,'MONGODB_URL')
+
self.assertEqual(unquote(urlsplit(uri).username),'fixtureuser_ferretdb')
+ self.assertEqual(urlsplit(uri).path,'/fixturedb_ferretdb')
+
+ def test_delete_uses_primary_and_the_same_existing_sql_names(self):
+ self.assertTrue(mongo.delete_db_user('fixtureuser','fixturedb'))
+ self.assert_primary_and_names('delete')
+
+ def test_invalid_database_is_rejected_before_rendering_or_sql(self):
+ self.values['mongodb.database']='bad;name'
+ self.assertFalse(mongo.create_db_user(self.user,self.metadata))
+ self.render.assert_not_called();self.sql.assert_not_called()
+
+ def test_invalid_delete_database_is_rejected_before_rendering_or_sql(self):
+ self.assertFalse(mongo.delete_db_user('fixtureuser','bad;name'))
+ self.render.assert_not_called();self.sql.assert_not_called()
+
+ def test_real_namespace_validation_remains_strict(self):
+ self.assertFalse(util.validate_namespace('fixtureuser_ferretdb'))
+ self.values['namespace']='fixture_user'
+ self.assertFalse(mongo.create_db_user(self.user,self.metadata))
+ self.render.assert_not_called()
+ self.sql.assert_not_called()
+
+ def
test_invalid_delete_namespace_is_rejected_before_rendering_or_sql(self):
+ self.assertFalse(mongo.delete_db_user('fixture_user','fixturedb'))
+ self.render.assert_not_called();self.sql.assert_not_called()
+
+ def test_failed_sql_does_not_publish_connection_metadata(self):
+ self.sql.return_value=''
+ self.assertFalse(mongo.create_db_user(self.user,self.metadata))
+ self.metadata.add_metadata.assert_not_called()
+
+ def test_missing_service_does_not_report_provisioning_success(self):
+ self.service.return_value=None
+ self.assertFalse(mongo.create_db_user(self.user,self.metadata))
+ self.metadata.add_metadata.assert_not_called()
+
+ def test_metadata_publication_failure_does_not_report_success(self):
+ self.metadata.add_metadata.side_effect=RuntimeError('synthetic
metadata failure')
+ self.assertFalse(mongo.create_db_user(self.user,self.metadata))
+
+
+if __name__=='__main__':unittest.main()