pg-stand 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pg_stand-0.2.2/src/pg_stand.egg-info → pg_stand-0.2.4}/PKG-INFO +7 -4
- {pg_stand-0.2.2 → pg_stand-0.2.4}/README.md +6 -3
- {pg_stand-0.2.2 → pg_stand-0.2.4}/pyproject.toml +1 -1
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/__init__.py +1 -1
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/runtime.py +30 -3
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/runtime_observe.py +6 -4
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/runtime_state.py +6 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4/src/pg_stand.egg-info}/PKG-INFO +7 -4
- {pg_stand-0.2.2 → pg_stand-0.2.4}/tests/test_runtime.py +109 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/LICENSE +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/MANIFEST.in +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/setup.cfg +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/.dockerignore +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/__main__.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/assets.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/cli.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/cli_parser.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/config.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/configs/primary-standby-async.yaml +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/configs/primary-standby-sync-logical.yaml +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/configs/primary-standby-sync.yaml +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/configs/single.yaml +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/credentials.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/database_credentials.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/docker/Dockerfile.pg10 +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/docker/Dockerfile.pg11-pg12 +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/docker/Dockerfile.pg13-pg17 +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/docker/Dockerfile.pg18 +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/docker/pg-stand-entrypoint.sh +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/docker/pg-stand-pgbouncer.sh +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/errors.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/orchestration.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/runtime_common.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/runtime_docker.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/runtime_nodes.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/runtime_replication.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand/schema/pg_stand-v1.schema.json +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand.egg-info/SOURCES.txt +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand.egg-info/dependency_links.txt +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand.egg-info/entry_points.txt +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand.egg-info/requires.txt +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/src/pg_stand.egg-info/top_level.txt +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/tests/integration_tls.sh +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/tests/test_cli.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/tests/test_config.py +0 -0
- {pg_stand-0.2.2 → pg_stand-0.2.4}/tests/test_packaging.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pg-stand
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Declarative Docker testbed manager for PostgreSQL
|
|
5
5
|
Author-email: O2eg <oleg.ispu@yandex.ru>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -259,9 +259,10 @@ avoided; the runtime selects a compatible image family automatically.
|
|
|
259
259
|
Every managed image installs PgBouncer 1.25.2, `iproute2`, `lshw`, OpenSSH server, `procps`,
|
|
260
260
|
`sysstat`, `util-linux`, plus the `pg_wait_sampling` and `pg_stat_kcache` packages. Every node
|
|
261
261
|
runs independent PgBouncer processes for session pooling on container port 6432 and transaction
|
|
262
|
-
pooling on container port 6433. Every
|
|
263
|
-
|
|
264
|
-
`pg_wait_sampling`, `pg_buffercache`, and `pg_stat_kcache`.
|
|
262
|
+
pooling on container port 6433. Every connectable database, including `app_01` through
|
|
263
|
+
`app_10`, `postgres`, and `template1`, automatically gets `pg_stat_statements`,
|
|
264
|
+
`pg_wait_sampling`, `pg_buffercache`, and `pg_stat_kcache`. New databases created from
|
|
265
|
+
`template1` inherit these extensions; `template0` remains pristine. The `auto_explain` module is also
|
|
265
266
|
loaded through
|
|
266
267
|
`shared_preload_libraries`. Planning tracking for `pg_stat_statements` and `pg_stat_kcache` is
|
|
267
268
|
enabled only on PostgreSQL 13 and newer.
|
|
@@ -503,6 +504,8 @@ stand is stopped or removed. `stop` and `down` release the lock. A failed partia
|
|
|
503
504
|
the lock when it has already started a database node, so another stand cannot start around the
|
|
504
505
|
failure. Destructive storage cleanup retains the lock until mounted data and credentials are no
|
|
505
506
|
longer being changed, which prevents a concurrent `up` from racing with deletion.
|
|
507
|
+
`plan` reports `blocked` when the active lease belongs to another stand or project
|
|
508
|
+
directory, before suggesting `up` or an apply operation.
|
|
506
509
|
|
|
507
510
|
Persistent container and network names must end with `-pg-stand-managed`. Every resource also
|
|
508
511
|
has mandatory project, project-directory instance, managed, resource-kind, node, and
|
|
@@ -219,9 +219,10 @@ avoided; the runtime selects a compatible image family automatically.
|
|
|
219
219
|
Every managed image installs PgBouncer 1.25.2, `iproute2`, `lshw`, OpenSSH server, `procps`,
|
|
220
220
|
`sysstat`, `util-linux`, plus the `pg_wait_sampling` and `pg_stat_kcache` packages. Every node
|
|
221
221
|
runs independent PgBouncer processes for session pooling on container port 6432 and transaction
|
|
222
|
-
pooling on container port 6433. Every
|
|
223
|
-
|
|
224
|
-
`pg_wait_sampling`, `pg_buffercache`, and `pg_stat_kcache`.
|
|
222
|
+
pooling on container port 6433. Every connectable database, including `app_01` through
|
|
223
|
+
`app_10`, `postgres`, and `template1`, automatically gets `pg_stat_statements`,
|
|
224
|
+
`pg_wait_sampling`, `pg_buffercache`, and `pg_stat_kcache`. New databases created from
|
|
225
|
+
`template1` inherit these extensions; `template0` remains pristine. The `auto_explain` module is also
|
|
225
226
|
loaded through
|
|
226
227
|
`shared_preload_libraries`. Planning tracking for `pg_stat_statements` and `pg_stat_kcache` is
|
|
227
228
|
enabled only on PostgreSQL 13 and newer.
|
|
@@ -463,6 +464,8 @@ stand is stopped or removed. `stop` and `down` release the lock. A failed partia
|
|
|
463
464
|
the lock when it has already started a database node, so another stand cannot start around the
|
|
464
465
|
failure. Destructive storage cleanup retains the lock until mounted data and credentials are no
|
|
465
466
|
longer being changed, which prevents a concurrent `up` from racing with deletion.
|
|
467
|
+
`plan` reports `blocked` when the active lease belongs to another stand or project
|
|
468
|
+
directory, before suggesting `up` or an apply operation.
|
|
466
469
|
|
|
467
470
|
Persistent container and network names must end with `-pg-stand-managed`. Every resource also
|
|
468
471
|
has mandatory project, project-directory instance, managed, resource-kind, node, and
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import shlex
|
|
6
7
|
import time
|
|
7
8
|
from collections.abc import Callable
|
|
@@ -15,7 +16,6 @@ from pg_stand.config import MANAGED_RESOURCE_SUFFIX, NodeConfig, StandConfig
|
|
|
15
16
|
from pg_stand.credentials import credential_paths, credentials_status, ensure_credentials
|
|
16
17
|
from pg_stand.database_credentials import (
|
|
17
18
|
APPLICATION_ACCOUNTS,
|
|
18
|
-
APPLICATION_DATABASES,
|
|
19
19
|
DEFAULT_REPLICATION_USER,
|
|
20
20
|
DatabaseCredentials,
|
|
21
21
|
read_database_credentials,
|
|
@@ -393,6 +393,11 @@ class _StandManagerCore:
|
|
|
393
393
|
nano_cpus=int(node.cpu_limit * 1_000_000_000),
|
|
394
394
|
mem_limit=node.memory_limit,
|
|
395
395
|
restart_policy={"Name": "unless-stopped"},
|
|
396
|
+
# Without an init process the postmaster is PID 1 and adopts orphaned
|
|
397
|
+
# children (docker exec/ssh pipelines). PostgreSQL 18 treats an unknown
|
|
398
|
+
# child exiting with a non-zero status as a backend crash and restarts
|
|
399
|
+
# the whole cluster (observed 2026-09-11 on pg-trace: exit code 141).
|
|
400
|
+
init=True,
|
|
396
401
|
)
|
|
397
402
|
except APIError as exc:
|
|
398
403
|
raise DockerRuntimeError(
|
|
@@ -460,8 +465,16 @@ class _StandManagerCore:
|
|
|
460
465
|
"/bin/sh",
|
|
461
466
|
"-ceu",
|
|
462
467
|
"""process=$(cat /proc/1/comm)
|
|
468
|
+
if [ "$process" = "docker-init" ]; then
|
|
469
|
+
# Its first child is the entrypoint, which execs the final server. Later
|
|
470
|
+
# children may include the temporary initdb server adopted by docker-init.
|
|
471
|
+
for child in $(cat /proc/1/task/1/children); do
|
|
472
|
+
process=$(cat "/proc/$child/comm")
|
|
473
|
+
break
|
|
474
|
+
done
|
|
475
|
+
fi
|
|
463
476
|
if [ "$process" != "postgres" ]; then
|
|
464
|
-
echo "container
|
|
477
|
+
echo "container main process is $process, waiting for final postgres process"
|
|
465
478
|
exit 1
|
|
466
479
|
fi
|
|
467
480
|
exec pg_isready --username "$1" --dbname postgres
|
|
@@ -563,7 +576,11 @@ exec pg_isready --username "$1" --dbname postgres
|
|
|
563
576
|
f"image {self.config.postgres.image_name} lacks diagnostic extensions: "
|
|
564
577
|
+ ", ".join(missing)
|
|
565
578
|
)
|
|
566
|
-
|
|
579
|
+
# Include postgres and template1, so databases created later inherit the
|
|
580
|
+
# same diagnostic support. Keep template0 pristine and non-connectable
|
|
581
|
+
# databases untouched.
|
|
582
|
+
databases = self._diagnostic_databases(container)
|
|
583
|
+
for database in databases:
|
|
567
584
|
for extension in DIAGNOSTIC_EXTENSIONS:
|
|
568
585
|
self._psql(
|
|
569
586
|
container,
|
|
@@ -582,6 +599,16 @@ exec pg_isready --username "$1" --dbname postgres
|
|
|
582
599
|
"unexpected shared_preload_libraries order: " + ",".join(actual_preloads)
|
|
583
600
|
)
|
|
584
601
|
|
|
602
|
+
def _diagnostic_databases(self, container: Any) -> list[str]:
|
|
603
|
+
value = self._psql(
|
|
604
|
+
container,
|
|
605
|
+
"select coalesce(json_agg(datname order by datname), '[]'::json)::text "
|
|
606
|
+
"from pg_catalog.pg_database where datallowconn and datname <> 'template0'",
|
|
607
|
+
database=self.config.postgres.database,
|
|
608
|
+
synchronous_commit_off=True,
|
|
609
|
+
)
|
|
610
|
+
return json.loads(value)
|
|
611
|
+
|
|
585
612
|
def _ensure_superuser_password(self, container: Any) -> None:
|
|
586
613
|
self._psql(
|
|
587
614
|
container,
|
|
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
|
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
10
|
from pg_stand.config import NodeConfig
|
|
11
|
-
from pg_stand.database_credentials import APPLICATION_ACCOUNTS
|
|
11
|
+
from pg_stand.database_credentials import APPLICATION_ACCOUNTS
|
|
12
12
|
from pg_stand.errors import DockerRuntimeError
|
|
13
13
|
from pg_stand.runtime_common import (
|
|
14
14
|
CONFIG_HASH_LABEL,
|
|
@@ -412,7 +412,8 @@ class ObservationMixin:
|
|
|
412
412
|
}
|
|
413
413
|
)
|
|
414
414
|
missing_by_database: dict[str, list[str]] = {}
|
|
415
|
-
|
|
415
|
+
diagnostic_databases = self._diagnostic_databases(container)
|
|
416
|
+
for database in diagnostic_databases:
|
|
416
417
|
extension_output = self._psql(
|
|
417
418
|
container,
|
|
418
419
|
"select extname from pg_catalog.pg_extension where extname = any (array["
|
|
@@ -428,9 +429,10 @@ class ObservationMixin:
|
|
|
428
429
|
{
|
|
429
430
|
"id": f"node.{node_name}.extensions",
|
|
430
431
|
"status": "healthy" if not missing_by_database else "degraded",
|
|
431
|
-
"summary": "
|
|
432
|
+
"summary": "diagnostic extensions are installed in all connectable databases"
|
|
432
433
|
if not missing_by_database
|
|
433
|
-
else "diagnostic extensions are missing from
|
|
434
|
+
else "diagnostic extensions are missing from connectable databases",
|
|
435
|
+
"databases": diagnostic_databases,
|
|
434
436
|
"missing_by_database": missing_by_database,
|
|
435
437
|
}
|
|
436
438
|
)
|
|
@@ -182,6 +182,12 @@ class StateStorageMixin:
|
|
|
182
182
|
"desired_state_hash": self.config.config_hash,
|
|
183
183
|
"applied_state_hash": state.get("config_hash") if state else None,
|
|
184
184
|
}
|
|
185
|
+
if self._find_active_lock() is not None:
|
|
186
|
+
try:
|
|
187
|
+
self._assert_current_stand_is_active()
|
|
188
|
+
except DockerRuntimeError as exc:
|
|
189
|
+
result.update(required_action="blocked", can_apply=False, reason=str(exc))
|
|
190
|
+
return self._finalize_plan(result)
|
|
185
191
|
if state is None:
|
|
186
192
|
result.update(
|
|
187
193
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pg-stand
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Declarative Docker testbed manager for PostgreSQL
|
|
5
5
|
Author-email: O2eg <oleg.ispu@yandex.ru>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -259,9 +259,10 @@ avoided; the runtime selects a compatible image family automatically.
|
|
|
259
259
|
Every managed image installs PgBouncer 1.25.2, `iproute2`, `lshw`, OpenSSH server, `procps`,
|
|
260
260
|
`sysstat`, `util-linux`, plus the `pg_wait_sampling` and `pg_stat_kcache` packages. Every node
|
|
261
261
|
runs independent PgBouncer processes for session pooling on container port 6432 and transaction
|
|
262
|
-
pooling on container port 6433. Every
|
|
263
|
-
|
|
264
|
-
`pg_wait_sampling`, `pg_buffercache`, and `pg_stat_kcache`.
|
|
262
|
+
pooling on container port 6433. Every connectable database, including `app_01` through
|
|
263
|
+
`app_10`, `postgres`, and `template1`, automatically gets `pg_stat_statements`,
|
|
264
|
+
`pg_wait_sampling`, `pg_buffercache`, and `pg_stat_kcache`. New databases created from
|
|
265
|
+
`template1` inherit these extensions; `template0` remains pristine. The `auto_explain` module is also
|
|
265
266
|
loaded through
|
|
266
267
|
`shared_preload_libraries`. Planning tracking for `pg_stat_statements` and `pg_stat_kcache` is
|
|
267
268
|
enabled only on PostgreSQL 13 and newer.
|
|
@@ -503,6 +504,8 @@ stand is stopped or removed. `stop` and `down` release the lock. A failed partia
|
|
|
503
504
|
the lock when it has already started a database node, so another stand cannot start around the
|
|
504
505
|
failure. Destructive storage cleanup retains the lock until mounted data and credentials are no
|
|
505
506
|
longer being changed, which prevents a concurrent `up` from racing with deletion.
|
|
507
|
+
`plan` reports `blocked` when the active lease belongs to another stand or project
|
|
508
|
+
directory, before suggesting `up` or an apply operation.
|
|
506
509
|
|
|
507
510
|
Persistent container and network names must end with `-pg-stand-managed`. Every resource also
|
|
508
511
|
has mandatory project, project-directory instance, managed, resource-kind, node, and
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import shutil
|
|
5
|
+
import subprocess
|
|
5
6
|
from dataclasses import replace
|
|
6
7
|
from datetime import datetime, timezone
|
|
7
8
|
from pathlib import Path
|
|
@@ -37,6 +38,8 @@ from pg_stand.runtime import (
|
|
|
37
38
|
standby_postgres_command,
|
|
38
39
|
)
|
|
39
40
|
from pg_stand.runtime_common import (
|
|
41
|
+
DIAGNOSTIC_EXTENSIONS,
|
|
42
|
+
DIAGNOSTIC_PRELOADS,
|
|
40
43
|
IMAGE_BASE_LABEL,
|
|
41
44
|
IMAGE_FAMILY_LABEL,
|
|
42
45
|
IMAGE_MAJOR_LABEL,
|
|
@@ -51,6 +54,37 @@ ROOT = Path(__file__).resolve().parents[1]
|
|
|
51
54
|
ASSET_ROOT = ROOT / "src" / "pg_stand"
|
|
52
55
|
|
|
53
56
|
|
|
57
|
+
def test_diagnostic_extensions_cover_existing_and_future_databases(tmp_path: Path) -> None:
|
|
58
|
+
config = load_config(ASSET_ROOT / "configs" / "single.yaml", project_directory=tmp_path)
|
|
59
|
+
manager = StandManager(config, client=object())
|
|
60
|
+
container = object()
|
|
61
|
+
databases = ["app_01", "custom\ndb", "postgres", "template1"]
|
|
62
|
+
|
|
63
|
+
def psql(_container, sql, **kwargs):
|
|
64
|
+
if "pg_available_extensions" in sql:
|
|
65
|
+
return "\n".join(DIAGNOSTIC_EXTENSIONS)
|
|
66
|
+
if "pg_catalog.pg_database" in sql:
|
|
67
|
+
assert "datallowconn" in sql and "datname <> 'template0'" in sql
|
|
68
|
+
return json.dumps(databases)
|
|
69
|
+
if sql == "show shared_preload_libraries":
|
|
70
|
+
return ",".join(DIAGNOSTIC_PRELOADS)
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
manager._exec = MagicMock()
|
|
74
|
+
manager._psql = MagicMock(side_effect=psql)
|
|
75
|
+
manager._ensure_diagnostic_extensions(container)
|
|
76
|
+
installed = {
|
|
77
|
+
(call.kwargs["database"], call.args[1])
|
|
78
|
+
for call in manager._psql.call_args_list
|
|
79
|
+
if call.args[1].startswith("create extension")
|
|
80
|
+
}
|
|
81
|
+
assert installed == {
|
|
82
|
+
(database, f'create extension if not exists "{extension}"')
|
|
83
|
+
for database in databases
|
|
84
|
+
for extension in DIAGNOSTIC_EXTENSIONS
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
54
88
|
@pytest.fixture(scope="session")
|
|
55
89
|
def credential_template(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
|
56
90
|
project = tmp_path_factory.mktemp("credential-template")
|
|
@@ -277,6 +311,47 @@ def test_readiness_waits_for_final_postgres_pid_one() -> None:
|
|
|
277
311
|
assert "/proc/1/comm" in command[2]
|
|
278
312
|
|
|
279
313
|
|
|
314
|
+
@pytest.mark.parametrize(
|
|
315
|
+
("pid_one", "children", "child_processes", "ready"),
|
|
316
|
+
[
|
|
317
|
+
("postgres", "", {}, True),
|
|
318
|
+
("docker-init", "7 ", {"7": "postgres"}, True),
|
|
319
|
+
("docker-init", "7 84 ", {"7": "bash", "84": "postgres"}, False),
|
|
320
|
+
("docker-init", "", {}, False),
|
|
321
|
+
("bash", "84 ", {"84": "postgres"}, False),
|
|
322
|
+
],
|
|
323
|
+
)
|
|
324
|
+
def test_readiness_checks_final_server_behind_docker_init(
|
|
325
|
+
tmp_path: Path, pid_one: str, children: str, child_processes: dict[str, str], ready: bool
|
|
326
|
+
) -> None:
|
|
327
|
+
config = load_config(ASSET_ROOT / "configs" / "single.yaml", environment={})
|
|
328
|
+
manager = StandManager(config, client=object())
|
|
329
|
+
container = MagicMock(status="running")
|
|
330
|
+
container.exec_run.return_value = SimpleNamespace(exit_code=0, output=b"ready")
|
|
331
|
+
manager._wait_for_postgres(container, timeout_seconds=5)
|
|
332
|
+
command = container.exec_run.call_args.args[0]
|
|
333
|
+
|
|
334
|
+
proc = tmp_path / "proc"
|
|
335
|
+
(proc / "1/task/1").mkdir(parents=True)
|
|
336
|
+
(proc / "1/comm").write_text(pid_one + "\n")
|
|
337
|
+
(proc / "1/task/1/children").write_text(children)
|
|
338
|
+
for pid, process in child_processes.items():
|
|
339
|
+
(proc / pid).mkdir()
|
|
340
|
+
(proc / pid / "comm").write_text(process + "\n")
|
|
341
|
+
probe = tmp_path / "pg_isready"
|
|
342
|
+
probe.write_text('#!/bin/sh\nprintf "%s\\n" "$@"\n')
|
|
343
|
+
probe.chmod(0o755)
|
|
344
|
+
script = command[2].replace("/proc/", "./proc/").replace("exec pg_isready", "exec ./pg_isready")
|
|
345
|
+
result = subprocess.run(
|
|
346
|
+
[*command[:2], script, *command[3:]], cwd=tmp_path, text=True, capture_output=True
|
|
347
|
+
)
|
|
348
|
+
assert (result.returncode == 0) is ready, result.stderr
|
|
349
|
+
if ready:
|
|
350
|
+
assert result.stdout.splitlines() == ["--username", "postgres", "--dbname", "postgres"]
|
|
351
|
+
else:
|
|
352
|
+
assert "waiting for final postgres process" in result.stdout
|
|
353
|
+
|
|
354
|
+
|
|
280
355
|
def test_active_lock_rejects_a_second_project() -> None:
|
|
281
356
|
config = load_config(ASSET_ROOT / "configs" / "single.yaml", environment={})
|
|
282
357
|
client = MagicMock()
|
|
@@ -325,6 +400,40 @@ def test_active_lock_is_created_with_exclusive_managed_identity() -> None:
|
|
|
325
400
|
assert kwargs["labels"][RESOURCE_KIND_LABEL] == "active-lock"
|
|
326
401
|
|
|
327
402
|
|
|
403
|
+
@pytest.mark.parametrize("applied", [False, True])
|
|
404
|
+
def test_plan_reports_foreign_active_stand_before_claiming_it_can_apply(
|
|
405
|
+
tmp_path: Path,
|
|
406
|
+
applied: bool,
|
|
407
|
+
) -> None:
|
|
408
|
+
config = load_config(ASSET_ROOT / "configs" / "single.yaml", project_directory=tmp_path)
|
|
409
|
+
client = MagicMock()
|
|
410
|
+
client.networks.list.return_value = [
|
|
411
|
+
_Resource(
|
|
412
|
+
{
|
|
413
|
+
"Labels": {
|
|
414
|
+
PROJECT_LABEL: "another-stand",
|
|
415
|
+
MANAGED_LABEL: "true",
|
|
416
|
+
RESOURCE_KIND_LABEL: "active-lock",
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
name=ACTIVE_LOCK_NETWORK,
|
|
420
|
+
),
|
|
421
|
+
]
|
|
422
|
+
manager = StandManager(config, client=client)
|
|
423
|
+
if applied:
|
|
424
|
+
manager._record_applied_state()
|
|
425
|
+
blocked = manager.plan()
|
|
426
|
+
assert blocked["required_action"] == "blocked"
|
|
427
|
+
assert blocked["can_apply"] is False
|
|
428
|
+
assert "another-stand" in blocked["reason"]
|
|
429
|
+
client.networks.create.assert_not_called()
|
|
430
|
+
client.containers.run.assert_not_called()
|
|
431
|
+
client.networks.list.return_value = []
|
|
432
|
+
available = manager.plan()
|
|
433
|
+
assert available["required_action"] == ("none" if applied else "up")
|
|
434
|
+
assert available["plan_hash"] != blocked["plan_hash"]
|
|
435
|
+
|
|
436
|
+
|
|
328
437
|
@pytest.mark.parametrize(
|
|
329
438
|
("version", "pgdata", "data_mount"),
|
|
330
439
|
[
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|