pglift-cli 1.7.0__tar.gz → 1.9.0__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.
Files changed (27) hide show
  1. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/PKG-INFO +9 -7
  2. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/pyproject.toml +6 -7
  3. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/_site.py +2 -3
  4. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/base.py +1 -0
  5. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/instance.py +17 -6
  6. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/main.py +5 -5
  7. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/pgconf.py +6 -6
  8. pglift_cli-1.9.0/src/pglift_cli/pghba.py +57 -0
  9. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/role.py +21 -3
  10. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/util.py +25 -5
  11. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/.gitignore +0 -0
  12. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/README.md +0 -0
  13. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/hatch.toml +0 -0
  14. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/__init__.py +0 -0
  15. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/__main__.py +0 -0
  16. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/_settings.py +0 -0
  17. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/console.py +0 -0
  18. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/database.py +0 -0
  19. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/hookspecs.py +0 -0
  20. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/model.py +0 -0
  21. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/patroni.py +0 -0
  22. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/pgbackrest/__init__.py +0 -0
  23. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/pgbackrest/repo_path.py +0 -0
  24. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/pm.py +0 -0
  25. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/postgres.py +0 -0
  26. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/prometheus.py +0 -0
  27. {pglift_cli-1.7.0 → pglift_cli-1.9.0}/src/pglift_cli/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pglift_cli
3
- Version: 1.7.0
3
+ Version: 1.9.0
4
4
  Summary: Command-line interface for pglift
5
5
  Project-URL: Documentation, https://pglift.readthedocs.io/
6
6
  Project-URL: Source, https://gitlab.com/dalibo/pglift/
@@ -18,6 +18,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
18
18
  Classifier: Programming Language :: Python :: 3.9
19
19
  Classifier: Programming Language :: Python :: 3.10
20
20
  Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
21
23
  Classifier: Topic :: Database
22
24
  Classifier: Topic :: System :: Systems Administration
23
25
  Classifier: Typing :: Typed
@@ -26,18 +28,18 @@ Requires-Dist: click!=8.1.0,!=8.1.4,>=8.0.0
26
28
  Requires-Dist: filelock!=3.12.1,>=3.9.0
27
29
  Requires-Dist: pluggy
28
30
  Requires-Dist: psycopg>=3.1
29
- Requires-Dist: pydantic>=2.5.0
31
+ Requires-Dist: pydantic!=2.10.0,!=2.10.1,>=2.5.0
30
32
  Requires-Dist: pyyaml>=6.0.1
31
- Requires-Dist: rich>=11.0.0
33
+ Requires-Dist: rich!=13.9.0,>=11.0.0
32
34
  Provides-Extra: dev
33
35
  Requires-Dist: anyio; extra == 'dev'
34
- Requires-Dist: mypy!=1.11.*,>=1.10.0; (python_version < '3.10') and extra == 'dev'
36
+ Requires-Dist: mypy!=1.11.*,!=1.12.*,>=1.10.0; (python_version < '3.10') and extra == 'dev'
35
37
  Requires-Dist: mypy>=1.10.0; (python_version >= '3.10') and extra == 'dev'
36
38
  Requires-Dist: patroni[etcd]>=2.1.5; extra == 'dev'
37
39
  Requires-Dist: port-for; extra == 'dev'
38
40
  Requires-Dist: prysk[pytest-plugin]>=0.14.0; extra == 'dev'
39
- Requires-Dist: pytest; extra == 'dev'
40
41
  Requires-Dist: pytest-cov; extra == 'dev'
42
+ Requires-Dist: pytest>=8; extra == 'dev'
41
43
  Requires-Dist: trustme; extra == 'dev'
42
44
  Requires-Dist: types-pyyaml>=6.0.12.10; extra == 'dev'
43
45
  Provides-Extra: test
@@ -45,11 +47,11 @@ Requires-Dist: anyio; extra == 'test'
45
47
  Requires-Dist: patroni[etcd]>=2.1.5; extra == 'test'
46
48
  Requires-Dist: port-for; extra == 'test'
47
49
  Requires-Dist: prysk[pytest-plugin]>=0.14.0; extra == 'test'
48
- Requires-Dist: pytest; extra == 'test'
49
50
  Requires-Dist: pytest-cov; extra == 'test'
51
+ Requires-Dist: pytest>=8; extra == 'test'
50
52
  Requires-Dist: trustme; extra == 'test'
51
53
  Provides-Extra: typing
52
- Requires-Dist: mypy!=1.11.*,>=1.10.0; (python_version < '3.10') and extra == 'typing'
54
+ Requires-Dist: mypy!=1.11.*,!=1.12.*,>=1.10.0; (python_version < '3.10') and extra == 'typing'
53
55
  Requires-Dist: mypy>=1.10.0; (python_version >= '3.10') and extra == 'typing'
54
56
  Requires-Dist: types-pyyaml>=6.0.12.10; extra == 'typing'
55
57
  Description-Content-Type: text/markdown
@@ -31,6 +31,8 @@ classifiers = [
31
31
  "Programming Language :: Python :: 3.9",
32
32
  "Programming Language :: Python :: 3.10",
33
33
  "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
34
36
  "Programming Language :: Python :: 3 :: Only",
35
37
  "Typing :: Typed",
36
38
  ]
@@ -42,8 +44,8 @@ dependencies = [
42
44
  "filelock >= 3.9.0, != 3.12.1",
43
45
  "pluggy",
44
46
  "psycopg >= 3.1",
45
- "pydantic >= 2.5.0",
46
- "rich >= 11.0.0",
47
+ "pydantic >= 2.5.0, != 2.10.0, != 2.10.1",
48
+ "rich >= 11.0.0, != 13.9.0",
47
49
  ]
48
50
 
49
51
  [project.optional-dependencies]
@@ -52,13 +54,13 @@ test = [
52
54
  "patroni[etcd] >= 2.1.5",
53
55
  "port-for",
54
56
  "prysk[pytest-plugin] >= 0.14.0",
55
- "pytest",
57
+ "pytest >= 8",
56
58
  "pytest-cov",
57
59
  "trustme",
58
60
  ]
59
61
  typing = [
60
62
  "mypy >= 1.10.0 ; python_version >= '3.10'",
61
- "mypy >= 1.10.0, != 1.11.* ; python_version < '3.10'",
63
+ "mypy >= 1.10.0, != 1.11.*, != 1.12.* ; python_version < '3.10'",
62
64
  "types-PyYAML >= 6.0.12.10",
63
65
  ]
64
66
  dev = [
@@ -75,6 +77,3 @@ Tracker = "https://gitlab.com/dalibo/pglift/-/issues/"
75
77
  "pglift_cli.pgbackrest" = "pglift_cli.pgbackrest"
76
78
  "pglift_cli.pgbackrest.repo_path" = "pglift_cli.pgbackrest.repo_path"
77
79
  "pglift_cli.prometheus" = "pglift_cli.prometheus"
78
-
79
- [project.scripts]
80
- pglift = "pglift_cli.main:cli"
@@ -20,7 +20,8 @@ from pglift import exceptions, plugin_manager
20
20
  from pglift.models import interface
21
21
 
22
22
  from . import hookspecs
23
- from ._settings import Settings, SiteSettings
23
+ from ._settings import Settings
24
+ from ._settings import SiteSettings as SiteSettings
24
25
  from .pm import PluginManager
25
26
 
26
27
 
@@ -30,8 +31,6 @@ def _settings() -> Settings:
30
31
  return SiteSettings()
31
32
  except (exceptions.SettingsError, pydantic.ValidationError) as e:
32
33
  raise click.ClickException(f"invalid site settings\n{e}") from e
33
- except exceptions.UnsupportedError as e:
34
- raise click.ClickException(f"unsupported operation: {e}") from None
35
34
 
36
35
 
37
36
  _default_settings = cache(Settings)
@@ -26,6 +26,7 @@ class CLIGroup(click.Group):
26
26
  "role",
27
27
  "database",
28
28
  "postgres",
29
+ "pghba",
29
30
  ]
30
31
 
31
32
  @classmethod
@@ -14,6 +14,7 @@ from pydantic.v1.utils import deep_update
14
14
 
15
15
  from pglift import (
16
16
  async_hooks,
17
+ exceptions,
17
18
  hooks,
18
19
  hookspecs,
19
20
  instances,
@@ -127,7 +128,7 @@ async def alter(obj: Obj, instance: system.Instance, **changes: Any) -> None:
127
128
  values = deep_update(values, changes)
128
129
  # No need for 'settings' in validation_context() as a 'version' key
129
130
  # must be present in 'values' when altering.
130
- with validation_context(operation="update"):
131
+ with validation_context(operation="update", instance=manifest):
131
132
  altered = _site.INSTANCE_MODEL.model_validate(values)
132
133
  await instances.apply(
133
134
  _site.SETTINGS, altered, _is_running=status == Status.running
@@ -149,10 +150,18 @@ async def apply(
149
150
  version = default_postgresql_version(_site.SETTINGS.postgresql)
150
151
  elif not isinstance(version, str):
151
152
  version = str(version)
152
- op: Operation = (
153
- "update" if instances.exists(name, version, _site.SETTINGS) else "create"
154
- )
155
- with validation_context(operation=op, settings=_site.SETTINGS):
153
+ op: Operation = "create"
154
+ actual: interface.Instance | None = None
155
+ if data.get("state") == "absent":
156
+ op = "update"
157
+ else:
158
+ try:
159
+ actual = await instances.get((name, version), settings=_site.SETTINGS)
160
+ except exceptions.InstanceNotFound:
161
+ pass
162
+ else:
163
+ op = "update"
164
+ with validation_context(operation=op, settings=_site.SETTINGS, instance=actual):
156
165
  instance = _site.INSTANCE_MODEL.model_validate(data)
157
166
  if dry_run:
158
167
  ret = interface.InstanceApplyResult(change_state=None)
@@ -196,7 +205,9 @@ async def get(instance: system.Instance, output_format: OutputFormat | None) ->
196
205
  }
197
206
  if not instance.postgresql.standby:
198
207
  exclude.add("standby")
199
- print_table_for([i], partial(model_dump, exclude=exclude), box=None)
208
+ print_table_for(
209
+ [i], partial(model_dump, exclude=exclude, mode="pretty"), box=None
210
+ )
200
211
 
201
212
 
202
213
  @cli.command("list")
@@ -5,11 +5,11 @@
5
5
  from __future__ import annotations
6
6
 
7
7
  import logging
8
- import pathlib
9
8
  import sys
10
9
  import warnings
11
10
  from functools import partial
12
11
  from importlib.metadata import version
12
+ from pathlib import Path
13
13
  from typing import Literal
14
14
 
15
15
  import click
@@ -94,7 +94,7 @@ def log_level(
94
94
  @click.option(
95
95
  "-l",
96
96
  "--log-file",
97
- type=click.Path(dir_okay=False, resolve_path=True, path_type=pathlib.Path),
97
+ type=click.Path(dir_okay=False, resolve_path=True, path_type=Path),
98
98
  metavar="LOGFILE",
99
99
  help="Write logs to LOGFILE, instead of stderr.",
100
100
  )
@@ -133,7 +133,7 @@ def log_level(
133
133
  def cli(
134
134
  context: click.Context,
135
135
  log_level: int | None,
136
- log_file: pathlib.Path | None,
136
+ log_file: Path | None,
137
137
  debug: bool,
138
138
  interactive: bool,
139
139
  ) -> None:
@@ -243,7 +243,7 @@ def site_settings(
243
243
  @click.option(
244
244
  "--settings",
245
245
  "settings_file",
246
- type=click.Path(exists=True, path_type=pathlib.Path),
246
+ type=click.Path(exists=True, path_type=Path),
247
247
  help="Custom settings file.",
248
248
  )
249
249
  @click.argument(
@@ -256,7 +256,7 @@ async def site_configure(
256
256
  context: click.Context,
257
257
  obj: Obj,
258
258
  action: Literal["install", "uninstall", "check"],
259
- settings_file: pathlib.Path | None,
259
+ settings_file: Path | None,
260
260
  ) -> None:
261
261
  """Manage installation of extra data files for pglift.
262
262
 
@@ -105,10 +105,10 @@ async def set_(obj: Obj, instance: system.Instance, parameters: dict[str, Any])
105
105
  status = await postgresql.status(pg_instance)
106
106
  manifest = await instances._get(instance, status)
107
107
  manifest.settings.update(parameters)
108
- changes = await instances.configure(
108
+ r = await instances.configure(
109
109
  pg_instance, manifest, _is_running=status == Status.running
110
110
  )
111
- show_configuration_changes(changes, parameters.keys())
111
+ show_configuration_changes(r.changes, parameters.keys())
112
112
 
113
113
 
114
114
  @cli.command("remove")
@@ -129,10 +129,10 @@ async def remove(obj: Obj, instance: system.Instance, parameters: tuple[str]) ->
129
129
  raise click.ClickException(
130
130
  f"{p!r} not found in managed configuration"
131
131
  ) from None
132
- changes = await instances.configure(
132
+ r = await instances.configure(
133
133
  pg_instance, manifest, _is_running=status == Status.running
134
134
  )
135
- show_configuration_changes(changes, parameters)
135
+ show_configuration_changes(r.changes, parameters)
136
136
 
137
137
 
138
138
  @cli.command("edit")
@@ -156,7 +156,7 @@ async def edit(obj: Obj, instance: system.Instance) -> None:
156
156
  manifest = await instances._get(instance, status)
157
157
  manifest.settings.clear()
158
158
  manifest.settings.update(values)
159
- changes = await instances.configure(
159
+ r = await instances.configure(
160
160
  pg_instance, manifest, _is_running=status == Status.running
161
161
  )
162
- show_configuration_changes(changes)
162
+ show_configuration_changes(r.changes)
@@ -0,0 +1,57 @@
1
+ # SPDX-FileCopyrightText: 2024 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+ import click
10
+
11
+ from pglift import hba
12
+ from pglift.models import interface, system
13
+
14
+ from . import model
15
+ from .util import (
16
+ Group,
17
+ Obj,
18
+ audit,
19
+ instance_identifier_option,
20
+ pass_postgresql_instance,
21
+ )
22
+
23
+
24
+ @click.group(cls=Group)
25
+ @instance_identifier_option
26
+ def cli(**kwargs: Any) -> None:
27
+ """Manage entries in the pg_hba.conf file of a PostgreSQL instance."""
28
+
29
+
30
+ @cli.command("add")
31
+ @model.as_parameters(interface.HbaRecord, "create")
32
+ @pass_postgresql_instance
33
+ @click.pass_obj
34
+ def add(
35
+ obj: Obj, instance: system.PostgreSQLInstance, hbarecord: interface.HbaRecord
36
+ ) -> None:
37
+ """Add a record in pg_hba.conf.
38
+
39
+ If no --connection-* option is specified, a 'local' record is added.
40
+ """
41
+ with obj.lock, audit():
42
+ hba.add(instance, hbarecord)
43
+
44
+
45
+ @cli.command("remove")
46
+ @model.as_parameters(interface.HbaRecord, "create")
47
+ @pass_postgresql_instance
48
+ @click.pass_obj
49
+ def remove(
50
+ obj: Obj, instance: system.PostgreSQLInstance, hbarecord: interface.HbaRecord
51
+ ) -> None:
52
+ """Remove a record from pg_hba.conf.
53
+
54
+ If no --connection-* option is specified, a 'local' record is removed.
55
+ """
56
+ with obj.lock, audit():
57
+ hba.remove(instance, hbarecord)
@@ -125,7 +125,12 @@ async def apply(
125
125
  ) -> None:
126
126
  """Apply manifest as a role"""
127
127
  op: Operation = (
128
- "update" if await roles.exists(instance, name=data["name"]) else "create"
128
+ "update"
129
+ if (
130
+ data.get("state") == "absent"
131
+ or await roles.exists(instance, name=data["name"])
132
+ )
133
+ else "create"
129
134
  )
130
135
  with validation_context(operation=op, settings=_site.SETTINGS):
131
136
  role = _site.ROLE_MODEL.model_validate(data)
@@ -152,7 +157,14 @@ async def ls(
152
157
  if output_format == "json":
153
158
  print_json_for([model_dump(r) for r in rls])
154
159
  else:
155
- print_table_for(rls, partial(model_dump, mode="pretty", exclude={"in_roles"}))
160
+ print_table_for(
161
+ rls,
162
+ partial(
163
+ model_dump,
164
+ mode="pretty",
165
+ exclude={"in_roles", "hba_records", "validity"},
166
+ ),
167
+ )
156
168
 
157
169
 
158
170
  @cli.command("get")
@@ -170,7 +182,13 @@ async def get(
170
182
  print_json_for(model_dump(r))
171
183
  else:
172
184
  print_table_for(
173
- [r], partial(model_dump, mode="pretty", exclude={"in_roles"}), box=None
185
+ [r],
186
+ partial(
187
+ model_dump,
188
+ mode="pretty",
189
+ exclude={"in_roles", "hba_records", "validity"},
190
+ ),
191
+ box=None,
174
192
  )
175
193
 
176
194
 
@@ -10,7 +10,6 @@ import json
10
10
  import logging
11
11
  import logging.handlers
12
12
  import os
13
- import pathlib
14
13
  import shlex
15
14
  import sys
16
15
  import tempfile
@@ -20,6 +19,7 @@ from collections.abc import Coroutine, Iterable, Iterator, Sequence
20
19
  from contextlib import contextmanager
21
20
  from datetime import timedelta
22
21
  from functools import cache, cached_property, partial, singledispatch, wraps
22
+ from pathlib import Path
23
23
  from typing import IO, Any, Callable, Literal, TypedDict, TypeVar
24
24
 
25
25
  import click
@@ -442,10 +442,19 @@ class ManifestData(TypedDict, total=False):
442
442
  OutputFormat = Literal["json"]
443
443
 
444
444
 
445
+ def set_output_format(
446
+ ctx: click.Context, param: click.Parameter, value: OutputFormat
447
+ ) -> OutputFormat:
448
+ assert ctx.obj.output_format is None
449
+ ctx.obj.output_format = value
450
+ return value
451
+
452
+
445
453
  output_format_option = click.option(
446
454
  "-o",
447
455
  "--output-format",
448
456
  type=click.Choice(typing.get_args(OutputFormat), case_sensitive=False),
457
+ callback=set_output_format,
449
458
  help="Specify the output format.",
450
459
  )
451
460
 
@@ -537,7 +546,7 @@ def audit(
537
546
 
538
547
 
539
548
  @contextmanager
540
- def command_logging(logdir: pathlib.Path | None) -> Iterator[None]:
549
+ def command_logging(logdir: Path | None) -> Iterator[None]:
541
550
  logdir_created = False
542
551
  logfilename = f"{time.time()}.log"
543
552
  if logdir is not None: # pragma: nocover (DEPRECATED)
@@ -549,13 +558,13 @@ def command_logging(logdir: pathlib.Path | None) -> Iterator[None]:
549
558
  logdir_created = True
550
559
  except OSError:
551
560
  # Might be, e.g. PermissionError, if log file path is not writable.
552
- logfile = pathlib.Path(
561
+ logfile = Path(
553
562
  tempfile.NamedTemporaryFile(
554
563
  prefix="pglift-", suffix="-" + logfilename
555
564
  ).name
556
565
  )
557
566
  else:
558
- logfile = pathlib.Path(
567
+ logfile = Path(
559
568
  tempfile.NamedTemporaryFile(prefix="pglift-", suffix="-" + logfilename).name
560
569
  )
561
570
  handler = logging.FileHandler(logfile)
@@ -614,8 +623,11 @@ class Obj:
614
623
  # instance_identifier_option decorator's callback.
615
624
  _instance: str | system.PostgreSQLInstance
616
625
 
617
- def __init__(self, *, debug: bool = False) -> None:
626
+ def __init__(
627
+ self, *, debug: bool = False, output_format: OutputFormat | None = None
628
+ ) -> None:
618
629
  self.debug = debug
630
+ self.output_format = output_format
619
631
 
620
632
  @cached_property
621
633
  def lock(self) -> filelock.FileLock:
@@ -660,6 +672,8 @@ class Command(click.Command):
660
672
  raise click.Abort from None
661
673
  except pydantic.ValidationError as e:
662
674
  logger.debug("a validation error occurred", exc_info=obj.debug)
675
+ if context.obj.output_format == "json":
676
+ console.print_json(e.json(include_url=False, include_context=False))
663
677
  raise click.ClickException(str(e)) from None
664
678
  except exceptions.Error as e:
665
679
  logger.debug("an internal error occurred", exc_info=obj.debug)
@@ -680,6 +694,10 @@ class Command(click.Command):
680
694
  raise click.ClickException(str(e).strip()) from None
681
695
 
682
696
 
697
+ def is_root() -> bool:
698
+ return os.getuid() == 0
699
+
700
+
683
701
  class Group(click.Group):
684
702
  command_class = Command
685
703
  group_class = type
@@ -690,6 +708,8 @@ class Group(click.Group):
690
708
  super().add_command(command, name)
691
709
 
692
710
  def invoke(self, ctx: click.Context) -> Any:
711
+ if is_root():
712
+ raise click.ClickException("pglift cannot be used as root")
693
713
  if not install.check(_site.SETTINGS):
694
714
  raise click.ClickException(
695
715
  "broken installation; did you run 'site-configure install'?",
File without changes
File without changes
File without changes