Skip to content

feat(lifecycle): add dormant PostgreSQL Docker lifecycle adapter - #181

Merged
aparragithub merged 5 commits into
mainfrom
feat/sp-resource-lifecycle-authority
Aug 8, 2026
Merged

feat(lifecycle): add dormant PostgreSQL Docker lifecycle adapter#181
aparragithub merged 5 commits into
mainfrom
feat/sp-resource-lifecycle-authority

Conversation

@aparragithub

@aparragithub aparragithub commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Closes #182

Summary

PR4 of the SP-RESOURCE-LIFECYCLE chain (stacked to main, after #177/#179/#180). Adds the live authority and journal boundary as a dormant adapter — the module is not imported by the server, so no runtime behavior changes until the composition slice (PR5) wires it.

Implements tasks 4.1–4.4.

Changes

  • Fixed-argv Docker observation (lifecycle.py): no shell, validated immutable identifiers, bounded timeout. Malformed output, nonzero exit, timeout, and identifier injection/mismatch all map to UNVERIFIABLE with zero mutation.
  • Read-only authority lookup (authority.py): LocalOwnershipAuthority accepts signed records carrying lifecycle metadata while preserving legacy records; lifecycle_records() stays read-only. The adapter coerces raw authority mappings into validated immutable LifecycleAuthorityRecord values and ignores missing/invalid/retired metadata fail-closed. Caller scope is never copied into the result.
  • Durable journal (JsonlLifecycleJournal): fsynced append-only JSONL that reloads immutable run and action records. Recovery verbs delegate without provision or restore.

Verification

uv run pytest:      1878 passed, 46 deselected, 13 subtests passed
uv run ruff check:  All checks passed!
uv run ruff format: 306 files already formatted
uv run mypy:        Success: no issues found in 302 source files

Slice size: 398 changed lines across exactly 3 paths, within the 400-line review budget.

Scope boundary

Does not touch portfolio, wizard, data-environments, migrations, or backfill. No new flags. Rollback is reverting this commit; PR3 stays green and dormant.

Whole-change verification runs once after PR5 completes the remaining requirement (Controlled operations and audit) and its Manual default scenario.

Summary by CodeRabbit

  • New Features

    • Added lifecycle management for PostgreSQL Docker instances, including observation, quarantine, adoption, reconciliation, deletion, and cleanup.
    • Added authority-backed verification of Docker identity, labels, and resource presence.
    • Added durable lifecycle event journaling with secure persistence and reload support.
    • Quarantine history now preserves the resource’s last activity timestamp.
  • Bug Fixes

    • Invalid or incomplete Docker evidence now fails safely.
    • Validation accepts additional record fields while continuing to enforce required fields and signatures.
  • Tests

    • Added coverage for lifecycle operations, legacy records, journal recovery, concurrency, and secure command execution.

Implement the PR4 slice of SP-RESOURCE-LIFECYCLE: a lifecycle adapter that
observes PostgreSQL Docker resources through fixed-argv commands, resolves
read-only authority records, and journals immutable evidence.

- Fixed-argv Docker observation with no shell; malformed output, nonzero
  exit, timeout, and identifier mismatch all map to UNVERIFIABLE with zero
  mutation.
- Read-only tenant/project authority lookup emitting typed presence
  evidence; caller scope is never copied into the result.
- Fsynced append-only JSONL journal that reloads immutable run and action
  records; recovery verbs delegate without provision or restore.

The module stays unimported by the server, so no runtime behavior changes
until the composition slice wires it.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6f5b8fc7-3000-4f51-ba79-170d2cab3ac0

📥 Commits

Reviewing files that changed from the base of the PR and between 31c577e and 91ebdd8.

📒 Files selected for processing (5)
  • src/odoo_forge/resource_lifecycle/service.py
  • src/odoo_forge/resource_lifecycle/types.py
  • src/odoo_forge_postgres_docker/lifecycle.py
  • tests/adapters/test_postgres_docker_lifecycle.py
  • tests/resource_lifecycle/test_service.py

📝 Walkthrough

Walkthrough

The PR adds a PostgreSQL Docker lifecycle adapter, authority-backed observation, recovery delegation, durable JSONL journaling, and quarantine-history activity preservation.

Changes

PostgreSQL Docker lifecycle

Layer / File(s) Summary
Authority and lifecycle record contracts
src/odoo_forge_postgres_docker/authority.py, src/odoo_forge_postgres_docker/lifecycle.py
Authority records expose the latest operation/name entries. Validation accepts additional fields. Immutable lifecycle records validate and coerce required fields.
Docker observation and recovery operations
src/odoo_forge_postgres_docker/lifecycle.py, tests/adapters/test_postgres_docker_lifecycle.py
The adapter scopes authority records, verifies Docker identity and labels, maps container state to typed presence, fails closed on invalid evidence, and delegates recovery operations without provisioning or restoration.
Durable lifecycle event journal
src/odoo_forge_postgres_docker/lifecycle.py, tests/adapters/test_postgres_docker_lifecycle.py
JsonlLifecycleJournal securely appends, synchronizes, locks, and reloads lifecycle events.
Quarantine activity history
src/odoo_forge/resource_lifecycle/types.py, src/odoo_forge/resource_lifecycle/service.py, tests/resource_lifecycle/test_service.py
Quarantine history stores and tests the observed last_activity timestamp.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LocalOwnershipAuthority
  participant PostgresDockerLifecycleAdapter
  participant Docker
  participant JsonlLifecycleJournal
  participant Provider
  LocalOwnershipAuthority->>PostgresDockerLifecycleAdapter: lifecycle_records()
  PostgresDockerLifecycleAdapter->>Docker: list and inspect container
  Docker-->>PostgresDockerLifecycleAdapter: identity, labels, and state
  PostgresDockerLifecycleAdapter->>JsonlLifecycleJournal: append lifecycle event
  PostgresDockerLifecycleAdapter->>Provider: delegate lifecycle action
Loading

Possibly related PRs

Suggested labels: type:feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: a dormant PostgreSQL Docker lifecycle adapter.
Linked Issues check ✅ Passed The PR implements the linked issue's Slice A requirements for Docker observation, authority lookup, durable journaling, and delegated recovery [#182].
Out of Scope Changes check ✅ Passed The changes remain within lifecycle scope and support the adapter, evidence journal, recovery delegation, or quarantine history requirements [#182].
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sp-resource-lifecycle-authority

Comment @coderabbitai help to get the list of available commands.

Comment thread tests/adapters/test_postgres_docker_lifecycle.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/odoo_forge_postgres_docker/lifecycle.py`:
- Around line 193-208: Update LifecycleJournal.append to acquire a process-safe
exclusive lock on the journal descriptor before any writes and retain it through
the complete write loop and fsync, releasing it during cleanup so each record
append is serialized. When O_CREAT creates the journal, also fsync
self.path.parent after the file creation to persist the directory entry.
- Around line 57-69: Validate timeout in
PostgresDockerLifecycleAdapter.__init__: reject non-positive or non-finite
values and any value exceeding the configured maximum finite limit before
assigning self._timeout. Reuse the existing timeout-limit configuration or
symbol if available, and add tests covering float("inf"), 0, and a value above
that limit.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c7956615-9089-4a81-b3cd-4e494ee4cf76

📥 Commits

Reviewing files that changed from the base of the PR and between b8c7dc6 and b18fab9.

📒 Files selected for processing (3)
  • src/odoo_forge_postgres_docker/authority.py
  • src/odoo_forge_postgres_docker/lifecycle.py
  • tests/adapters/test_postgres_docker_lifecycle.py

Comment thread src/odoo_forge_postgres_docker/lifecycle.py
Comment thread src/odoo_forge_postgres_docker/lifecycle.py
CodeQL py/side-effect-in-assert flagged the mock_calls expectation. The
preceding assert also invoked adapter.cleanup() inside the assert itself,
so under python -O the call would be stripped and the delegation
expectation would no longer hold. Bind both results before asserting.
…able

Address two review findings on the lifecycle adapter.

The adapter accepted any timeout, so float("inf") let subprocess.run wait
without a deadline and defeated the bounded-timeout guarantee the Docker
boundary relies on. Reject non-finite, non-positive, and over-limit values
at construction, matching how LifecycleService already rejects a negative
max_cleanup_retries.

A journal record can span several os.write calls when a write is partial.
Under O_APPEND another process could interleave its own record between
those calls and corrupt both JSONL lines, so hold an exclusive lock across
the whole record through fsync. Syncing the file also left a newly created
journal's directory entry unrecoverable after a crash, losing the entire
audit trail; fsync the parent directory when the append creates the file.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/odoo_forge_postgres_docker/lifecycle.py`:
- Around line 203-216: Update the journal-reading logic in events() to open the
journal through a descriptor, acquire fcntl.LOCK_SH before reading, and release
it afterward so reads cannot overlap the locked write sequence. Preserve
existing event parsing behavior, and add a concurrent reader/writer test
covering reads during append operations.
- Around line 219-222: Update the append flow around _fsync_directory and the
created condition so every successful append fsyncs self.path.parent before
returning, not only the process that created the file. Preserve the existing
journal locking and append behavior while removing the initialization-only
restriction.

In `@tests/adapters/test_postgres_docker_lifecycle.py`:
- Around line 146-188: Replace the syscall-order assertions in
test_journal_holds_an_exclusive_lock_from_first_write_through_fsync and
test_journal_fsyncs_parent_directory_only_when_it_creates_the_file with
observable journal behavior tests. Use concurrent append processes against
JsonlLifecycleJournal, then verify events() reloads complete, intact events;
move any crash-recovery scenario requiring integration infrastructure to a
separately marked, default-deselected integration test.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 45a0651e-d00b-42b5-a079-309e79d0a67a

📥 Commits

Reviewing files that changed from the base of the PR and between 153c996 and 31c577e.

📒 Files selected for processing (2)
  • src/odoo_forge_postgres_docker/lifecycle.py
  • tests/adapters/test_postgres_docker_lifecycle.py

Comment thread src/odoo_forge_postgres_docker/lifecycle.py
Comment thread src/odoo_forge_postgres_docker/lifecycle.py Outdated
Comment thread tests/adapters/test_postgres_docker_lifecycle.py Outdated
…reads

A non-creating append could return before the creating process synced the
directory entry, leaving the audit trail unrecoverable after a crash. Sync
on every append instead of only the first, and take a shared lock when
reading so a concurrent append cannot hand back a truncated final line.
…istory

Quarantine history carried the evidence digest and timestamp but not the
baseline the expiration decision was made against, so the durable trail
could not show why a resource was judged expired.
@aparragithub
aparragithub merged commit 2cc27c3 into main Aug 8, 2026
6 checks passed
@aparragithub
aparragithub deleted the feat/sp-resource-lifecycle-authority branch August 8, 2026 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resource lifecycle: live authority, durable journal, and runtime composition

2 participants