From 0529918df7f55b18d49602b5593f41f7cb7b0acb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:32:53 +0000 Subject: [PATCH 1/3] chore(release): 26.9.3 --- CITATION.cff | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 973d32f..0e7c9db 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -24,5 +24,5 @@ keywords: - management - integration license: Apache-2.0 -version: 26.09.2 -date-released: "2026-09-14" +version: 26.9.3 +date-released: "2026-09-15" diff --git a/pyproject.toml b/pyproject.toml index d0fea3d..129faf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "portabase-cli" -version = "26.09.2" +version = "26.9.3" description = "The official command line interface (CLI) for managing and deploying Portabase instances with ease." readme = "README.md" requires-python = ">=3.12" From 31251e48e8aedba2d8afb6540814ef7270174c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20LAGACHE?= Date: Fri, 18 Sep 2026 12:44:41 +0200 Subject: [PATCH 2/3] add: options --- engines/mongodb.py | 34 ++++++++++++++++++++++++++++++++++ tests/engines/mongodb.py | 25 ++++++++++++++++++++++++- uv.lock | 2 +- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/engines/mongodb.py b/engines/mongodb.py index 09232ba..94b5fb9 100644 --- a/engines/mongodb.py +++ b/engines/mongodb.py @@ -3,6 +3,7 @@ import secrets from typing import Any +from core.fields import Field from core.specs import DatabaseSpec from core.utils import generate_password from engines.base import DbEngine @@ -14,6 +15,38 @@ class MongoEngine(DbEngine): template = "engines/mongodb.yml.j2" auth_variants = True + def option_fields(self) -> list[Field]: + return [ + Field( + "auth_source", + "Auth source", + "text", + default="", + help=( + "Authentication database, set as authSource on the URI. Leave " + "empty to use admin when credentials are provided. Override if " + "your user is defined in another database." + ), + ), + Field( + "replica_set", + "Replica set", + "text", + default="", + help=( + "Replica set name, set as replicaSet on the URI. Required to " + "connect to a self-hosted replica set." + ), + ), + Field( + "tls", + "Force TLS?", + "bool", + default=False, + help="When enabled, adds tls=true to the URI to force a TLS connection.", + ), + ] + def generate( self, *, auth: bool, ports: PortAllocator, answers: dict[str, Any] ) -> DatabaseSpec: @@ -29,6 +62,7 @@ def generate( database=db_name, username="admin" if auth else "", password=generate_password(16) if auth else None, + options=dict(answers.get("options", {})), ) def env_vars(self, spec: DatabaseSpec) -> dict[str, str]: diff --git a/tests/engines/mongodb.py b/tests/engines/mongodb.py index dce63a4..310018e 100644 --- a/tests/engines/mongodb.py +++ b/tests/engines/mongodb.py @@ -79,7 +79,11 @@ def fields(): ("password", "secret", None), ] assert MONGO.fields_new() == [] - assert MONGO.option_fields() == [] + assert field_specs(MONGO.option_fields()) == [ + ("auth_source", "text", ""), + ("replica_set", "text", ""), + ("tls", "bool", False), + ] def from_existing(): @@ -130,3 +134,22 @@ def compose_service_inline(render_engine): f"MONGO_INITDB_ROOT_PASSWORD={rendered.spec.password}", f"MONGO_INITDB_DATABASE={rendered.spec.database}", ] + + +def generate_keeps_options(ports): + options = {"replica_set": "rs0", "tls": True} + spec = MONGO.generate(auth=True, ports=ports, answers={"options": options}) + assert spec.options == options + + +def only_non_default_options_reach_the_agent(ports): + options = {"auth_source": "", "replica_set": "rs0", "tls": False, "unknown": 1} + spec = MONGO.generate(auth=True, ports=ports, answers={"options": options}) + assert MONGO.non_default_options(spec) == {"replica_set": "rs0"} + assert MONGO.agent_entry(spec)["options"] == {"replica_set": "rs0"} + tls_only = spec.with_options({"tls": True}) + assert MONGO.agent_entry(tls_only)["options"] == {"tls": True} + custom_auth = spec.with_options({"auth_source": "users"}) + assert MONGO.agent_entry(custom_auth)["options"] == {"auth_source": "users"} + defaults = spec.with_options({"auth_source": "", "replica_set": "", "tls": False}) + assert "options" not in MONGO.agent_entry(defaults) diff --git a/uv.lock b/uv.lock index 357a540..69cc7c8 100644 --- a/uv.lock +++ b/uv.lock @@ -521,7 +521,7 @@ wheels = [ [[package]] name = "portabase-cli" -version = "26.8.12" +version = "26.9.3" source = { virtual = "." } dependencies = [ { name = "jinja2" }, From 88e2e2de9925f4b4c1721112a4b8a430d7dc0c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20LAGACHE?= Date: Thu, 24 Sep 2026 12:11:14 +0200 Subject: [PATCH 3/3] fix: conflict --- engines/mongodb.py | 19 +++++++++++++ tests/engines/mongodb.py | 61 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/engines/mongodb.py b/engines/mongodb.py index af7d765..31a0c02 100644 --- a/engines/mongodb.py +++ b/engines/mongodb.py @@ -3,6 +3,7 @@ import secrets from typing import Any +from core.errors import ValidationError from core.fields import Field from core.specs import DatabaseSpec from core.utils import generate_password @@ -24,6 +25,24 @@ class MongoEngine(DbEngine): template = "engines/mongodb.yml.j2" auth_variants = True + def fields_existing(self) -> list[Field]: + overrides = { + "port": Field( + "port", + "Port", + "int", + default=self.default_port, + help=( + "Set the port to 0 for an SRV connection (mongodb+srv://, " + "e.g. MongoDB Atlas); use the cluster hostname as host." + ), + validator=validate_port, + ), + "username": Field("username", "Username", "text", default=""), + "password": Field("password", "Password", "secret", default=""), + } + return [overrides.get(field.name, field) for field in super().fields_existing()] + def option_fields(self) -> list[Field]: return [ Field( diff --git a/tests/engines/mongodb.py b/tests/engines/mongodb.py index 88154cb..891cf06 100644 --- a/tests/engines/mongodb.py +++ b/tests/engines/mongodb.py @@ -154,3 +154,64 @@ def only_non_default_options_reach_the_agent(ports): assert MONGO.agent_entry(custom_auth)["options"] == {"auth_source": "users"} defaults = spec.with_options({"auth_source": "", "replica_set": "", "tls": False}) assert "options" not in MONGO.agent_entry(defaults) + + +def port_field_mentions_srv(): + port = next(field for field in MONGO.fields_existing() if field.name == "port") + assert "mongodb+srv://" in (port.help or "") + + +@pytest.mark.parametrize("port", [0, 27017, 65535]) +def port_validator_accepts(port): + field = next(field for field in MONGO.fields_existing() if field.name == "port") + assert field.validator is not None + assert field.validator(port) == port + + +@pytest.mark.parametrize("port", [-1, 65536]) +def port_validator_rejects(port): + field = next(field for field in MONGO.fields_existing() if field.name == "port") + assert field.validator is not None + with pytest.raises(ValidationError): + field.validator(port) + + +def srv_existing(): + spec = MONGO.from_existing( + {**EXISTING_ANSWERS, "host": "cluster0.abcde.mongodb.net", "port": 0} + ) + assert spec.port == 0 + assert MONGO.is_srv(spec) + assert MONGO.describe(spec) == "mongodb+srv://cluster0.abcde.mongodb.net" + assert MONGO.agent_entry(spec) == { + "name": "External DB", + "database": "app", + "type": "mongodb", + "username": "u", + "password": "p", + "host": "cluster0.abcde.mongodb.net", + "generated_id": spec.id, + } + + +def srv_missing_port(): + spec = DatabaseSpec( + id="x", engine="mongodb", name="Atlas", host="c.mongodb.net", port=None + ) + assert MONGO.is_srv(spec) + assert "port" not in MONGO.agent_entry(spec) + assert MONGO.describe(spec) == "mongodb+srv://c.mongodb.net" + + +def srv_without_auth(): + answers = {"host": "c.mongodb.net", "port": 0, "database": "app"} + spec = MONGO.from_existing({**answers, "username": "", "password": ""}) + entry = MONGO.agent_entry(spec) + assert "port" not in entry + assert (entry["username"], entry["password"]) == ("", "") + + +def non_srv_existing(): + spec = MONGO.from_existing(EXISTING_ANSWERS) + assert not MONGO.is_srv(spec) + assert MONGO.describe(spec) == "db.example:1234"