pglift-cli 2.3.0__tar.gz → 2.5.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 (47) hide show
  1. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/PKG-INFO +1 -1
  2. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/database.py +3 -1
  3. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/instance.py +3 -1
  4. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/model.py +105 -20
  5. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/pgconf.py +3 -4
  6. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/pghba.py +20 -0
  7. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/role.py +10 -2
  8. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/util.py +6 -3
  9. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/expect/test-cli-walkthrough.t +45 -10
  10. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/expect/test-help.t +8 -0
  11. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/expect/test-port-validation.t +6 -6
  12. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/expect/test-prometheus.t +1 -1
  13. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/expect/test-standby-pgbackrest.t +2 -2
  14. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/expect/test-transactions.t +12 -3
  15. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/unit/test_audit.py +3 -3
  16. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/unit/test_cli.py +27 -0
  17. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/unit/test_model.py +18 -0
  18. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/.gitignore +0 -0
  19. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/README.md +0 -0
  20. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/hatch.toml +0 -0
  21. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/pyproject.toml +0 -0
  22. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/pytest.ini +0 -0
  23. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/__init__.py +0 -0
  24. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/__main__.py +0 -0
  25. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/_settings.py +0 -0
  26. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/_site.py +0 -0
  27. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/base.py +0 -0
  28. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/console.py +0 -0
  29. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/hookspecs.py +0 -0
  30. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/main.py +0 -0
  31. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/patroni.py +0 -0
  32. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/pgbackrest/__init__.py +0 -0
  33. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/pgbackrest/repo_path.py +0 -0
  34. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/pm.py +0 -0
  35. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/postgres.py +0 -0
  36. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/prometheus.py +0 -0
  37. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/src/pglift_cli/py.typed +0 -0
  38. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/expect/.gitignore +0 -0
  39. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/expect/test-base.t +0 -0
  40. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/expect/test-demote.t +0 -0
  41. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/expect/test-upgrade.t +0 -0
  42. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/unit/__init__.py +0 -0
  43. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/unit/conftest.py +0 -0
  44. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/unit/test__site.py +0 -0
  45. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/unit/test_main.py +0 -0
  46. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/unit/test_pm.py +0 -0
  47. {pglift_cli-2.3.0 → pglift_cli-2.5.0}/tests/unit/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pglift_cli
3
- Version: 2.3.0
3
+ Version: 2.5.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/
@@ -155,7 +155,9 @@ async def get(
155
155
  if output_format == "json":
156
156
  print_json_for(model_dump(db))
157
157
  else:
158
- print_table_for([db], functools.partial(model_dump, mode="pretty"), box=None)
158
+ print_table_for(
159
+ [db], functools.partial(model_dump, context={"pretty": True}), box=None
160
+ )
159
161
 
160
162
 
161
163
  @cli.command("list")
@@ -275,7 +275,9 @@ async def get(instance: Instance, output_format: OutputFormat | None) -> None:
275
275
  if not instance.postgresql.standby:
276
276
  exclude.add("standby")
277
277
  print_table_for(
278
- [i], partial(model_dump, exclude=exclude, mode="pretty"), box=None
278
+ [i],
279
+ partial(model_dump, exclude=exclude, context={"pretty": True}),
280
+ box=None,
279
281
  )
280
282
 
281
283
 
@@ -17,8 +17,10 @@ import click
17
17
  import pydantic
18
18
  from pydantic.fields import FieldInfo
19
19
  from pydantic.v1.utils import deep_update, lenient_issubclass
20
+ from pydantic_core import ErrorDetails
20
21
 
21
22
  from pglift.annotations import cli
23
+ from pglift.exceptions import MutuallyExclusiveError
22
24
  from pglift.models.helpers import is_optional, optional_type
23
25
  from pglift.models.interface import PresenceState
24
26
  from pglift.types import (
@@ -54,7 +56,7 @@ def as_parameters(model_type: ModelType, operation: Operation) -> ClickDecorator
54
56
  for modelname, argname in modelnames_and_argnames:
55
57
  value = kwargs.pop(argname)
56
58
  if value is DEFAULT:
57
- continue
59
+ continue # ignore unset or set with default value parameters
58
60
  args[modelname] = value
59
61
  return args
60
62
 
@@ -165,7 +167,11 @@ class ParamSpec(ABC):
165
167
  loc: tuple[str, ...]
166
168
  description: str | None = None
167
169
 
168
- objtype: ClassVar = click.Parameter
170
+ objtype: ClassVar[type[click.Parameter]] = click.Parameter
171
+
172
+ @property
173
+ def param(self) -> click.Parameter:
174
+ return self.objtype(self.param_decls, **self.attrs)
169
175
 
170
176
  @property
171
177
  @abstractmethod
@@ -179,9 +185,7 @@ class ParamSpec(ABC):
179
185
  return self.loc == loc
180
186
 
181
187
  def badparameter_exception(self, message: str) -> click.BadParameter:
182
- return click.BadParameter(
183
- message, None, param=self.objtype(self.param_decls, **self.attrs)
184
- )
188
+ return click.BadParameter(message, None, param=self.param)
185
189
 
186
190
 
187
191
  class ArgumentSpec(ParamSpec):
@@ -226,6 +230,50 @@ class _Parent:
226
230
  required: bool
227
231
 
228
232
 
233
+ def is_editable(ftype: Any) -> bool:
234
+ """Determine whether a given type is considered "editable".
235
+
236
+ A type is considered editable if:
237
+
238
+ - It is a subclass of pydantic.BaseModel.
239
+ - It has a type hint for a field named "state" of type PresenceState.
240
+
241
+ This function also handles types wrapped with typing.Annotated,
242
+ automatically extracting the underlying type for the check.
243
+
244
+ >>> class Editable(pydantic.BaseModel):
245
+ ... state: typing.Annotated[PresenceState, object()]
246
+ >>>
247
+ >>> is_editable(Editable)
248
+ True
249
+ >>> class AlsoEditable(pydantic.BaseModel):
250
+ ... state: typing.Annotated[typing.Annotated[PresenceState, object()], object()]
251
+ >>>
252
+ >>> is_editable(AlsoEditable)
253
+ True
254
+ >>>
255
+ >>> class EditableNotAnnotated(pydantic.BaseModel):
256
+ ... state: PresenceState
257
+ >>>
258
+ >>> is_editable(EditableNotAnnotated)
259
+ True
260
+ >>> class NotEditable(pydantic.BaseModel):
261
+ ... state: typing.Annotated[typing.Annotated[str, object()], object()]
262
+ >>>
263
+ >>> is_editable(NotEditable)
264
+ False
265
+ """
266
+ # Check if there's a "state" field (PresenceState type)
267
+ if typing.get_origin(ftype) is typing.Annotated:
268
+ ftype = typing.get_args(ftype)[0]
269
+ assert ftype is not None
270
+ hints = typing.get_type_hints(ftype)
271
+ return (
272
+ lenient_issubclass(ftype, pydantic.BaseModel)
273
+ and hints.get("state") is PresenceState
274
+ )
275
+
276
+
229
277
  def _paramspecs_from_model(
230
278
  model_type: ModelType,
231
279
  operation: Operation,
@@ -237,11 +285,18 @@ def _paramspecs_from_model(
237
285
  """
238
286
 
239
287
  def default(_ctx: click.Context, param: click.Argument, value: Any) -> Any:
288
+ """This function is intended to distinguish parameters that were
289
+ explicitly provided by the user versus those that were omitted (or
290
+ explicitly provided with value equal to the default).
291
+
292
+ If the parameter value is unset (or equal to the default value), it
293
+ returns DEFAULT instead of the raw value.
294
+ """
240
295
  if (param.multiple and value == ()) or (value == param.default):
241
296
  return DEFAULT
242
297
  return value
243
298
 
244
- def update_callback(
299
+ def add_state_field_callback(
245
300
  ctx: click.Context,
246
301
  _param: click.Argument,
247
302
  value: Any,
@@ -250,6 +305,20 @@ def _paramspecs_from_model(
250
305
  key: str = "name",
251
306
  remove: bool = False,
252
307
  ) -> None:
308
+ """Callback that appends each value to ctx.params[optname] as a dictionary
309
+ with the specified key and a "state" field, set to either "present" or
310
+ "absent" depending on the remove argument.
311
+
312
+
313
+ Example, --add-user alice --add-user bob --remove-user carol will
314
+ result to:
315
+
316
+ ctx.params["user"] = (
317
+ {"name": "alice", "state": "present"},
318
+ {"name": "bob", "state": "present"},
319
+ {"name": "carol", "state": "absent"},
320
+ )
321
+ """
253
322
  if optname not in ctx.params:
254
323
  ctx.params[optname] = ()
255
324
  ctx.params[optname] += tuple(
@@ -257,17 +326,6 @@ def _paramspecs_from_model(
257
326
  )
258
327
  return
259
328
 
260
- def is_editable(ftype: Any) -> bool:
261
- # Check if there's a "state" field (PresenceState type)
262
- if typing.get_origin(ftype) is typing.Annotated:
263
- ftype = typing.get_args(ftype)[0]
264
- assert ftype is not None
265
- return (
266
- lenient_issubclass(ftype, pydantic.BaseModel)
267
- and "state" in ftype.model_fields
268
- and ftype.model_fields["state"].annotation is PresenceState
269
- )
270
-
271
329
  for fname, field in model_type.model_fields.items():
272
330
  if field_annotation(field, cli.Hidden):
273
331
  continue
@@ -381,7 +439,7 @@ def _paramspecs_from_model(
381
439
  field,
382
440
  {
383
441
  "callback": functools.partial(
384
- update_callback,
442
+ add_state_field_callback,
385
443
  optname=modelname,
386
444
  key=config.item_key,
387
445
  ),
@@ -399,7 +457,7 @@ def _paramspecs_from_model(
399
457
  field,
400
458
  {
401
459
  "callback": functools.partial(
402
- update_callback,
460
+ add_state_field_callback,
403
461
  optname=modelname,
404
462
  remove=True,
405
463
  key=config.item_key,
@@ -447,15 +505,42 @@ def _paramspecs_from_model(
447
505
  )
448
506
 
449
507
 
508
+ def fieldname_and_options(*paramspec: ParamSpec) -> dict[str, str]:
509
+ """Return a mapping between model field name and corresponding CLI options."""
510
+ r: dict[str, str] = {}
511
+ for pspec in paramspec:
512
+ param = pspec.param
513
+ assert param.name
514
+ r[param.name] = ", ".join(param.opts)
515
+ return r
516
+
517
+
518
+ def format_error_message(error: ErrorDetails, *paramspec: ParamSpec) -> str:
519
+ """Format templated error message"""
520
+ try:
521
+ ctx_error = error["ctx"]["error"]
522
+ except KeyError:
523
+ return error["msg"]
524
+ if isinstance(ctx_error, MutuallyExclusiveError):
525
+ options = fieldname_and_options(*paramspec)
526
+ return ctx_error.format(options)
527
+ return str(ctx_error)
528
+
529
+
450
530
  @contextmanager
451
531
  def catch_validationerror(*paramspec: ParamSpec) -> Iterator[None]:
452
532
  try:
453
533
  yield None
454
534
  except pydantic.ValidationError as e:
455
535
  errors = e.errors()
536
+ for err in errors:
537
+ if not err.get("loc"):
538
+ raise click.UsageError(format_error_message(err, *paramspec)) from None
456
539
  for pspec in paramspec:
457
540
  for err in errors:
458
541
  if pspec.match_loc(err["loc"]):
459
- raise pspec.badparameter_exception(err["msg"]) from None
542
+ raise pspec.badparameter_exception(
543
+ format_error_message(err, *paramspec)
544
+ ) from None
460
545
  logger.debug("a validation error occurred", exc_info=True)
461
546
  raise click.ClickException(str(e)) from None
@@ -10,7 +10,7 @@ from typing import Any
10
10
  import click
11
11
  import pgtoolkit.conf
12
12
 
13
- from pglift import h, hook, instances, manager, postgresql
13
+ from pglift import instances, manager, postgresql
14
14
  from pglift.models import Instance, PostgreSQLInstance
15
15
  from pglift.types import ConfigChanges, Status
16
16
 
@@ -153,9 +153,8 @@ async def edit(obj: Obj, instance: Instance) -> None:
153
153
  """Edit managed configuration."""
154
154
  pg_instance = instance.postgresql
155
155
  with obj.lock, audit():
156
- actual_config = hook(
157
- instance._settings, h.postgresql_editable_conf, instance=pg_instance
158
- )
156
+ with manager.from_instance(pg_instance):
157
+ actual_config = await instances.postgresql_editable_conf(pg_instance)
159
158
  edited = click.edit(text=actual_config)
160
159
  if edited is None:
161
160
  click.echo("no change", err=True)
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
  from typing import Any
8
8
 
9
9
  import click
10
+ from pgtoolkit.hba import parse as parse_hba
10
11
 
11
12
  from pglift import diff, hba, manager
12
13
  from pglift.models import PostgreSQLInstance, interface
@@ -94,3 +95,22 @@ async def remove(
94
95
  if (diffvalue := diff.get()) is not None:
95
96
  for diffitem in diffvalue:
96
97
  console.print(diffitem)
98
+
99
+
100
+ @cli.command("edit")
101
+ @pass_postgresql_instance
102
+ @click.pass_obj
103
+ @async_command
104
+ async def edit(obj: Obj, instance: PostgreSQLInstance) -> None:
105
+ """Edit managed HBA records."""
106
+ with obj.lock, audit():
107
+ with manager.from_instance(instance):
108
+ hba_ = await hba.get(instance)
109
+ actual_hba = "\n".join([str(r) for r in hba_.lines])
110
+ edited = click.edit(text=actual_hba)
111
+ if edited is None:
112
+ click.echo("no change", err=True)
113
+ return
114
+ entries = parse_hba(edited.splitlines())
115
+ with manager.from_instance(instance):
116
+ await hba.save(instance, entries, reload_on_change=True)
@@ -170,7 +170,11 @@ async def ls(instance: PostgreSQLInstance, output_format: OutputFormat | None) -
170
170
  else:
171
171
  print_table_for(
172
172
  rls,
173
- partial(model_dump, mode="pretty", exclude={"hba_records", "validity"}),
173
+ partial(
174
+ model_dump,
175
+ context={"pretty": True},
176
+ exclude={"hba_records", "validity"},
177
+ ),
174
178
  )
175
179
 
176
180
 
@@ -191,7 +195,11 @@ async def get(
191
195
  else:
192
196
  print_table_for(
193
197
  [r],
194
- partial(model_dump, mode="pretty", exclude={"hba_records", "validity"}),
198
+ partial(
199
+ model_dump,
200
+ context={"pretty": True},
201
+ exclude={"hba_records", "validity"},
202
+ ),
195
203
  box=None,
196
204
  )
197
205
 
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  import abc
8
8
  import asyncio
9
+ import inspect
9
10
  import json
10
11
  import logging
11
12
  import logging.handlers
@@ -390,12 +391,14 @@ def _instance_identifier(
390
391
  callback=callback,
391
392
  shell_complete=_list_instances,
392
393
  )(fn)
393
- assert command.__doc__
394
- command.__doc__ += (
395
- "\n\n INSTANCE identifies target instance as <version>/<name> where the "
394
+ doc = inspect.getdoc(command)
395
+ assert doc
396
+ doc += (
397
+ "\n\nINSTANCE identifies target instance as <version>/<name> where the "
396
398
  "<version>/ prefix may be omitted if there is only one instance "
397
399
  "matching <name>."
398
400
  )
401
+ command.__doc__ = doc
399
402
  if not required:
400
403
  command.__doc__ += " Required if there is more than one instance on system."
401
404
  return command
@@ -167,7 +167,7 @@ Error cases for instance operations
167
167
  Usage: pglift instance create [OPTIONS] NAME
168
168
  Try 'pglift instance create --help' for help.
169
169
 
170
- Error: Invalid value for '--standby-for': Value error, missing "=" after "port" in connection info string
170
+ Error: Invalid value for '--standby-for': missing "=" after "port" in connection info string
171
171
 
172
172
  [2]
173
173
  $ pglift instance apply
@@ -197,11 +197,11 @@ Error cases for instance operations
197
197
 
198
198
  $ grep 'pglift_cli.audit' $TMPDIR/pglift-audit.log
199
199
  INFO - pglift_cli.audit - command: .*\/pglift --non-interactive site-configure install (re)
200
- INFO - pglift_cli.audit - command completed \(\d+.\d+ seconds\) (re)
200
+ INFO - pglift_cli.audit - command completed \(\d+(\.\d+)? seconds\) (re)
201
201
  INFO - pglift_cli.audit - command: .*\/pglift --non-interactive instance create main --data-checksums --auth-host=ident --port=\d+ --surole-password=s3per --pgbackrest-stanza=main '--pgbackrest-password=b@ck up!' --prometheus-port=\d+ (re)
202
- INFO - pglift_cli.audit - command completed \(\d+.\d+ seconds\) (re)
202
+ INFO - pglift_cli.audit - command completed \(\d+(\.\d+)? seconds\) (re)
203
203
  INFO - pglift_cli.audit - command: .*\/pglift --non-interactive instance create main --pgbackrest-stanza=st --surole-password=s3per (re)
204
- ERROR - pglift_cli.audit - command failed \(\d+.\d+ seconds\) (re)
204
+ ERROR - pglift_cli.audit - command failed \(\d+(\.\d+)? seconds\) (re)
205
205
 
206
206
  List instances
207
207
 
@@ -393,6 +393,9 @@ PostgreSQL configuration
393
393
 
394
394
  HBA configuration management:
395
395
 
396
+ $ version=$(pglift instance list -o json | jq -r '.[] | .version')
397
+ $ # Adding a comment in the file
398
+ $ printf "\n# a comment" >> $TMPDIR/srv/pgsql/${version}/main/data/pg_hba.conf
396
399
  $ pglift pghba add --user bob --method trust
397
400
  INFO entry added to HBA configuration
398
401
  $ pglift pghba add --dry-run --diff --user bob --method trust \
@@ -401,8 +404,8 @@ HBA configuration management:
401
404
  --- $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
402
405
  +++ $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
403
406
  @@ -*,3 +*,4 @@ (glob)
404
- host all all ::1/128 ident
405
407
  local adb arole trust
408
+ # a comment
406
409
  local all bob trust
407
410
  +host all bob 127.0.0.1 trust
408
411
  DRY RUN: no changes made
@@ -428,7 +431,6 @@ HBA configuration management:
428
431
  > --connection-address 192.168.12.10/32 \
429
432
  > --database mybd,myotherdb
430
433
  INFO entry added to HBA configuration
431
- $ version=$(pglift instance list -o json | jq -r '.[] | .version')
432
434
  $ grep 'bob' $TMPDIR/srv/pgsql/${version}/main/data/pg_hba.conf
433
435
  local all bob trust
434
436
  host all bob 127.0.0.1 trust
@@ -446,7 +448,7 @@ HBA configuration management:
446
448
  --- $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
447
449
  +++ $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
448
450
  @@ -*,5 +*,4 @@ (glob)
449
- local adb arole trust
451
+ # a comment
450
452
  local all bob trust
451
453
  host all bob 127.0.0.1 trust
452
454
  -host myotherdb bob 192.168.12.10 255.255.255.255 trust
@@ -463,7 +465,7 @@ HBA configuration management:
463
465
  --- $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
464
466
  +++ $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
465
467
  @@ -*,4 +*,3 @@ (glob)
466
- local adb arole trust
468
+ # a comment
467
469
  local all bob trust
468
470
  host all bob 127.0.0.1 trust
469
471
  -host mybd,myotherdb bob,peter 192.168.12.10/32 trust
@@ -531,6 +533,13 @@ Add and manipulate roles:
531
533
  INFO removing now empty $TMPDIR/.pgpass
532
534
  DRY RUN: no changes made
533
535
 
536
+ $ pglift role -i main create dba --password=qwerty --encrypted-password=md5azerty
537
+ Usage: pglift role create [OPTIONS] NAME
538
+ Try 'pglift role create --help' for help.
539
+
540
+ Error: '--password' and '--encrypted-password' can't be used together
541
+ [2]
542
+
534
543
  $ pglift role alter dba --connection-limit=10 --inherit --no-pgpass --no-login --revoke=pg_read_all_stats --grant=pg_monitor --valid-until=2026-01-01
535
544
  INFO altering role 'dba'
536
545
  INFO removing entry for 'dba' in \$TMPDIR\/.pgpass \(port=\d+\) (re)
@@ -598,7 +607,7 @@ Add and manipulate roles:
598
607
  --- $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
599
608
  +++ $TMPDIR/srv/pgsql/1*/main/data/pg_hba.conf (glob)
600
609
  \@\@ -\d+,3 \+\d+,7 \@\@ (re)
601
- local adb arole trust
610
+ # a comment
602
611
  local all bob trust
603
612
  host all bob 127.0.0.1 trust
604
613
  +local mydb test trust
@@ -1113,7 +1122,7 @@ Databases
1113
1122
  Usage: pglift role drop [OPTIONS] NAME
1114
1123
  Try 'pglift role drop --help' for help.
1115
1124
 
1116
- Error: Invalid value for '--reassign-owned': Value error, field is mutually exclusive with 'drop_owned'
1125
+ Error: '--drop-owned' and '--reassign-owned' can't be used together
1117
1126
  [2]
1118
1127
  $ pglift role drop test --drop-owned --dry-run
1119
1128
  INFO dropping role 'test'
@@ -1122,6 +1131,32 @@ Databases
1122
1131
  $ pglift role drop test --drop-owned
1123
1132
  INFO dropping role 'test'
1124
1133
  INFO removing entries from HBA configuration
1134
+ $ pglift role create doctor
1135
+ INFO creating role 'doctor'
1136
+ $ pglift database create tardis_db --owner doctor
1137
+ INFO creating 'tardis_db' database in 1\d/main (re)
1138
+ $ pglift database get tardis_db --output-format=json | jq -r .owner
1139
+ doctor
1140
+ $ pglift role drop doctor --reassign-owned=postgres
1141
+ INFO dropping role 'doctor'
1142
+ $ pglift database get tardis_db --output-format=json | jq -r .owner
1143
+ postgres
1144
+ $ pglift role create who
1145
+ INFO creating role 'who'
1146
+ $ pglift database create unit_db --owner who
1147
+ INFO creating 'unit_db' database in 1\d/main (re)
1148
+ $ pglift database get unit_db --output-format=json | jq -r .owner
1149
+ who
1150
+ $ pglift role drop who --drop-owned --reassign-owned=postgres
1151
+ Usage: pglift role drop [OPTIONS] NAME
1152
+ Try 'pglift role drop --help' for help.
1153
+
1154
+ Error: '--drop-owned' and '--reassign-owned' can't be used together
1155
+ [2]
1156
+ $ pglift role drop who --no-drop-owned --reassign-owned=postgres
1157
+ INFO dropping role 'who'
1158
+ $ pglift database get unit_db --output-format=json | jq -r .owner
1159
+ postgres
1125
1160
 
1126
1161
  Profiles
1127
1162
  $ pglift role -i main create dba1 --password mySup3rS3cr3t1377 --login
@@ -920,6 +920,7 @@ pg_hba.conf management commands
920
920
 
921
921
  Commands:
922
922
  add Add a record in HBA configuration.
923
+ edit Edit managed HBA records.
923
924
  remove Remove a record from HBA configuration.
924
925
  $ pglift pghba add --help
925
926
  Usage: pglift pghba add [OPTIONS]
@@ -967,6 +968,13 @@ pg_hba.conf management commands
967
968
  --diff Include differences resulting from applied
968
969
  changes in returned result.
969
970
  --help Show this message and exit.
971
+ $ pglift pghba edit --help
972
+ Usage: pglift pghba edit [OPTIONS]
973
+
974
+ Edit managed HBA records.
975
+
976
+ Options:
977
+ --help Show this message and exit.
970
978
 
971
979
  Patroni commands:
972
980
 
@@ -60,19 +60,19 @@ With custom ports
60
60
  Usage: pglift instance create [OPTIONS] NAME
61
61
  Try 'pglift instance create --help' for help.
62
62
 
63
- Error: Invalid value for '--prometheus-port': Value error, port \d+ already in use (re)
63
+ Error: Invalid value for '--prometheus-port': port \d+ already in use (re)
64
64
  [2]
65
65
  $ pglift instance create other --port=$PG1PORT --prometheus-port=$PGE2PORT
66
66
  Usage: pglift instance create [OPTIONS] NAME
67
67
  Try 'pglift instance create --help' for help.
68
68
 
69
- Error: Invalid value for '--port': Value error, port \d+ already in use (re)
69
+ Error: Invalid value for '--port': port \d+ already in use (re)
70
70
  [2]
71
71
  $ pglift instance create other --port=$PG1PORT --prometheus-port=$PGE1PORT
72
72
  Usage: pglift instance create [OPTIONS] NAME
73
73
  Try 'pglift instance create --help' for help.
74
74
 
75
- Error: Invalid value for '--prometheus-port': Value error, port \d+ already in use (re)
75
+ Error: Invalid value for '--prometheus-port': port \d+ already in use (re)
76
76
  [2]
77
77
  $ pglift instance drop main
78
78
  INFO dropping instance 1\d\/main (re)
@@ -102,13 +102,13 @@ With a port set in postgresql.conf template
102
102
  Usage: pglift instance create [OPTIONS] NAME
103
103
  Try 'pglift instance create --help' for help.
104
104
 
105
- Error: Invalid value for '--port': Value error, port \d+ already in use (re)
105
+ Error: Invalid value for '--port': port \d+ already in use (re)
106
106
  [2]
107
107
  $ pglift instance create other --port=$PG2PORT --prometheus-port=$PGE1PORT
108
108
  Usage: pglift instance create [OPTIONS] NAME
109
109
  Try 'pglift instance create --help' for help.
110
110
 
111
- Error: Invalid value for '--prometheus-port': Value error, port \d+ already in use (re)
111
+ Error: Invalid value for '--prometheus-port': port \d+ already in use (re)
112
112
  [2]
113
113
 
114
114
  $ pglift instance drop main
@@ -134,7 +134,7 @@ With default ports
134
134
  Usage: pglift instance create [OPTIONS] NAME
135
135
  Try 'pglift instance create --help' for help.
136
136
 
137
- Error: Invalid value for '--port': Value error, port 5432 already in use
137
+ Error: Invalid value for '--port': port 5432 already in use
138
138
  [2]
139
139
 
140
140
  $ pglift instance drop main
@@ -91,7 +91,7 @@ Check port conflicts
91
91
  Usage: pglift postgres_exporter install [OPTIONS] NAME DSN PORT
92
92
  Try 'pglift postgres_exporter install --help' for help.
93
93
 
94
- Error: Invalid value for 'PORT': Value error, port \d+ already in use (re)
94
+ Error: Invalid value for 'PORT': port \d+ already in use (re)
95
95
  [2]
96
96
 
97
97
  $ pglift postgres_exporter uninstall test
@@ -146,7 +146,7 @@ Cannot create a standby with --slot option
146
146
  Usage: pglift instance create [OPTIONS] NAME
147
147
  Try 'pglift instance create --help' for help.
148
148
 
149
- Error: Invalid value for '--slot': Value error, replication slots cannot be set on a standby instance
149
+ Error: Invalid value for '--slot': replication slots cannot be set on a standby instance
150
150
  [2]
151
151
 
152
152
 
@@ -187,7 +187,7 @@ Try to create primary instance with same stanza
187
187
  Usage: pglift instance create [OPTIONS] NAME
188
188
  Try 'pglift instance create --help' for help.
189
189
 
190
- Error: Invalid value for '--pgbackrest-stanza': Value error, Stanza 'app' already bound to another instance \(datadir=\$TMPDIR\/1\/srv\/pgsql\/1\d\/pg1\/data\) (re)
190
+ Error: Invalid value for '--pgbackrest-stanza': Stanza 'app' already bound to another instance \(datadir=\$TMPDIR\/1\/srv\/pgsql\/1\d\/pg1\/data\) (re)
191
191
  [2]
192
192
 
193
193
  Add some data to the primary, check replication
@@ -54,16 +54,25 @@ Try to create an instance with a non-existing encoding, triggering a failure in
54
54
  [1]
55
55
 
56
56
  Try to create a database with a non-existing tablespace
57
- (XXX the final error message is wrong, probably a bug here)
58
57
 
59
58
  $ pglift -Linfo database create db --tablespace=nosuchtbspc
60
59
  INFO starting PostgreSQL 1\d\/main (re)
61
60
  INFO creating 'db' database in 1\d\/main (re)
62
61
  WARNING tablespace "nosuchtbspc" does not exist
63
62
  WARNING reverting: creating 'db' database in 1\d\/main (re)
64
- INFO dropping 'db' database
65
63
  INFO stopping PostgreSQL 1\d\/main (re)
66
- Error: database "db" does not exist
64
+ Error: tablespace "nosuchtbspc" does not exist
65
+ [1]
66
+
67
+ Try to create a database with a non-existing owner
68
+
69
+ $ pglift -Linfo database create db --owner=nosuchowner
70
+ INFO starting PostgreSQL 1\d\/main (re)
71
+ INFO creating 'db' database in 1\d\/main (re)
72
+ WARNING role "nosuchowner" does not exist
73
+ WARNING reverting: creating 'db' database in 1\d\/main (re)
74
+ INFO stopping PostgreSQL 1\d\/main (re)
75
+ Error: role "nosuchowner" does not exist
67
76
  [1]
68
77
 
69
78
  (cleanup)
@@ -36,7 +36,7 @@ def test_audit(tmp_path: Path) -> None:
36
36
  assert logf.read().splitlines() == [
37
37
  "INFO:pglift_cli.audit command: test error",
38
38
  "ERROR:pglift oups",
39
- "ERROR:pglift_cli.audit command failed (0.00 seconds)",
39
+ "ERROR:pglift_cli.audit command failed (0 seconds)",
40
40
  ]
41
41
 
42
42
  with audit(["test", "ok"], settings, dry_run=True):
@@ -44,7 +44,7 @@ def test_audit(tmp_path: Path) -> None:
44
44
  assert logf.read().splitlines() == [
45
45
  "INFO:pglift_cli.audit command: test ok (DRY RUN)",
46
46
  "DEBUG:pglift running",
47
- "INFO:pglift_cli.audit command completed (0.00 seconds)",
47
+ "INFO:pglift_cli.audit command completed (0 seconds)",
48
48
  ]
49
49
 
50
50
  with pytest.raises(exceptions.Cancelled):
@@ -54,5 +54,5 @@ def test_audit(tmp_path: Path) -> None:
54
54
  assert logf.read().splitlines() == [
55
55
  "INFO:pglift_cli.audit command: test cancel",
56
56
  "INFO:pglift trying",
57
- "WARNING:pglift_cli.audit command cancelled (0.00 seconds)",
57
+ "WARNING:pglift_cli.audit command cancelled (0 seconds)",
58
58
  ]
@@ -16,6 +16,8 @@ import pytest
16
16
  import yaml
17
17
  from click.shell_completion import ShellComplete
18
18
  from click.testing import CliRunner
19
+ from pgtoolkit.hba import HBARecord
20
+ from pgtoolkit.hba import parse as parse_hba
19
21
  from rich.console import ConsoleDimensions
20
22
 
21
23
  from pglift import instances, postgresql
@@ -499,6 +501,31 @@ def test_pgconf_edit(
499
501
  assert result.stderr == "bonjour: on -> 'matin\n"
500
502
 
501
503
 
504
+ @pytest.mark.usefixtures("installed")
505
+ def test_pghba_edit(runner: CliRunner, obj: Obj, instance: Instance) -> None:
506
+ hba_r = HBARecord(
507
+ conntype="local", user="santaclaus", database="list", method="md5"
508
+ )
509
+ hba_f = postgresql.hba_path(instance.postgresql)
510
+ assert hba_r not in parse_hba(hba_f)
511
+ with (
512
+ patch("click.edit", return_value=str(hba_r), autospec=True),
513
+ # Unfortunately we patch postgresql.is_running(), because we can't
514
+ # inject a custom (fake) instance manager here. Indeed pytest and the
515
+ # pglift CLI do not share the same event loop. Since the instance
516
+ # manager is passed via a ContextVar, and ContextVars are local to their
517
+ # event loop, it is not accessible across that "boundary".
518
+ patch.object(postgresql, "is_running", return_value=False),
519
+ ):
520
+ result = runner.invoke(
521
+ cli,
522
+ ["pghba", f"--instance={instance}", "edit"],
523
+ obj=obj,
524
+ )
525
+ assert result.exit_code == 0, result.stderr
526
+ assert hba_r in parse_hba(hba_f)
527
+
528
+
502
529
  @pytest.mark.usefixtures("installed")
503
530
  def test_pgconf_edit_no_change(
504
531
  runner: CliRunner, obj: Obj, instance: Instance, postgresql_conf: str
@@ -87,6 +87,7 @@ def test_as_parameters(runner: CliRunner) -> None:
87
87
  " --address-coords-lat LAT Latitude.\n"
88
88
  " --birth-date DATE Date of birth. [required]\n"
89
89
  " --birth-place PLACE Place of birth.\n"
90
+ " --is-dead / --no-is-dead Is dead.\n"
90
91
  " --phone-numbers PHONE_NUMBERS Phone numbers. (Can be used multiple times.)\n"
91
92
  " --pet PET Owned pets. (Can be used multiple times.)\n"
92
93
  " --member-of GROUP Groups the person is a member of. (Can be used\n"
@@ -128,6 +129,7 @@ def test_as_parameters(runner: CliRunner) -> None:
128
129
  },
129
130
  "age": 42,
130
131
  "birth": {"date": "1981-02-18"},
132
+ "is_dead": False,
131
133
  "name": "alice",
132
134
  "nickname": "**********",
133
135
  "relation": "friend",
@@ -151,6 +153,21 @@ def test_as_parameters(runner: CliRunner) -> None:
151
153
  in result.stderr
152
154
  )
153
155
 
156
+ result = runner.invoke(
157
+ add_person,
158
+ [
159
+ "foo",
160
+ "friend",
161
+ "--age=17",
162
+ "--birth-date=1987-06-05",
163
+ "--nickname=aaa",
164
+ "--is-dead",
165
+ ],
166
+ )
167
+ assert result.exit_code == 2
168
+ assert "--is-dead' and '--age' can't be used together" in result.stderr
169
+ assert "For further information visit" not in result.stderr
170
+
154
171
 
155
172
  def test_as_parameters_update() -> None:
156
173
  @click.command("update-person")
@@ -179,6 +196,7 @@ def test_as_parameters_update() -> None:
179
196
  " --address-coords-long LONG Longitude.\n"
180
197
  " --address-coords-lat LAT Latitude.\n"
181
198
  " --birth-date DATE Date of birth. [required]\n"
199
+ " --is-dead / --no-is-dead Is dead.\n"
182
200
  " --add-pet PET Add pet. (Can be used multiple times.)\n"
183
201
  " --remove-pet PET Remove pet. (Can be used multiple times.)\n"
184
202
  " --add-to GROUP Add to group. (Can be used multiple times.)\n"
File without changes
File without changes
File without changes
File without changes
File without changes