pglift-cli 2.0.0__tar.gz → 2.1.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.0.0 → pglift_cli-2.1.0}/PKG-INFO +2 -2
  2. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/pyproject.toml +1 -1
  3. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/database.py +32 -14
  4. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/instance.py +39 -4
  5. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/pgbackrest/__init__.py +4 -4
  6. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/role.py +24 -15
  7. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/expect/test-cli-walkthrough.t +93 -3
  8. pglift_cli-2.1.0/tests/expect/test-demote.t +231 -0
  9. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/expect/test-help.t +32 -0
  10. pglift_cli-2.1.0/tests/expect/test-transactions.t +70 -0
  11. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/unit/test__site.py +9 -13
  12. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/unit/test_cli.py +13 -17
  13. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/unit/test_main.py +7 -9
  14. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/.gitignore +0 -0
  15. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/README.md +0 -0
  16. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/hatch.toml +0 -0
  17. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/pytest.ini +0 -0
  18. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/__init__.py +0 -0
  19. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/__main__.py +0 -0
  20. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/_settings.py +0 -0
  21. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/_site.py +0 -0
  22. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/base.py +0 -0
  23. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/console.py +0 -0
  24. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/hookspecs.py +0 -0
  25. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/main.py +0 -0
  26. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/model.py +0 -0
  27. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/patroni.py +0 -0
  28. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/pgbackrest/repo_path.py +0 -0
  29. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/pgconf.py +0 -0
  30. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/pghba.py +0 -0
  31. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/pm.py +0 -0
  32. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/postgres.py +0 -0
  33. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/prometheus.py +0 -0
  34. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/py.typed +0 -0
  35. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/src/pglift_cli/util.py +0 -0
  36. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/expect/.gitignore +0 -0
  37. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/expect/test-base.t +0 -0
  38. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/expect/test-port-validation.t +0 -0
  39. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/expect/test-prometheus.t +0 -0
  40. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/expect/test-standby-pgbackrest.t +0 -0
  41. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/expect/test-upgrade.t +0 -0
  42. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/unit/__init__.py +0 -0
  43. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/unit/conftest.py +0 -0
  44. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/unit/test_audit.py +0 -0
  45. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/unit/test_model.py +0 -0
  46. {pglift_cli-2.0.0 → pglift_cli-2.1.0}/tests/unit/test_pm.py +0 -0
  47. {pglift_cli-2.0.0 → pglift_cli-2.1.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.0.0
3
+ Version: 2.1.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/
@@ -23,7 +23,7 @@ Classifier: Topic :: Database
23
23
  Classifier: Topic :: System :: Systems Administration
24
24
  Classifier: Typing :: Typed
25
25
  Requires-Python: <4,>=3.10
26
- Requires-Dist: click!=8.1.0,!=8.1.4,>=8.0.0
26
+ Requires-Dist: click!=8.1.0,!=8.1.4,<8.2.0,>=8.0.0
27
27
  Requires-Dist: filelock!=3.12.1,>=3.9.0
28
28
  Requires-Dist: pluggy
29
29
  Requires-Dist: psycopg>=3.1
@@ -39,7 +39,7 @@ dynamic = ["version"]
39
39
 
40
40
  dependencies = [
41
41
  "PyYAML >= 6.0.1",
42
- "click >= 8.0.0, != 8.1.0, != 8.1.4",
42
+ "click >= 8.0.0, != 8.1.0, != 8.1.4, < 8.2.0",
43
43
  "filelock >= 3.9.0, != 3.12.1",
44
44
  "pluggy",
45
45
  "psycopg >= 3.1",
@@ -37,6 +37,7 @@ from .util import (
37
37
  print_result_diff,
38
38
  print_schema,
39
39
  print_table_for,
40
+ system_configure,
40
41
  )
41
42
 
42
43
 
@@ -65,14 +66,18 @@ def cli(**kwargs: Any) -> None:
65
66
 
66
67
  @cli.command("create")
67
68
  @model.as_parameters(interface.Database, "create")
69
+ @dry_run_option
68
70
  @pass_postgresql_instance
69
71
  @click.pass_obj
70
72
  @async_command
71
73
  async def create(
72
- obj: Obj, instance: PostgreSQLInstance, database: interface.Database
74
+ obj: Obj,
75
+ instance: PostgreSQLInstance,
76
+ database: interface.Database,
77
+ dry_run: bool,
73
78
  ) -> None:
74
79
  """Create a database in a PostgreSQL instance"""
75
- with obj.lock, audit():
80
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
76
81
  async with postgresql.running(instance):
77
82
  if await databases.exists(instance, database.name):
78
83
  raise click.ClickException("database already exists")
@@ -83,14 +88,19 @@ async def create(
83
88
  @cli.command("alter")
84
89
  @model.as_parameters(interface.Database, "update")
85
90
  @click.argument("dbname")
91
+ @dry_run_option
86
92
  @pass_postgresql_instance
87
93
  @click.pass_obj # type: ignore[arg-type]
88
94
  @async_command
89
95
  async def alter(
90
- obj: Obj, instance: PostgreSQLInstance, dbname: str, **changes: Any
96
+ obj: Obj,
97
+ instance: PostgreSQLInstance,
98
+ dbname: str,
99
+ dry_run: bool,
100
+ **changes: Any,
91
101
  ) -> None:
92
102
  """Alter a database in a PostgreSQL instance"""
93
- with obj.lock, audit():
103
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
94
104
  async with postgresql.running(instance):
95
105
  values = (await databases.get(instance, dbname)).model_dump(by_alias=True)
96
106
  values = deep_update(values, changes)
@@ -117,12 +127,14 @@ async def apply(
117
127
  ) -> None:
118
128
  """Apply manifest as a database"""
119
129
  database = interface.Database.model_validate(data)
120
- if dry_run:
121
- ret = interface.ApplyResult(change_state=None)
122
- else:
123
- with obj.lock, audit(dry_run=dry_run), diff.enabled(diff_format):
124
- async with postgresql.running(instance):
125
- ret = await databases.apply(instance, database)
130
+ with (
131
+ obj.lock,
132
+ audit(dry_run=dry_run),
133
+ diff.enabled(diff_format),
134
+ system_configure(dry_run=dry_run),
135
+ ):
136
+ async with postgresql.running(instance):
137
+ ret = await databases.apply(instance, database)
126
138
  if output_format == "json":
127
139
  print_json_for(ret)
128
140
  else:
@@ -181,6 +193,7 @@ async def ls(
181
193
 
182
194
  @cli.command("drop")
183
195
  @model.as_parameters(interface.DatabaseDropped, "create")
196
+ @dry_run_option
184
197
  @pass_postgresql_instance
185
198
  @click.pass_obj
186
199
  @async_command
@@ -188,9 +201,10 @@ async def drop(
188
201
  obj: Obj,
189
202
  instance: PostgreSQLInstance,
190
203
  databasedropped: interface.DatabaseDropped,
204
+ dry_run: bool,
191
205
  ) -> None:
192
206
  """Drop a database"""
193
- with obj.lock, audit():
207
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
194
208
  async with postgresql.running(instance):
195
209
  await databases.drop(instance, databasedropped)
196
210
 
@@ -274,11 +288,15 @@ async def run(
274
288
  )
275
289
  @click.argument("dbname")
276
290
  @pass_postgresql_instance
291
+ @dry_run_option
277
292
  @async_command
278
- async def dump(instance: PostgreSQLInstance, dbname: str, output: Path | None) -> None:
293
+ async def dump(
294
+ instance: PostgreSQLInstance, dbname: str, output: Path | None, dry_run: bool
295
+ ) -> None:
279
296
  """Dump a database"""
280
- async with postgresql.running(instance):
281
- await databases.dump(instance, dbname, output)
297
+ with system_configure(dry_run=dry_run):
298
+ async with postgresql.running(instance):
299
+ await databases.dump(instance, dbname, output)
282
300
 
283
301
 
284
302
  @cli.command("dumps")
@@ -114,8 +114,8 @@ async def create(obj: Obj, instance: interface.Instance, drop_on_error: bool) ->
114
114
  with obj.lock, audit():
115
115
  if instances.exists(instance.name, instance.version, _site.SETTINGS):
116
116
  raise click.ClickException("instance already exists")
117
- async with task.async_transaction(drop_on_error):
118
- with manager.from_manifest(instance, settings=_site.SETTINGS):
117
+ with manager.from_manifest(instance, settings=_site.SETTINGS):
118
+ async with task.async_transaction(drop_on_error):
119
119
  await instances.apply(_site.SETTINGS, instance)
120
120
 
121
121
 
@@ -213,6 +213,41 @@ async def promote(obj: Obj, instance: Instance) -> None:
213
213
  await instances.promote(instance)
214
214
 
215
215
 
216
+ @cli.command("demote")
217
+ @instance_identifier(nargs=1)
218
+ @model.as_parameters(postgresql.RewindSource, "create")
219
+ @click.option(
220
+ "--start/--no-start",
221
+ help="Start the instance at the end of the demotion process",
222
+ default=True,
223
+ show_default=True,
224
+ )
225
+ @click.argument("rewind_opts", nargs=-1, type=click.UNPROCESSED)
226
+ @click.pass_obj
227
+ @async_command
228
+ async def demote(
229
+ obj: Obj,
230
+ instance: Instance,
231
+ rewindsource: postgresql.RewindSource,
232
+ start: bool,
233
+ rewind_opts: tuple[str, ...],
234
+ ) -> None:
235
+ """Demote PostgreSQL INSTANCE as standby of specified source server using pg_rewind.
236
+
237
+ The instance must not be running and it may be started at the end of the
238
+ "demotion" process.
239
+
240
+ Extra options can be passed to the pg_rewind command. They may need to
241
+ be prefixed with -- to separate them from the current command options
242
+ when confusion arises. When using extra options, providing the instance
243
+ identifier is required.
244
+ """
245
+ with obj.lock, audit(), manager.from_instance(instance.postgresql):
246
+ await instances.demote(instance, rewindsource, rewind_opts=rewind_opts)
247
+ if start:
248
+ await instances.start(instance)
249
+
250
+
216
251
  @cli.command("get")
217
252
  @output_format_option
218
253
  @instance_identifier(nargs=1)
@@ -519,8 +554,8 @@ async def upgrade(
519
554
  """
520
555
  with obj.lock, audit():
521
556
  await postgresql.check_status(instance.postgresql, Status.not_running)
522
- async with task.async_transaction():
523
- with manager.from_instance(instance.postgresql):
557
+ with manager.from_instance(instance.postgresql):
558
+ async with task.async_transaction():
524
559
  new_instance = await instances.upgrade(
525
560
  instance,
526
561
  version=newversion,
@@ -12,7 +12,7 @@ import click
12
12
 
13
13
  from pglift import pgbackrest, postgresql, types
14
14
  from pglift.models import Instance
15
- from pglift.pgbackrest import base, models
15
+ from pglift.pgbackrest import models
16
16
  from pglift.pgbackrest import register_if as register_if
17
17
 
18
18
  from .. import _site, hookimpl
@@ -46,7 +46,7 @@ def pgbackrest_proxy(
46
46
  """Proxy to pgbackrest operations on an instance"""
47
47
  s = context.obj.instance.service(models.Service)
48
48
  settings = pgbackrest.get_settings(_site.SETTINGS)
49
- cmd_args = base.make_cmd(s.stanza, settings, *command)
49
+ cmd_args = pgbackrest.make_cmd(s.stanza, settings, *command)
50
50
  try:
51
51
  subprocess.run(cmd_args, capture_output=False, check=True) # nosec
52
52
  except subprocess.CalledProcessError as e:
@@ -70,7 +70,7 @@ async def instance_restore(
70
70
  ) from None
71
71
  settings = pgbackrest.get_settings(_site.SETTINGS)
72
72
  with obj.lock, audit():
73
- await base.restore(instance, settings, label=label, date=date)
73
+ await pgbackrest.restore(instance, settings, label=label, date=date)
74
74
 
75
75
 
76
76
  @click.command("backups", cls=Command)
@@ -82,7 +82,7 @@ async def instance_backups(
82
82
  ) -> None:
83
83
  """List available backups for INSTANCE"""
84
84
  settings = pgbackrest.get_settings(_site.SETTINGS)
85
- backups = [b async for b in base.iter_backups(instance, settings)]
85
+ backups = [b async for b in pgbackrest.iter_backups(instance, settings)]
86
86
  if output_format == "json":
87
87
  print_json_for([model_dump(b) for b in backups])
88
88
  else:
@@ -35,6 +35,7 @@ from .util import (
35
35
  print_result_diff,
36
36
  print_schema,
37
37
  print_table_for,
38
+ system_configure,
38
39
  )
39
40
 
40
41
 
@@ -76,11 +77,14 @@ def cli(**kwargs: Any) -> None:
76
77
  @cli.command("create")
77
78
  @model.as_parameters(_site.ROLE_MODEL, "create")
78
79
  @pass_postgresql_instance
80
+ @dry_run_option
79
81
  @click.pass_obj
80
82
  @async_command
81
- async def create(obj: Obj, instance: PostgreSQLInstance, role: interface.Role) -> None:
83
+ async def create(
84
+ obj: Obj, instance: PostgreSQLInstance, role: interface.Role, dry_run: bool
85
+ ) -> None:
82
86
  """Create a role in a PostgreSQL instance"""
83
- with obj.lock, audit():
87
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
84
88
  async with postgresql.running(instance):
85
89
  if await roles.exists(instance, role.name):
86
90
  raise click.ClickException("role already exists")
@@ -92,13 +96,14 @@ async def create(obj: Obj, instance: PostgreSQLInstance, role: interface.Role) -
92
96
  @model.as_parameters(_site.ROLE_MODEL, "update")
93
97
  @click.argument("rolname")
94
98
  @pass_postgresql_instance
99
+ @dry_run_option
95
100
  @click.pass_obj # type: ignore[arg-type]
96
101
  @async_command
97
102
  async def alter(
98
- obj: Obj, instance: PostgreSQLInstance, rolname: str, **changes: Any
103
+ obj: Obj, instance: PostgreSQLInstance, rolname: str, dry_run: bool, **changes: Any
99
104
  ) -> None:
100
105
  """Alter a role in a PostgreSQL instance"""
101
- with obj.lock, audit():
106
+ with obj.lock, audit(dry_run=dry_run), system_configure(dry_run=dry_run):
102
107
  async with postgresql.running(instance):
103
108
  with manager.from_instance(instance):
104
109
  values = model_dump(await roles.get(instance, rolname))
@@ -136,13 +141,15 @@ async def apply(
136
141
  )
137
142
  with validation_context(operation=op, settings=_site.SETTINGS):
138
143
  role = _site.ROLE_MODEL.model_validate(data)
139
- if dry_run:
140
- ret = interface.ApplyResult(change_state=None)
141
- else:
142
- with obj.lock, audit(dry_run=dry_run), diff.enabled(diff_format):
143
- async with postgresql.running(instance):
144
- with manager.from_instance(instance):
145
- ret = await roles.apply(instance, role)
144
+ with (
145
+ obj.lock,
146
+ audit(dry_run=dry_run),
147
+ diff.enabled(diff_format),
148
+ system_configure(dry_run=dry_run),
149
+ ):
150
+ async with postgresql.running(instance):
151
+ with manager.from_instance(instance):
152
+ ret = await roles.apply(instance, role)
146
153
  if output_format == "json":
147
154
  print_json_for(ret)
148
155
  else:
@@ -192,14 +199,16 @@ async def get(
192
199
  @cli.command("drop")
193
200
  @model.as_parameters(interface.RoleDropped, "create")
194
201
  @pass_postgresql_instance
202
+ @dry_run_option
195
203
  @async_command
196
204
  async def drop(
197
- instance: PostgreSQLInstance, roledropped: interface.RoleDropped
205
+ instance: PostgreSQLInstance, roledropped: interface.RoleDropped, dry_run: bool
198
206
  ) -> None:
199
207
  """Drop a role"""
200
- async with postgresql.running(instance):
201
- with manager.from_instance(instance):
202
- await roles.drop(instance, roledropped)
208
+ with audit(dry_run=dry_run), system_configure(dry_run=dry_run):
209
+ async with postgresql.running(instance):
210
+ with manager.from_instance(instance):
211
+ await roles.drop(instance, roledropped)
203
212
 
204
213
 
205
214
  @cli.command("privileges")
@@ -218,7 +218,6 @@ Get the status of an instance:
218
218
  DEBUG debug logging at \$TMPDIR/pglift-\w+-\d+.\d+.log (re)
219
219
  DEBUG looking for 'postgres_exporter@1\d-main' service status by its PID at \$TMPDIR\/run\/prometheus\/1\d-main.pid (re)
220
220
  DEBUG get status of PostgreSQL instance 1\d\/main (re)
221
- DEBUG \/usr\/.+1\d\/bin\/pg_ctl --version (re)
222
221
  DEBUG \/usr\/.+1\d\/bin\/pg_ctl status -D \$TMPDIR\/srv\/pgsql\/1\d\/main\/data (re)
223
222
 
224
223
  Reload, restart:
@@ -347,6 +346,7 @@ PostgreSQL configuration
347
346
  Error: Invalid value for '<PARAMETER>=<VALUE>...': invalid
348
347
  [2]
349
348
  $ pglift --non-interactive pgconf set --dry-run log_statement=ddl
349
+ WARNING failed to get data_checksums status: 'Data page checksum version' not found in controldata
350
350
  INFO configuring PostgreSQL
351
351
  INFO instance 1\d\/main needs reload due to parameter changes: log_statement (re)
352
352
  INFO reloading PostgreSQL configuration for 1\d\/main (re)
@@ -368,6 +368,7 @@ PostgreSQL configuration
368
368
  Error: 'fsync' not found in managed configuration
369
369
  [1]
370
370
  $ pglift --non-interactive pgconf remove --dry-run log_statement
371
+ WARNING failed to get data_checksums status: 'Data page checksum version' not found in controldata
371
372
  INFO configuring PostgreSQL
372
373
  INFO instance 1\d\/main needs reload due to parameter changes: log_statement (re)
373
374
  INFO reloading PostgreSQL configuration for 1\d\/main (re)
@@ -380,6 +381,14 @@ PostgreSQL configuration
380
381
  log_statement: ddl -> None
381
382
  $ pglift pgconf show log_statement
382
383
  # log_statement = 'ddl'
384
+ $ pglift --non-interactive pgconf set port=1234 --dry-run
385
+ WARNING failed to get data_checksums status: 'Data page checksum version' not found in controldata
386
+ INFO configuring PostgreSQL
387
+ INFO reconfiguring Prometheus postgres_exporter 1\d-main (re)
388
+ INFO restarting Prometheus postgres_exporter 1\d-main (re)
389
+ INFO configuring pgBackRest stanza 'main' for pg1-path=\$TMPDIR\/srv\/pgsql\/1\d\/main\/data (re)
390
+ port: \d+ -> 1234 (re)
391
+ DRY RUN: no changes made
383
392
 
384
393
  pg_hba.conf management:
385
394
 
@@ -478,6 +487,11 @@ Roles
478
487
 
479
488
  Add and manipulate roles:
480
489
 
490
+ $ pglift role -i main create dba --login --pgpass --password=qwerty --in-role=pg_read_all_stats --dry-run
491
+ INFO creating role 'dba'
492
+ INFO adding an entry for 'dba' in \$TMPDIR\/.pgpass \(port=\d+\) (re)
493
+ DRY RUN: no changes made
494
+
481
495
  $ pglift role -i main create dba --login --pgpass --password=qwerty --in-role=pg_read_all_stats
482
496
  INFO creating role 'dba'
483
497
  INFO adding an entry for 'dba' in \$TMPDIR\/.pgpass \(port=\d+\) (re)
@@ -489,6 +503,10 @@ Add and manipulate roles:
489
503
  name has_\xe2\x80\xa6 inhe\xe2\x80\xa6 login supe\xe2\x80\xa6 crea\xe2\x80\xa6 crea\xe2\x80\xa6 rep\xe2\x80\xa6 conn\xe2\x80\xa6 val\xe2\x80\xa6 memb\xe2\x80\xa6 pgp\xe2\x80\xa6 (esc)
490
504
  dba True True True False False False Fal\xe2\x80\xa6 pg_r\xe2\x80\xa6 True (esc)
491
505
 
506
+ $ pglift role -i main create dba --dry-run
507
+ Error: role already exists
508
+ [1]
509
+
492
510
  $ pglift role -i main create dba
493
511
  Error: role already exists
494
512
  [1]
@@ -500,6 +518,11 @@ Add and manipulate roles:
500
518
  dba
501
519
  (1 row)
502
520
 
521
+ $ 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 --dry-run
522
+ INFO altering role 'dba'
523
+ INFO removing entry for 'dba' in \$TMPDIR\/.pgpass \(port=\d+\) (re)
524
+ INFO removing now empty $TMPDIR/.pgpass
525
+ DRY RUN: no changes made
503
526
 
504
527
  $ 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
505
528
  INFO altering role 'dba'
@@ -551,6 +574,16 @@ Add and manipulate roles:
551
574
  > database: otherdb
552
575
  > method: trust
553
576
  > EOF
577
+ $ pglift role apply -f $TMPDIR/role.yaml --dry-run
578
+ INFO creating role 'test'
579
+ INFO pg_hba.conf updated
580
+ INFO reloading PostgreSQL configuration for 1\d\/main (re)
581
+ DRY RUN: no changes made
582
+ $ grep 'test' $TMPDIR/srv/pgsql/${version}/main/data/pg_hba.conf
583
+ [1]
584
+ $ pglift role get test
585
+ Error: role 'test' not found
586
+ [1]
554
587
  $ pglift role apply -f $TMPDIR/role.yaml --diff
555
588
  INFO creating role 'test'
556
589
  INFO pg_hba.conf updated
@@ -760,20 +793,40 @@ Add and manipulate roles:
760
793
 
761
794
  Databases
762
795
 
796
+ $ pglift database create test --dry-run
797
+ INFO creating 'test' database in 1\d\/main (re)
798
+ DRY RUN: no changes made
763
799
  $ pglift database create test --owner test
764
800
  INFO creating 'test' database in 1\d\/main (re)
765
801
  $ pglift database create myapp --owner dba --schema app
766
802
  INFO creating 'myapp' database in 1\d\/main (re)
767
803
  INFO creating schema 'app' in database myapp with owner 'dba'
768
804
 
769
- $ pglift database create other
770
- INFO creating 'other' database in 1\d/main (re)
771
805
  $ cat > $TMPDIR/other.yaml <<EOF
772
806
  > name: other
773
807
  > extensions:
774
808
  > - name: pg_stat_statements
775
809
  > EOF
776
810
  $ pglift database apply -f $TMPDIR/other.yaml --dry-run
811
+ INFO creating 'other' database in 1\d/main (re)
812
+ INFO creating extension 'pg_stat_statements' in database other
813
+ DRY RUN: no changes made
814
+ $ pglift database create other
815
+ INFO creating 'other' database in 1\d/main (re)
816
+ $ pglift database apply -f $TMPDIR/other.yaml --dry-run
817
+ INFO altering 'other' database on instance 1\d\/main (re)
818
+ INFO creating extension 'pg_stat_statements' in database other
819
+ DRY RUN: no changes made
820
+ $ pglift database run -d other "SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements')"
821
+ INFO running "SELECT EXISTS \(SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'\)" on other database of 1\d\/main (re)
822
+ INFO SELECT 1
823
+ Database
824
+ other
825
+ ┏━━━━━━━━┓
826
+ ┃ exists ┃
827
+ ┡━━━━━━━━┩
828
+ │ False │
829
+ └────────┘
777
830
  $ pglift database apply -f $TMPDIR/other.yaml -o json --diff
778
831
  INFO altering 'other' database on instance 1\d\/main (re)
779
832
  INFO creating extension 'pg_stat_statements' in database other
@@ -781,6 +834,26 @@ Databases
781
834
  "change_state": "changed",
782
835
  "diff": []
783
836
  }
837
+ $ pglift database run -d other "SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements')"
838
+ INFO running "SELECT EXISTS \(SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'\)" on other database of 1\d\/main (re)
839
+ INFO SELECT 1
840
+ Database
841
+ other
842
+ ┏━━━━━━━━┓
843
+ ┃ exists ┃
844
+ ┡━━━━━━━━┩
845
+ │ True │
846
+ └────────┘
847
+ $ cat > $TMPDIR/other.yaml <<EOF
848
+ > name: other
849
+ > state: absent
850
+ > EOF
851
+ $ pglift database apply -f $TMPDIR/other.yaml --dry-run
852
+ INFO dropping 'other' database
853
+ DRY RUN: no changes made
854
+ $ pglift database drop other --dry-run
855
+ INFO dropping 'other' database
856
+ DRY RUN: no changes made
784
857
  $ pglift database get other
785
858
  name owner settin… schemas extensi… locale public… subscri… tables… (esc)
786
859
  other postgr… public pg_stat… C pg_def… (esc)
@@ -788,6 +861,12 @@ Databases
788
861
  Error: database already exists
789
862
  [1]
790
863
 
864
+ $ pglift database alter other --owner=test --add-extension=unaccent --add-schema=myschema --dry-run
865
+ INFO altering 'other' database on instance 1\d\/main (re)
866
+ INFO creating schema 'myschema' in database other with owner 'test'
867
+ INFO creating extension 'unaccent' in database other
868
+ DRY RUN: no changes made
869
+
791
870
  $ pglift database alter other --owner=test --add-extension=unaccent --add-schema=myschema
792
871
  INFO altering 'other' database on instance 1\d\/main (re)
793
872
  INFO creating schema 'myschema' in database other with owner 'test'
@@ -933,6 +1012,9 @@ Databases
933
1012
  INFO backing up database 'nosuchdb' on instance 1\d\/main (re)
934
1013
  Error: .+ database "nosuchdb" does not exist (re)
935
1014
  [1]
1015
+ $ pglift database dump postgres --dry-run
1016
+ INFO backing up database 'postgres' on instance 1\d/main (re)
1017
+ DRY RUN: no changes made
936
1018
  $ pglift database dump postgres
937
1019
  INFO backing up database 'postgres' on instance 1\d/main (re)
938
1020
  $ ls $TMPDIR/srv/dumps/*-main
@@ -996,6 +1078,10 @@ Databases
996
1078
  $ pglift database drop myapp
997
1079
  INFO dropping 'myapp' database
998
1080
 
1081
+ $ pglift role drop test --dry-run
1082
+ INFO dropping role 'test'
1083
+ INFO removing entries from pg_hba.conf
1084
+ DRY RUN: no changes made
999
1085
  $ pglift role drop test
1000
1086
  INFO dropping role 'test'
1001
1087
  Error: role "test" cannot be dropped because some objects depend on it (detail: owner of database test
@@ -1007,6 +1093,10 @@ Databases
1007
1093
 
1008
1094
  Error: Invalid value for '--reassign-owned': Value error, field is mutually exclusive with 'drop_owned'
1009
1095
  [2]
1096
+ $ pglift role drop test --drop-owned --dry-run
1097
+ INFO dropping role 'test'
1098
+ INFO removing entries from pg_hba.conf
1099
+ DRY RUN: no changes made
1010
1100
  $ pglift role drop test --drop-owned
1011
1101
  INFO dropping role 'test'
1012
1102
  INFO removing entries from pg_hba.conf
@@ -0,0 +1,231 @@
1
+ # SPDX-FileCopyrightText: 2021 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ Site settings and configuration
6
+
7
+ $ PASSWORDS=$TMPDIR/passwords.json
8
+ $ cat > $PASSWORDS << 'EOF'
9
+ > {
10
+ > "postgres": "s3per",
11
+ > "replication": "r3pl"
12
+ > }
13
+ > EOF
14
+
15
+ $ export PGLIFT_CLI__LOG_FORMAT="%(levelname)-4s %(message)s"
16
+ $ export PGLIFT_POSTGRESQL__AUTH__LOCAL=md5
17
+ $ export PGLIFT_POSTGRESQL__AUTH__PASSFILE=null
18
+ $ export PGLIFT_POSTGRESQL__AUTH__PASSWORD_COMMAND='["jq", "-r", ".{role}", "'$PASSWORDS'"]'
19
+ $ export PGLIFT_POSTGRESQL__REPLROLE=replication
20
+
21
+ $ export PGLIFT_CONFIG_DIR="$TMPDIR/$TESTFILE.conf.d"
22
+ $ mkdir -p $PGLIFT_CONFIG_DIR/postgresql
23
+ $ cat > $PGLIFT_CONFIG_DIR/postgresql/pg_hba.conf << 'EOF'
24
+ > local all {surole} {auth.local}
25
+ > local all all {auth.local}
26
+ > host all all 127.0.0.1/32 {auth.host}
27
+ > host all all ::1/128 {auth.host}
28
+ > local replication all {auth.local}
29
+ > host replication {replrole} 127.0.0.1/32 {auth.host}
30
+ > host replication {replrole} ::1/128 {auth.host}
31
+ > EOF
32
+
33
+ $ PREFIX1=$TMPDIR/1
34
+ $ PREFIX2=$TMPDIR/2
35
+ $ RUN_PREFIX1=$PREFIX1/run
36
+ $ RUN_PREFIX2=$PREFIX2/run
37
+ $ PG1PORT=$(port-for pg1)
38
+ $ PG2PORT=$(port-for pg2)
39
+ $ alias pglift1="env PGLIFT_PREFIX=$PREFIX1 PGLIFT_RUN_PREFIX=$RUN_PREFIX1 pglift --log-level=INFO --non-interactive"
40
+ $ alias pglift2="env PGLIFT_PREFIX=$PREFIX2 PGLIFT_RUN_PREFIX=$RUN_PREFIX2 pglift --log-level=INFO --non-interactive"
41
+
42
+ $ pglift1 site-configure install
43
+ INFO creating PostgreSQL log directory: $TMPDIR/1/log/postgresql
44
+ $ pglift2 site-configure install
45
+ INFO creating PostgreSQL log directory: $TMPDIR/2/log/postgresql
46
+
47
+ (Cleanup steps)
48
+
49
+ $ trap "
50
+ > pglift1 instance drop; \
51
+ > pglift2 instance drop; \
52
+ > pglift1 site-configure uninstall; \
53
+ > pglift2 site-configure uninstall; \
54
+ > port-for -u pg1; port-for -u pg2" \
55
+ > EXIT
56
+
57
+ Create a primary instance
58
+
59
+ $ cat > $TMPDIR/primary.yaml <<EOF
60
+ > name: pg1
61
+ > port: $PG1PORT
62
+ > surole_password: s3per
63
+ > replrole_password: r3pl
64
+ > data_checksums: true
65
+ > settings:
66
+ > log_line_prefix: ' ~ '
67
+ > EOF
68
+ $ pglift1 instance apply -f $TMPDIR/primary.yaml
69
+ INFO initializing PostgreSQL
70
+ INFO configuring PostgreSQL authentication
71
+ INFO configuring PostgreSQL
72
+ INFO creating PostgreSQL socket directory directory: $TMPDIR/1/run/postgresql
73
+ INFO starting PostgreSQL 1\d\/pg1 (re)
74
+ INFO creating role 'replication'
75
+ INFO creating instance dumps directory: \$TMPDIR\/1\/srv\/dumps\/1\d-pg1 (re)
76
+
77
+ Create a standby instance
78
+
79
+ $ pglift2 instance create pg2 \
80
+ > --data-checksums \
81
+ > --standby-for="host=$RUN_PREFIX1/postgresql port=$PG1PORT user=replication" \
82
+ > --standby-password=r3pl \
83
+ > --port=$PG2PORT \
84
+ > --surole-password=s3per
85
+ INFO initializing PostgreSQL
86
+ INFO configuring PostgreSQL authentication
87
+ INFO configuring PostgreSQL
88
+ INFO creating PostgreSQL socket directory directory: $TMPDIR/2/run/postgresql
89
+ INFO starting PostgreSQL 1\d\/pg2 (re)
90
+ INFO creating instance dumps directory: \$TMPDIR\/2\/srv\/dumps\/1\d-pg2 (re)
91
+
92
+ Add some data to the primary, check replication
93
+
94
+ $ pglift1 database run -d postgres \
95
+ > "CREATE TABLE t AS (SELECT generate_series(1, 3) i)"
96
+ INFO running "CREATE TABLE t AS \(SELECT generate_series\(1, 3\) i\)" on postgres database of 1\d\/pg1 (re)
97
+ INFO SELECT 3
98
+ $ pglift2 -Lerror database run -o json -d postgres "SELECT i FROM t"
99
+ {
100
+ "postgres": [
101
+ {
102
+ "i": 1
103
+ },
104
+ {
105
+ "i": 2
106
+ },
107
+ {
108
+ "i": 3
109
+ }
110
+ ]
111
+ }
112
+ $ pglift2 instance get -o json \
113
+ > | jq '.standby.slot, .standby.replication_lag, .standby.wal_sender_state, .state'
114
+ null
115
+ "0"
116
+ "streaming"
117
+ "started"
118
+
119
+ Prepare the primary for rewind connection by creating a rewind user (and resp.
120
+ connection database), on the primary (to be demoted)
121
+
122
+ $ pglift1 -Lerror role create rwd --login --replication --password=rwdpw
123
+ $ pglift1 -Lerror database create rwdb --owner rwd
124
+ $ pglift1 -Lerror database run -d rwdb \
125
+ > "GRANT EXECUTE ON function pg_catalog.pg_ls_dir(text, boolean, boolean) TO rwd"
126
+ $ pglift1 -Lerror database run -d rwdb \
127
+ > "GRANT EXECUTE ON function pg_catalog.pg_stat_file(text, boolean) TO rwd"
128
+ $ pglift1 -Lerror database run -d rwdb \
129
+ > "GRANT EXECUTE ON function pg_catalog.pg_read_binary_file(text) TO rwd"
130
+ $ pglift1 -Lerror database run -d rwdb \
131
+ > "GRANT EXECUTE ON function pg_catalog.pg_read_binary_file(text, bigint, bigint, boolean) TO rwd"
132
+
133
+ Check this role from the standby
134
+ $ pglift2 -Lerror role get rwd -o json
135
+ {
136
+ "name": "rwd",
137
+ "has_password": true,
138
+ "inherit": true,
139
+ "login": true,
140
+ "superuser": false,
141
+ "createdb": false,
142
+ "createrole": false,
143
+ "replication": true,
144
+ "connection_limit": null,
145
+ "valid_until": null,
146
+ "validity": null,
147
+ "memberships": [],
148
+ "hba_records": []
149
+ }
150
+
151
+ Promote the standby
152
+
153
+ $ pglift1 -Lerror instance stop
154
+ $ pglift2 instance status
155
+ PostgreSQL: running
156
+ $ pglift2 instance promote
157
+ INFO promoting PostgreSQL instance
158
+
159
+ Check connection to the new primary
160
+
161
+ $ psql -t -d "host=$RUN_PREFIX2/postgresql port=$PG2PORT user=rwd password=rwdpw dbname=rwdb" -c 'show cluster_name'
162
+ pg2
163
+
164
+
165
+ Demote (rewind) the primary as a standby
166
+
167
+ $ pglift1 instance demote pg1 \
168
+ > --from="host=$RUN_PREFIX2/postgresql port=$PG2PORT user=rwd dbname=rwdb" \
169
+ > --password=rwdpw \
170
+ > -- --no-ensure-shutdown
171
+ INFO demoting PostgreSQL instance
172
+ INFO configuring PostgreSQL
173
+ INFO starting PostgreSQL 1\d\/pg1 (re)
174
+
175
+ Check we can connect to the standby with the replication user
176
+
177
+ $ psql -t -d "host=$RUN_PREFIX1/postgresql port=$PG1PORT user=replication password=r3pl dbname=postgres" -c 'show cluster_name'
178
+ pg1
179
+
180
+
181
+ Check that the instance is really a standby
182
+
183
+ $ ls $PREFIX1/srv/pgsql/1*/pg1/data/standby.signal
184
+ $TMPDIR/1/srv/pgsql/1*/pg1/data/standby.signal (glob)
185
+ $ pglift1 -Lerror database run -d postgres 'SHOW primary_conninfo' -o json
186
+ {
187
+ "postgres": [
188
+ {
189
+ "primary_conninfo": "user=rwd password=rwdpw channel_binding=prefer host='$TMPDIR/2/run/postgresql' port=* sslmode=prefer *" (glob)
190
+ }
191
+ ]
192
+ }
193
+ $ pglift1 instance get -o json \
194
+ > | jq '.standby.slot, .standby.replication_lag, .standby.wal_sender_state, .state'
195
+ null
196
+ "0"
197
+ null
198
+ "started"
199
+
200
+ And finally check replication is functional
201
+
202
+ $ pglift2 -Lerror database create rewindme
203
+ $ pglift2 -Lerror database run -d rewindme "CREATE TABLE t AS (SELECT * FROM generate_series(1, 3) v)"
204
+
205
+ $ pglift1 -Lerror database run -d rewindme "TABLE t" -o json
206
+ {
207
+ "rewindme": [
208
+ {
209
+ "v": 1
210
+ },
211
+ {
212
+ "v": 2
213
+ },
214
+ {
215
+ "v": 3
216
+ }
217
+ ]
218
+ }
219
+
220
+ (Cleanup)
221
+
222
+ INFO dropping instance 1\d\/pg1 (re)
223
+ INFO stopping PostgreSQL 1\d\/pg1 (re)
224
+ INFO deleting PostgreSQL data and WAL directories
225
+ INFO dropping instance 1\d\/pg2 (re)
226
+ INFO stopping PostgreSQL 1\d\/pg2 (re)
227
+ INFO deleting PostgreSQL data and WAL directories
228
+ INFO deleting PostgreSQL log directory
229
+ INFO deleting PostgreSQL socket directory
230
+ INFO deleting PostgreSQL log directory
231
+ INFO deleting PostgreSQL socket directory (no-eol)
@@ -117,6 +117,7 @@ Instance commands
117
117
  backup Back up PostgreSQL INSTANCE
118
118
  backups List available backups for INSTANCE
119
119
  create Initialize a PostgreSQL instance
120
+ demote Demote PostgreSQL INSTANCE as standby of specified source...
120
121
  drop Drop PostgreSQL INSTANCE
121
122
  env Output environment variables suitable to handle to...
122
123
  exec Execute command in the libpq environment for PostgreSQL...
@@ -403,6 +404,30 @@ Instance commands
403
404
 
404
405
  Options:
405
406
  --help Show this message and exit.
407
+ $ pglift instance demote --help
408
+ Usage: pglift instance demote [OPTIONS] [INSTANCE] [REWIND_OPTS]...
409
+
410
+ Demote PostgreSQL INSTANCE as standby of specified source server using
411
+ pg_rewind.
412
+
413
+ The instance must not be running and it may be started at the end of the
414
+ "demotion" process.
415
+
416
+ Extra options can be passed to the pg_rewind command. They may need to be
417
+ prefixed with -- to separate them from the current command options when
418
+ confusion arises. When using extra options, providing the instance
419
+ identifier is required.
420
+
421
+ INSTANCE identifies target instance as <version>/<name> where the <version>/
422
+ prefix may be omitted if there is only one instance matching <name>.
423
+ Required if there is more than one instance on system.
424
+
425
+ Options:
426
+ --from TEXT DSN of source server to synchronize from. [required]
427
+ --password PASSWORD Password for the rewind user.
428
+ --start / --no-start Start the instance at the end of the demotion process
429
+ [default: start]
430
+ --help Show this message and exit.
406
431
  $ pglift instance reload --help
407
432
  Usage: pglift instance reload [OPTIONS] [INSTANCE]...
408
433
 
@@ -578,6 +603,7 @@ Role commands
578
603
  used multiple times.)
579
604
  --revoke ROLE Revoke membership of the given role. (Can be
580
605
  used multiple times.)
606
+ --dry-run Simulate change operations.
581
607
  --help Show this message and exit.
582
608
  $ pglift role create --help
583
609
  Usage: pglift role create [OPTIONS] NAME
@@ -604,6 +630,7 @@ Role commands
604
630
  --validity VALIDITY DEPRECATED. Use 'valid_until' instead.
605
631
  --in-role ROLE Roles which this role should be a member of.
606
632
  (Can be used multiple times.)
633
+ --dry-run Simulate change operations.
607
634
  --help Show this message and exit.
608
635
  $ pglift role drop --help
609
636
  Usage: pglift role drop [OPTIONS] NAME
@@ -617,6 +644,7 @@ Role commands
617
644
  Reassign all PostgreSQL's objects owned by
618
645
  the role being dropped to the specified role
619
646
  name.
647
+ --dry-run Simulate change operations.
620
648
  --help Show this message and exit.
621
649
  $ pglift role get --help
622
650
  Usage: pglift role get [OPTIONS] NAME
@@ -691,6 +719,7 @@ Database commands
691
719
  be used multiple times.)
692
720
  --tablespace TABLESPACE The name of the tablespace that will be
693
721
  associated with the database.
722
+ --dry-run Simulate change operations.
694
723
  --help Show this message and exit.
695
724
  $ pglift database create --help
696
725
  Usage: pglift database create [OPTIONS] NAME
@@ -714,6 +743,7 @@ Database commands
714
743
  Only restore the schema (data definitions).
715
744
  --tablespace TABLESPACE The name of the tablespace that will be
716
745
  associated with the database.
746
+ --dry-run Simulate change operations.
717
747
  --help Show this message and exit.
718
748
  $ pglift database drop --help
719
749
  Usage: pglift database drop [OPTIONS] NAME
@@ -722,6 +752,7 @@ Database commands
722
752
 
723
753
  Options:
724
754
  --force / --no-force Force the drop.
755
+ --dry-run Simulate change operations.
725
756
  --help Show this message and exit.
726
757
  $ pglift database dump --help
727
758
  Usage: pglift database dump [OPTIONS] DBNAME
@@ -731,6 +762,7 @@ Database commands
731
762
  Options:
732
763
  -o, --output DIRECTORY Write dump file(s) to DIRECTORY instead of default
733
764
  dumps directory.
765
+ --dry-run Simulate change operations.
734
766
  --help Show this message and exit.
735
767
  $ pglift database dumps --help
736
768
  Usage: pglift database dumps [OPTIONS] [DBNAME]...
@@ -0,0 +1,70 @@
1
+ # SPDX-FileCopyrightText: 2025 Dalibo
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ Tests for transactions (revert of created objects in case of operational errors).
6
+
7
+ $ export PGLIFT_CLI__LOG_FORMAT="%(levelname)-4s %(message)s"
8
+ $ export PGLIFT_PREFIX=$TMPDIR
9
+ $ export PGLIFT_RUN_PREFIX=$TMPDIR/run
10
+ $ export PGLIFT_POSTGRESQL='{
11
+ > "datadir": "pgsql/{name}/data",
12
+ > "waldir": "pgsql/{name}/wal"
13
+ > }'
14
+ $ export PGLIFT_CONFIG_DIR=/dev/null
15
+
16
+ $ trap "
17
+ > pglift --non-interactive instance drop; \
18
+ > pglift site-configure uninstall; \
19
+ > port-for -u pg" \
20
+ > EXIT
21
+
22
+ $ PGPORT=$(port-for pg)
23
+
24
+ $ pglift -Linfo site-configure install
25
+ INFO creating PostgreSQL log directory: $TMPDIR/log/postgresql
26
+
27
+ Create a working instance for later use (this also checks that creating an instance "stopped" works)
28
+
29
+ $ pglift -Linfo instance create main --port=$PGPORT --state=stopped
30
+ INFO initializing PostgreSQL
31
+ INFO configuring PostgreSQL authentication
32
+ INFO configuring PostgreSQL
33
+ INFO creating PostgreSQL socket directory directory: $TMPDIR/run/postgresql
34
+ INFO starting PostgreSQL 1\d\/main (re)
35
+ INFO stopping PostgreSQL 1\d/main (re)
36
+ INFO creating instance dumps directory: $TMPDIR/srv/dumps/1*-main (glob)
37
+
38
+ Try to create an instance with a non-existing encoding, triggering a failure in 'initdb'
39
+
40
+ $ pglift -Linfo instance create test --encoding=notanencoding --port=$PGPORT
41
+ INFO initializing PostgreSQL
42
+ WARNING Command '['*/bin/pg_ctl', 'init', '-D', '$TMPDIR/srv/pgsql/test/data', '-o', '--auth-host=trust --auth-local=trust --encoding=notanencoding --locale=C --username=postgres --waldir=$TMPDIR/srv/pgsql/test/wal']' returned non-zero exit status 1. (glob)
43
+ WARNING reverting: initializing PostgreSQL
44
+ INFO deleting PostgreSQL data and WAL directories
45
+ Error: Command '['*/bin/pg_ctl', 'init', '-D', '$TMPDIR/srv/pgsql/test/data', '-o', '--auth-host=trust --auth-local=trust --encoding=notanencoding --locale=C --username=postgres --waldir=$TMPDIR/srv/pgsql/test/wal']' returned non-zero exit status 1. (glob)
46
+ initdb: error: "notanencoding" is not a valid server encoding name
47
+ pg_ctl: database system initialization failed
48
+
49
+ The files belonging to this database system will be owned by user "*". (glob)
50
+ This user must also own the server process.
51
+
52
+ The database cluster will be initialized with locale "C".
53
+
54
+ [1]
55
+
56
+ Try to create a database with a non-existing tablespace
57
+ (XXX the final error message is wrong, probably a bug here)
58
+
59
+ $ pglift -Linfo database create db --tablespace=nosuchtbspc
60
+ INFO starting PostgreSQL 1\d\/main (re)
61
+ INFO creating 'db' database in 1\d\/main (re)
62
+ WARNING tablespace "nosuchtbspc" does not exist
63
+ WARNING reverting: creating 'db' database in 1\d\/main (re)
64
+ INFO dropping 'db' database
65
+ INFO stopping PostgreSQL 1\d\/main (re)
66
+ Error: database "db" does not exist
67
+ [1]
68
+
69
+ (cleanup)
70
+ WARNING instance 1*/main is already stopped (no-eol) (glob)
@@ -17,29 +17,25 @@ def test_site_settings(
17
17
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path, prefix_dir: str
18
18
  ) -> None:
19
19
  prefix = tmp_path / prefix_dir
20
- with monkeypatch.context() as m:
21
- m.setenv("PGLIFT_PREFIX", str(prefix))
22
- s = _site.SETTINGS
23
- assert s.prefix == prefix
20
+ monkeypatch.setenv("PGLIFT_PREFIX", str(prefix))
21
+ s = _site.SETTINGS
22
+ assert s.prefix == prefix
24
23
 
25
24
 
26
25
  @pytest.mark.usefixtures("cache_clear")
27
26
  def test_settings_global(monkeypatch: pytest.MonkeyPatch) -> None:
28
- with monkeypatch.context() as m:
29
- m.setenv("pglift_postgresql", json.dumps({"invalid": None}))
30
- with pytest.raises(click.ClickException, match="invalid site settings"):
31
- _ = _site.SETTINGS
27
+ monkeypatch.setenv("pglift_postgresql", json.dumps({"invalid": None}))
28
+ with pytest.raises(click.ClickException, match="invalid site settings"):
29
+ _ = _site.SETTINGS
32
30
 
33
31
 
34
32
  @pytest.mark.usefixtures("cache_clear")
35
- def test_yaml_site_settings_error(
36
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
37
- ) -> None:
33
+ def test_yaml_site_settings_error(tmp_path: Path) -> None:
38
34
  configdir = tmp_path / "pglift"
39
35
  configdir.mkdir()
40
36
  settings_fpath = configdir / "settings.yaml"
41
37
  settings_fpath.write_text("this is not yaml")
42
- with monkeypatch.context() as m:
43
- m.setattr(_site.SiteSettings, "yaml_file", settings_fpath)
38
+ with pytest.MonkeyPatch.context() as mp:
39
+ mp.setattr(_site.SiteSettings, "yaml_file", settings_fpath)
44
40
  with pytest.raises(click.ClickException, match="invalid site settings"):
45
41
  _ = _site.SETTINGS
@@ -70,15 +70,13 @@ def settings(settings: BaseSettings) -> Settings:
70
70
 
71
71
 
72
72
  @pytest.fixture
73
- def site_settings(
74
- monkeypatch: pytest.MonkeyPatch, settings: Settings, tmp_path: Path
75
- ) -> Iterator[Settings]:
73
+ def site_settings(settings: Settings, tmp_path: Path) -> Iterator[Settings]:
76
74
  """Make _site.SETTINGS filled with the value of 'settings' fixture."""
77
75
  sf = tmp_path / "settings.yaml"
78
76
  sf.write_text(yaml.dump(settings.model_dump(mode="json")))
79
77
  _site.clear_caches()
80
- with monkeypatch.context() as m:
81
- m.setattr(_site.SiteSettings, "yaml_file", sf)
78
+ with pytest.MonkeyPatch.context() as mp:
79
+ mp.setattr(_site.SiteSettings, "yaml_file", sf)
82
80
  yield _site.SETTINGS
83
81
  _site.clear_caches()
84
82
 
@@ -283,6 +281,7 @@ def test_instance_commands_completion(obj: Obj) -> None:
283
281
  "backup",
284
282
  "backups",
285
283
  "create",
284
+ "demote",
286
285
  "drop",
287
286
  "env",
288
287
  "exec",
@@ -321,14 +320,12 @@ def test_site_settings_json(runner: CliRunner, settings: Settings, obj: Obj) ->
321
320
 
322
321
 
323
322
  @pytest.mark.usefixtures("site_settings")
324
- def test_site_settings_defaults(
325
- runner: CliRunner, obj: Obj, monkeypatch: pytest.MonkeyPatch
326
- ) -> None:
323
+ def test_site_settings_defaults(runner: CliRunner, obj: Obj) -> None:
327
324
  """site-settings --defaults returns default settings, not accounting for
328
325
  settings.yaml or env vars.
329
326
  """
330
- with monkeypatch.context() as m:
331
- m.setenv("PGLIFT_PREFIX", "/srv/pglift")
327
+ with pytest.MonkeyPatch.context() as mp:
328
+ mp.setenv("PGLIFT_PREFIX", "/srv/pglift")
332
329
  result = runner.invoke(
333
330
  cli, ["site-settings", "-o", "json", "--defaults"], obj=obj
334
331
  )
@@ -343,12 +340,11 @@ def test_site_settings_no_defaults(
343
340
  runner: CliRunner,
344
341
  settings: Settings,
345
342
  obj: Obj,
346
- monkeypatch: pytest.MonkeyPatch,
347
343
  ) -> None:
348
344
  s = settings.model_dump(mode="json")
349
345
  assert s["powa"] is not None
350
- with monkeypatch.context() as m:
351
- m.setenv("PGLIFT_PREFIX", "/srv/pglift")
346
+ with pytest.MonkeyPatch.context() as mp:
347
+ mp.setenv("PGLIFT_PREFIX", "/srv/pglift")
352
348
  result = runner.invoke(
353
349
  cli, ["site-settings", "-o", "json", "--no-defaults"], obj=obj
354
350
  )
@@ -419,8 +415,8 @@ def test_instance_schema(runner: CliRunner, obj: Obj) -> None:
419
415
  def test_instance_shell_var_missing(
420
416
  runner: CliRunner, instance: Instance, obj: Obj, monkeypatch: pytest.MonkeyPatch
421
417
  ) -> None:
422
- with patch("os.execle", autospec=True) as execle, monkeypatch.context() as m:
423
- m.delenv("SHELL", raising=False)
418
+ with patch("os.execle", autospec=True) as execle:
419
+ monkeypatch.delenv("SHELL", raising=False)
424
420
  r = runner.invoke(
425
421
  cli,
426
422
  ["instance", "shell", instance.name],
@@ -438,8 +434,8 @@ def test_instance_shell_var_missing(
438
434
  def test_instance_shell(
439
435
  runner: CliRunner, instance: Instance, obj: Obj, monkeypatch: pytest.MonkeyPatch
440
436
  ) -> None:
441
- with patch("os.execle", autospec=True) as execle, monkeypatch.context() as m:
442
- m.setenv("SHELL", "fooshell")
437
+ with patch("os.execle", autospec=True) as execle:
438
+ monkeypatch.setenv("SHELL", "fooshell")
443
439
  runner.invoke(
444
440
  cli,
445
441
  ["instance", "shell", instance.name],
@@ -55,17 +55,15 @@ def test_completion(runner: CliRunner, shell: str) -> None:
55
55
 
56
56
 
57
57
  @pytest.mark.usefixtures("cache_clear")
58
- def test_log_level(
59
- runner: CliRunner, obj: Obj, monkeypatch: pytest.MonkeyPatch
60
- ) -> None:
58
+ def test_log_level(runner: CliRunner, obj: Obj) -> None:
61
59
  @main.cli.command("foo")
62
60
  def foo() -> None:
63
61
  logger = logging.getLogger("pglift")
64
62
  logger.debug("debug message")
65
63
  print("something")
66
64
 
67
- with monkeypatch.context() as m:
68
- m.setenv("PGLIFT_CLI__LOG_LEVEL", "invalid")
65
+ with pytest.MonkeyPatch.context() as mp:
66
+ mp.setenv("PGLIFT_CLI__LOG_LEVEL", "invalid")
69
67
  result = runner.invoke(main.cli, ["foo"], obj=obj)
70
68
  assert result.exit_code == 1
71
69
  assert (
@@ -73,15 +71,15 @@ def test_log_level(
73
71
  in result.stderr
74
72
  )
75
73
 
76
- with monkeypatch.context() as m:
77
- m.setenv("PGLIFT_CLI__LOG_LEVEL", "debug")
74
+ with pytest.MonkeyPatch.context() as mp:
75
+ mp.setenv("PGLIFT_CLI__LOG_LEVEL", "debug")
78
76
  result = runner.invoke(main.cli, ["foo"])
79
77
  assert result.exit_code == 0 and "something" in result.stdout, result.stderr
80
78
  assert "debug message" in result.stderr
81
79
 
82
- with monkeypatch.context() as m:
80
+ with pytest.MonkeyPatch.context() as mp:
83
81
  # -L option takes precedence
84
- m.setenv("PGLIFT_CLI__LOG_LEVEL", "debug")
82
+ mp.setenv("PGLIFT_CLI__LOG_LEVEL", "debug")
85
83
  result = runner.invoke(main.cli, ["-Linfo", "foo"], obj=obj)
86
84
  assert result.exit_code == 0 and "something" in result.stdout, result.stderr
87
85
  assert "debug message" not in result.stderr
File without changes
File without changes
File without changes
File without changes