pg-stand 0.1.0__py3-none-any.whl
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/.dockerignore +5 -0
- pg_stand/__init__.py +3 -0
- pg_stand/__main__.py +4 -0
- pg_stand/assets.py +108 -0
- pg_stand/cli.py +334 -0
- pg_stand/cli_parser.py +374 -0
- pg_stand/config.py +756 -0
- pg_stand/configs/primary-standby-async.yaml +59 -0
- pg_stand/configs/primary-standby-sync-logical.yaml +74 -0
- pg_stand/configs/primary-standby-sync.yaml +59 -0
- pg_stand/configs/single.yaml +40 -0
- pg_stand/credentials.py +441 -0
- pg_stand/database_credentials.py +169 -0
- pg_stand/docker/Dockerfile.pg10 +54 -0
- pg_stand/docker/Dockerfile.pg11-pg12 +54 -0
- pg_stand/docker/Dockerfile.pg13-pg17 +54 -0
- pg_stand/docker/Dockerfile.pg18 +54 -0
- pg_stand/docker/pg-stand-entrypoint.sh +107 -0
- pg_stand/docker/pg-stand-pgbouncer.sh +170 -0
- pg_stand/errors.py +10 -0
- pg_stand/runtime.py +963 -0
- pg_stand/runtime_common.py +455 -0
- pg_stand/runtime_docker.py +573 -0
- pg_stand/runtime_nodes.py +505 -0
- pg_stand/runtime_observe.py +744 -0
- pg_stand/runtime_replication.py +791 -0
- pg_stand/runtime_state.py +572 -0
- pg_stand/schema/pg_stand-v1.schema.json +203 -0
- pg_stand-0.1.0.dist-info/METADATA +946 -0
- pg_stand-0.1.0.dist-info/RECORD +34 -0
- pg_stand-0.1.0.dist-info/WHEEL +5 -0
- pg_stand-0.1.0.dist-info/entry_points.txt +2 -0
- pg_stand-0.1.0.dist-info/licenses/LICENSE +21 -0
- pg_stand-0.1.0.dist-info/top_level.txt +1 -0
pg_stand/.dockerignore
ADDED
pg_stand/__init__.py
ADDED
pg_stand/__main__.py
ADDED
pg_stand/assets.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Bundled profiles, schema, and managed-image build assets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pg_stand.errors import PgStandError
|
|
10
|
+
|
|
11
|
+
PACKAGE_ROOT = Path(__file__).resolve().parent
|
|
12
|
+
PROFILE_FILES = (
|
|
13
|
+
"single.yaml",
|
|
14
|
+
"primary-standby-async.yaml",
|
|
15
|
+
"primary-standby-sync.yaml",
|
|
16
|
+
"primary-standby-sync-logical.yaml",
|
|
17
|
+
)
|
|
18
|
+
SCHEMA_FILES = ("pg_stand-v1.schema.json",)
|
|
19
|
+
DOCKER_FILES = (
|
|
20
|
+
"Dockerfile.pg10",
|
|
21
|
+
"Dockerfile.pg11-pg12",
|
|
22
|
+
"Dockerfile.pg13-pg17",
|
|
23
|
+
"Dockerfile.pg18",
|
|
24
|
+
"pg-stand-entrypoint.sh",
|
|
25
|
+
"pg-stand-pgbouncer.sh",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _ensure_directory(path: Path, *, label: str) -> None:
|
|
30
|
+
if path.is_symlink():
|
|
31
|
+
raise PgStandError(f"{label} must not be a symbolic link: {path}")
|
|
32
|
+
try:
|
|
33
|
+
path.mkdir(parents=True, exist_ok=True, mode=0o755)
|
|
34
|
+
except OSError as exc:
|
|
35
|
+
raise PgStandError(f"cannot create {label} {path}: {exc}") from exc
|
|
36
|
+
if not path.is_dir():
|
|
37
|
+
raise PgStandError(f"{label} is not a directory: {path}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def docker_build_context() -> Path:
|
|
41
|
+
"""Return the installed package directory used as the managed-image build context."""
|
|
42
|
+
docker_directory = PACKAGE_ROOT / "docker"
|
|
43
|
+
missing = [name for name in DOCKER_FILES if not (docker_directory / name).is_file()]
|
|
44
|
+
dockerignore = PACKAGE_ROOT / ".dockerignore"
|
|
45
|
+
if not dockerignore.is_file():
|
|
46
|
+
missing.append("../.dockerignore")
|
|
47
|
+
if missing:
|
|
48
|
+
raise PgStandError("installed pg-stand package lacks Docker assets: " + ", ".join(missing))
|
|
49
|
+
return PACKAGE_ROOT
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _write_asset(source: Path, target: Path, *, force: bool) -> str:
|
|
53
|
+
payload = source.read_bytes()
|
|
54
|
+
existed = target.exists()
|
|
55
|
+
if target.is_symlink():
|
|
56
|
+
raise PgStandError(f"refusing to replace a symbolic link: {target}")
|
|
57
|
+
if target.exists():
|
|
58
|
+
if not target.is_file():
|
|
59
|
+
raise PgStandError(f"project asset target is not a regular file: {target}")
|
|
60
|
+
if target.read_bytes() == payload:
|
|
61
|
+
return "unchanged"
|
|
62
|
+
if not force:
|
|
63
|
+
raise PgStandError(
|
|
64
|
+
f"project asset already exists with different content: {target}; "
|
|
65
|
+
"use init --force to replace it"
|
|
66
|
+
)
|
|
67
|
+
descriptor, temporary_name = tempfile.mkstemp(dir=target.parent, prefix=f".{target.name}.")
|
|
68
|
+
temporary = Path(temporary_name)
|
|
69
|
+
try:
|
|
70
|
+
os.fchmod(descriptor, 0o644)
|
|
71
|
+
with os.fdopen(descriptor, "wb") as stream:
|
|
72
|
+
descriptor = -1
|
|
73
|
+
stream.write(payload)
|
|
74
|
+
temporary.replace(target)
|
|
75
|
+
except BaseException:
|
|
76
|
+
if descriptor >= 0:
|
|
77
|
+
os.close(descriptor)
|
|
78
|
+
temporary.unlink(missing_ok=True)
|
|
79
|
+
raise
|
|
80
|
+
return "replaced" if existed else "created"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def initialize_project(directory: str | Path, *, force: bool = False) -> dict[str, object]:
|
|
84
|
+
"""Materialize editable example profiles and the public schema in a project directory."""
|
|
85
|
+
project_directory = Path(directory).expanduser().resolve()
|
|
86
|
+
_ensure_directory(project_directory, label="project directory")
|
|
87
|
+
files: list[dict[str, str]] = []
|
|
88
|
+
for subdirectory, names in (("configs", PROFILE_FILES), ("schema", SCHEMA_FILES)):
|
|
89
|
+
target_directory = project_directory / subdirectory
|
|
90
|
+
_ensure_directory(target_directory, label="project asset directory")
|
|
91
|
+
resolved_target_directory = target_directory.resolve()
|
|
92
|
+
if project_directory not in resolved_target_directory.parents:
|
|
93
|
+
raise PgStandError(f"project asset directory escaped the project: {target_directory}")
|
|
94
|
+
for name in names:
|
|
95
|
+
source = PACKAGE_ROOT / subdirectory / name
|
|
96
|
+
if not source.is_file():
|
|
97
|
+
raise PgStandError(f"installed pg-stand package lacks asset: {source}")
|
|
98
|
+
target = target_directory / name
|
|
99
|
+
try:
|
|
100
|
+
state = _write_asset(source, target, force=force)
|
|
101
|
+
except OSError as exc:
|
|
102
|
+
raise PgStandError(f"cannot write project asset {target}: {exc}") from exc
|
|
103
|
+
files.append({"path": str(target), "state": state})
|
|
104
|
+
return {
|
|
105
|
+
"project_directory": str(project_directory),
|
|
106
|
+
"force": force,
|
|
107
|
+
"files": files,
|
|
108
|
+
}
|
pg_stand/cli.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Command-line interface for pg-stand."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
import docker
|
|
17
|
+
from pg_stand.assets import initialize_project
|
|
18
|
+
from pg_stand.cli_parser import _parser
|
|
19
|
+
from pg_stand.config import StandConfig, load_config
|
|
20
|
+
from pg_stand.errors import DockerRuntimeError, PgStandError
|
|
21
|
+
from pg_stand.runtime import StandManager, discover_active_stand
|
|
22
|
+
|
|
23
|
+
_DURATION_RE = re.compile(r"^(?P<value>[0-9]+(?:\.[0-9]+)?)(?P<unit>[smhd])$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _emit(value: Any, *, as_json: bool) -> None:
|
|
27
|
+
if as_json:
|
|
28
|
+
print(json.dumps(value, indent=2, sort_keys=True))
|
|
29
|
+
else:
|
|
30
|
+
print(yaml.safe_dump(value, sort_keys=False, allow_unicode=True).rstrip())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _emit_stream(value: Any, *, as_json: bool) -> None:
|
|
34
|
+
if as_json:
|
|
35
|
+
print(json.dumps(value, sort_keys=True), flush=True)
|
|
36
|
+
else:
|
|
37
|
+
print("---")
|
|
38
|
+
print(yaml.safe_dump(value, sort_keys=False, allow_unicode=True).rstrip(), flush=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _moment(value: str, *, now: datetime | None = None) -> datetime:
|
|
42
|
+
reference = now or datetime.now(timezone.utc)
|
|
43
|
+
match = _DURATION_RE.fullmatch(value.strip().lower())
|
|
44
|
+
if match:
|
|
45
|
+
factors = {"s": 1, "m": 60, "h": 3600, "d": 86400}
|
|
46
|
+
seconds = float(match.group("value")) * factors[match.group("unit")]
|
|
47
|
+
return reference - timedelta(seconds=seconds)
|
|
48
|
+
try:
|
|
49
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
50
|
+
except ValueError as exc:
|
|
51
|
+
raise DockerRuntimeError(
|
|
52
|
+
f"invalid time {value!r}; use an ISO-8601 timestamp or duration such as 30m"
|
|
53
|
+
) from exc
|
|
54
|
+
if parsed.tzinfo is None:
|
|
55
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
56
|
+
return parsed.astimezone(timezone.utc)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _health_options(args: argparse.Namespace) -> dict[str, Any]:
|
|
60
|
+
return {
|
|
61
|
+
"max_physical_lag_bytes": args.max_physical_lag_bytes,
|
|
62
|
+
"max_physical_lag_seconds": args.max_physical_lag_seconds,
|
|
63
|
+
"max_logical_lag_seconds": args.max_logical_lag_seconds,
|
|
64
|
+
"min_free_bytes": args.min_free_bytes,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _manager(config: StandConfig) -> StandManager:
|
|
69
|
+
return StandManager(config, output=lambda message: print(message, file=sys.stderr))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _watch(manager: StandManager, args: argparse.Namespace, *, replication_only: bool) -> int:
|
|
73
|
+
samples = 0
|
|
74
|
+
while args.count == 0 or samples < args.count:
|
|
75
|
+
value = manager.replication_status() if replication_only else manager.overview()
|
|
76
|
+
_emit_stream(value, as_json=args.json or args.json_lines)
|
|
77
|
+
samples += 1
|
|
78
|
+
if args.count == 0 or samples < args.count:
|
|
79
|
+
time.sleep(args.interval)
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _emit_pretty_logs(records: list[dict[str, Any]]) -> None:
|
|
84
|
+
for record in records:
|
|
85
|
+
backend = f" {record['backend_type']}" if record["backend_type"] else ""
|
|
86
|
+
print(
|
|
87
|
+
f"{record['log_time']} [{record['node']}] {record['severity']}{backend}: "
|
|
88
|
+
f"{record['message']}"
|
|
89
|
+
)
|
|
90
|
+
for key in ("detail", "hint", "context", "query"):
|
|
91
|
+
if record[key]:
|
|
92
|
+
print(f" {key}: {record[key]}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _execute(args: argparse.Namespace, config: StandConfig) -> int:
|
|
96
|
+
if args.command == "validate":
|
|
97
|
+
result = {
|
|
98
|
+
"valid": True,
|
|
99
|
+
"config": str(config.source),
|
|
100
|
+
"project": config.metadata.name,
|
|
101
|
+
"postgres_version": config.postgres.version,
|
|
102
|
+
"image": config.postgres.image_name,
|
|
103
|
+
"topology": config.topology.mode,
|
|
104
|
+
"tls": config.postgres.tls.enabled,
|
|
105
|
+
}
|
|
106
|
+
_emit(result, as_json=args.json)
|
|
107
|
+
return 0
|
|
108
|
+
if args.command == "show":
|
|
109
|
+
_emit(config.public_document(), as_json=args.json)
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
manager = _manager(config)
|
|
113
|
+
if args.command == "up":
|
|
114
|
+
_emit(manager.up(timeout_seconds=args.timeout), as_json=args.json)
|
|
115
|
+
elif args.command == "status":
|
|
116
|
+
_emit(manager.status(), as_json=args.json)
|
|
117
|
+
elif args.command == "plan":
|
|
118
|
+
result = manager.plan()
|
|
119
|
+
_emit(result, as_json=args.json)
|
|
120
|
+
return 2 if result["required_action"] == "blocked" else 0
|
|
121
|
+
elif args.command == "resources":
|
|
122
|
+
_emit(manager.resources(args.node), as_json=args.json)
|
|
123
|
+
elif args.command == "health":
|
|
124
|
+
result = manager.health(**_health_options(args))
|
|
125
|
+
_emit(result, as_json=args.json)
|
|
126
|
+
return int(result["exit_code"])
|
|
127
|
+
elif args.command == "wait":
|
|
128
|
+
result = manager.wait_until_healthy(
|
|
129
|
+
timeout_seconds=args.timeout,
|
|
130
|
+
interval_seconds=args.interval,
|
|
131
|
+
**_health_options(args),
|
|
132
|
+
)
|
|
133
|
+
_emit(result, as_json=args.json)
|
|
134
|
+
return int(result["exit_code"])
|
|
135
|
+
elif args.command == "watch":
|
|
136
|
+
return _watch(manager, args, replication_only=False)
|
|
137
|
+
elif args.command == "replication":
|
|
138
|
+
if args.replication_command == "watch":
|
|
139
|
+
return _watch(manager, args, replication_only=True)
|
|
140
|
+
_emit(manager.replication_status(), as_json=args.json)
|
|
141
|
+
elif args.command == "apply":
|
|
142
|
+
requested_mode = "restart" if args.restart else ("reload" if args.reload else None)
|
|
143
|
+
if args.dry_run:
|
|
144
|
+
result = manager.plan(requested_mode=requested_mode)
|
|
145
|
+
_emit(result, as_json=args.json)
|
|
146
|
+
if result["required_action"] == "blocked":
|
|
147
|
+
return 2
|
|
148
|
+
return 0 if result["can_apply"] else 1
|
|
149
|
+
if requested_mode is None:
|
|
150
|
+
raise DockerRuntimeError(
|
|
151
|
+
"apply requires --reload or --restart unless --dry-run is used"
|
|
152
|
+
)
|
|
153
|
+
_emit(
|
|
154
|
+
manager.apply(
|
|
155
|
+
mode=requested_mode,
|
|
156
|
+
timeout_seconds=args.timeout,
|
|
157
|
+
stop_timeout_seconds=args.stop_timeout,
|
|
158
|
+
),
|
|
159
|
+
as_json=args.json,
|
|
160
|
+
)
|
|
161
|
+
elif args.command == "stop":
|
|
162
|
+
manager.stop(timeout_seconds=args.timeout)
|
|
163
|
+
_emit({"project": config.metadata.name, "state": "stopped"}, as_json=args.json)
|
|
164
|
+
elif args.command == "restart":
|
|
165
|
+
manager.stop(timeout_seconds=args.stop_timeout)
|
|
166
|
+
_emit(manager.up(timeout_seconds=args.timeout), as_json=args.json)
|
|
167
|
+
elif args.command == "recreate":
|
|
168
|
+
_emit(
|
|
169
|
+
manager.recreate(
|
|
170
|
+
clear_data=args.clear_data,
|
|
171
|
+
timeout_seconds=args.timeout,
|
|
172
|
+
stop_timeout_seconds=args.stop_timeout,
|
|
173
|
+
),
|
|
174
|
+
as_json=args.json,
|
|
175
|
+
)
|
|
176
|
+
elif args.command == "down":
|
|
177
|
+
manager.down(clear_data=args.clear_data, timeout_seconds=args.timeout)
|
|
178
|
+
_emit(
|
|
179
|
+
{"project": config.metadata.name, "state": "removed", "data_cleared": args.clear_data},
|
|
180
|
+
as_json=args.json,
|
|
181
|
+
)
|
|
182
|
+
elif args.command == "storage":
|
|
183
|
+
if args.storage_command == "init":
|
|
184
|
+
result = manager.storage_init()
|
|
185
|
+
elif args.storage_command == "status":
|
|
186
|
+
result = manager.storage_status()
|
|
187
|
+
elif args.storage_command == "usage":
|
|
188
|
+
result = manager.storage_usage()
|
|
189
|
+
elif args.storage_command == "clean":
|
|
190
|
+
result = manager.storage_clean()
|
|
191
|
+
else: # pragma: no cover
|
|
192
|
+
raise AssertionError(f"unhandled storage command: {args.storage_command}")
|
|
193
|
+
_emit(result, as_json=args.json)
|
|
194
|
+
elif args.command == "logical":
|
|
195
|
+
_emit(
|
|
196
|
+
manager.logical_sync(
|
|
197
|
+
full=args.full,
|
|
198
|
+
timeout_seconds=args.timeout,
|
|
199
|
+
stop_timeout_seconds=args.stop_timeout,
|
|
200
|
+
),
|
|
201
|
+
as_json=args.json,
|
|
202
|
+
)
|
|
203
|
+
elif args.command == "standby":
|
|
204
|
+
_emit(
|
|
205
|
+
manager.standby_reinit(
|
|
206
|
+
timeout_seconds=args.timeout,
|
|
207
|
+
stop_timeout_seconds=args.stop_timeout,
|
|
208
|
+
),
|
|
209
|
+
as_json=args.json,
|
|
210
|
+
)
|
|
211
|
+
elif args.command == "node":
|
|
212
|
+
if args.node_command == "inspect":
|
|
213
|
+
result = manager.node_inspect(args.node)
|
|
214
|
+
else:
|
|
215
|
+
result = manager.node_restart(
|
|
216
|
+
args.node,
|
|
217
|
+
timeout_seconds=args.timeout,
|
|
218
|
+
stop_timeout_seconds=args.stop_timeout,
|
|
219
|
+
)
|
|
220
|
+
_emit(result, as_json=args.json)
|
|
221
|
+
elif args.command == "logs":
|
|
222
|
+
has_filters = any((args.since, args.until, args.severity, args.backend_type, args.contains))
|
|
223
|
+
if args.follow:
|
|
224
|
+
if args.node == "all" or has_filters or args.format != "raw":
|
|
225
|
+
raise DockerRuntimeError("--follow supports one node in raw format without filters")
|
|
226
|
+
manager.logs(args.node, follow=True, tail=args.tail)
|
|
227
|
+
elif args.format == "raw" and args.node != "all" and not has_filters and not args.json:
|
|
228
|
+
manager.logs(args.node, tail=args.tail)
|
|
229
|
+
else:
|
|
230
|
+
now = datetime.now(timezone.utc)
|
|
231
|
+
records = manager.log_records(
|
|
232
|
+
args.node,
|
|
233
|
+
tail=args.tail,
|
|
234
|
+
since=_moment(args.since, now=now) if args.since else None,
|
|
235
|
+
until=_moment(args.until, now=now) if args.until else None,
|
|
236
|
+
severity=args.severity,
|
|
237
|
+
backend_type=args.backend_type,
|
|
238
|
+
contains=args.contains,
|
|
239
|
+
)
|
|
240
|
+
if args.json or args.format == "json":
|
|
241
|
+
print(json.dumps(records, indent=2, sort_keys=True))
|
|
242
|
+
else:
|
|
243
|
+
_emit_pretty_logs(records)
|
|
244
|
+
elif args.command == "events":
|
|
245
|
+
if args.follow and args.until:
|
|
246
|
+
raise DockerRuntimeError("events --follow cannot be combined with --until")
|
|
247
|
+
now = datetime.now(timezone.utc)
|
|
248
|
+
since = int(_moment(args.since, now=now).timestamp())
|
|
249
|
+
until = (
|
|
250
|
+
None
|
|
251
|
+
if args.follow
|
|
252
|
+
else int((_moment(args.until, now=now) if args.until else now).timestamp())
|
|
253
|
+
)
|
|
254
|
+
for event in manager.docker_events(since_epoch=since, until_epoch=until, count=args.count):
|
|
255
|
+
_emit_stream(event, as_json=args.json or args.json_lines)
|
|
256
|
+
elif args.command == "doctor":
|
|
257
|
+
result = manager.doctor()
|
|
258
|
+
_emit(result, as_json=args.json)
|
|
259
|
+
return int(result["exit_code"])
|
|
260
|
+
elif args.command == "capabilities":
|
|
261
|
+
_emit(manager.capabilities(), as_json=args.json)
|
|
262
|
+
elif args.command == "dsn":
|
|
263
|
+
_emit(
|
|
264
|
+
manager.connection_info(
|
|
265
|
+
args.node,
|
|
266
|
+
database=args.database,
|
|
267
|
+
show_password=args.show_password,
|
|
268
|
+
),
|
|
269
|
+
as_json=args.json,
|
|
270
|
+
)
|
|
271
|
+
elif args.command == "psql":
|
|
272
|
+
return manager.run_psql(args.node, database=args.database)
|
|
273
|
+
elif args.command == "ssh":
|
|
274
|
+
return manager.run_ssh(args.node)
|
|
275
|
+
elif args.command == "image":
|
|
276
|
+
if args.image_command == "list":
|
|
277
|
+
result = manager.image_list()
|
|
278
|
+
elif args.image_command == "inspect":
|
|
279
|
+
result = manager.image_inspect()
|
|
280
|
+
else:
|
|
281
|
+
result = manager.image_build()
|
|
282
|
+
_emit(result, as_json=args.json)
|
|
283
|
+
elif args.command == "cleanup":
|
|
284
|
+
if args.cleanup_command == "status":
|
|
285
|
+
result = manager.cleanup_status()
|
|
286
|
+
else:
|
|
287
|
+
result = manager.cleanup(
|
|
288
|
+
remove_containers=args.containers or args.all,
|
|
289
|
+
remove_storage=args.storage or args.all,
|
|
290
|
+
remove_images=args.images or args.all,
|
|
291
|
+
remove_cache=args.cache or args.all,
|
|
292
|
+
remove_credentials=args.all,
|
|
293
|
+
stop_timeout_seconds=args.stop_timeout,
|
|
294
|
+
)
|
|
295
|
+
_emit(result, as_json=args.json)
|
|
296
|
+
elif args.command == "sql":
|
|
297
|
+
print(manager.sql(args.node, args.statement, database=args.database))
|
|
298
|
+
else: # pragma: no cover
|
|
299
|
+
raise AssertionError(f"unhandled command: {args.command}")
|
|
300
|
+
return 0
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
304
|
+
parser = _parser()
|
|
305
|
+
args = parser.parse_args(argv)
|
|
306
|
+
try:
|
|
307
|
+
if args.command == "active":
|
|
308
|
+
result = discover_active_stand()
|
|
309
|
+
_emit(result, as_json=args.json)
|
|
310
|
+
return 0 if result["active"] else 1
|
|
311
|
+
if args.command == "init":
|
|
312
|
+
_emit(
|
|
313
|
+
initialize_project(args.directory, force=args.force),
|
|
314
|
+
as_json=args.json,
|
|
315
|
+
)
|
|
316
|
+
return 0
|
|
317
|
+
if args.config is None:
|
|
318
|
+
raise DockerRuntimeError(f"--config is required for {args.command}")
|
|
319
|
+
config = load_config(
|
|
320
|
+
args.config,
|
|
321
|
+
postgres_version=args.postgres_version,
|
|
322
|
+
tls_enabled=args.tls,
|
|
323
|
+
)
|
|
324
|
+
return _execute(args, config)
|
|
325
|
+
except (PgStandError, docker.errors.DockerException) as exc:
|
|
326
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
327
|
+
return 2
|
|
328
|
+
except KeyboardInterrupt:
|
|
329
|
+
print("ERROR: interrupted", file=sys.stderr)
|
|
330
|
+
return 130
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
if __name__ == "__main__": # pragma: no cover
|
|
334
|
+
raise SystemExit(main())
|