Skip to content

release: 3.7.0 - #1399

Closed
Jaro-c wants to merge 22 commits into
mainfrom
release/3.7.0
Closed

release: 3.7.0#1399
Jaro-c wants to merge 22 commits into
mainfrom
release/3.7.0

Conversation

@Jaro-c

@Jaro-c Jaro-c commented Aug 9, 2026

Copy link
Copy Markdown
Member

Summary

Bump podup to 3.7.0.

Version bumps in lockstep (the release workflow's verify job fails closed if any of the three drifts):

  • Cargo.toml: 3.6.13.7.0
  • Cargo.lock: regenerated to match
  • debian/changelog: new entry at the top with the same version, full changelog for the cycle.

Changelog covers everything merged into develop since the 3.6.1 release:

  • Addedpodman-lane-develop-nightly workflow with the 88% coverage floor on the Podman 6 leg; per-engine project-label filter cache (Engine::project_label_filter_encoded / …_raw / …_with); write_frame shared helper on the Engine.
  • Fixedinterpolate_value gates the parent mapping rebuild on mapping_needs_interp; progress ticker caches is_terminal + window size at begin; progress_line short-circuits on a SESSION_OPEN AtomicBool; identity_style caches the PROJECT_PREFIX; restart_service_set returns Arc<HashSet<String>> halves; is_exec_teardown_noise anchors on the read unixpacket system prefix; confirm_lost_response uses eq_ignore_ascii_case; libpod_pull_policy hard-errors on unknown values; Engine::run_lifecycle_hook takes the stdout lock per-frame, symmetric with stderr; libpod/client/mod.rs and engine/mod.rs are both under the 500-line hard cap (the per-PR lane is no longer red on the line-limit check).
  • Changed — Podman baseline moved to 6.0.2 (re-validated via the live lane on a fresh Fedora rawhide cloud image; PODUP_SUMMARY pass=179 fail=0 flaky=0 on the Podman 6 leg).

Validation

  • cargo build --locked --bin podup --features test-helpers
  • cargo test --locked --lib --features test-helpers — 1605 passed, 0 failed
  • cargo fmt --all --check
  • cargo clippy --locked --all-targets --all-features -- -D warnings
  • The release workflow's verify job (which runs at tag time on main) will gate on these three files agreeing and on cargo audit finding no RUSTSEC advisories, then build and publish the artifacts.

Release plan

Once this PR is merged into main, the release workflow runs by pushing the tag v3.7.0 onto main (or via gh workflow run release --ref main -f tag=v3.7.0). The verify step will re-check the three version files match and audit dependencies; the build matrix will produce the 7 targets (Linux x86_64 / arm64, macOS arm64 / x86_64, Windows x86_64 / arm64, the Debian .debs for amd64 / arm64); the release job will sign, build-provenance-attest, and publish. The published release will close the open podman-version-watch cycle and surface in the apt pool on the next daily rebuild.

Closes the open validate podup on Podman X issue that was revalidated for 6.0.2 earlier in the day.

Jaro-c and others added 22 commits August 7, 2026 16:43
…e reporting (#1365) (#1375)

Closes #1365.

I bound the libpod log/stream parsers to a 1 MiB cap, enforced before
any allocation: `parse_frame` rejects a hostile size field with
`StreamTooLarge` before splitting the buffer, and `parse_multiplexed` /
`parse_json_lines` use a cumulative-bytes counter so many small frames
cannot grow the buffered slice past the cap between checks.

The wire-format correction is the bigger behavioural fix: the logs frame
header is **8 bytes** (4 bytes stream type + 4 bytes size big-endian
uint32), the daemon does not cap frame sizes, and **stats/events are
NDJSON rather than frame-stream** — three different parsers, not one.
The trailing-bytes path of `parse_json_lines` now reports
`StreamEndedEarly` when the remainder does not parse, instead of a serde
error that hid the cause.

I also rewrote the hijack response-head reader to drain the head bytes
first before deciding the error, with a coverage test that runs against
a peer that never sends the terminator.

New fuzz target `stream_ndjson` exercises the cumulative cap; the
existing `stream_frame` target now seeds an oversized header and expects
`StreamTooLarge` without an OOM.

## Validation

- `cargo build`, `cargo test --lib` (1540+ tests), `cargo test --bins`,
`cargo fmt --check`, `cargo clippy -- -D warnings`: all green on the
branch.
- Fuzz target updates: `fuzz/fuzz_targets/stream_frame.rs` already
exercised `parse_frame`; I added the 8-byte header size, a 4 GiB hostile
header that must error without OOM, and a new `stream_ndjson` target for
the cumulative-cap path.
- Lane: real-Podman 5 and 6 on this PR.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…64 padding, ed25519 error classification (#1359) (#1376)

## Test plan

- **Fixture release** under `tests/fixtures/releases/` with three
rotation slots populated in `install.sh`. The CI step iterates every
`*.sig` and asserts verification.
- **Shell test for the padding fix**: a known-old fixture and a
known-mis-padded fixture, both should fail verification with the correct
error class.
- **CI**: extend `.github/workflows/installer-contract.yml` with a step
that runs the shell fixtures on Linux, a Windows-runner step for
`install.ps1`, and the existing Rust reference test for parity.

Closes #1359.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…nd events (#1366) (#1378)

Closes #1366.

Five sites in `internal/engine/build/mod.rs` and one in
`internal/engine/events.rs` called
`serde_json::to_string(...).unwrap_or_default()` and dropped the result
of a serialisation failure as the empty string. An empty `buildargs=`
reached libpod as a no-arg query parameter and the build silently ran
with image defaults; an empty events row corrupted the NDJSON stream a
parser was reading line-by-line.

All six sites now flow through one `to_query_json` helper
(`internal/engine/mod.rs`) that returns `Result<String>` and surfaces
the field name and the underlying error in a `ComposeError::Build`
message. The events side logs the cause at `debug` and drops the single
row, so the stream stays well-formed.

Unit tests: a synthetic `Serialize` impl that always errors is run
through `to_query_json` with each of the five build-side labels and the
events-side label, asserting `Err` and the message. The happy path is
pinned to the actual types each call site serialises.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…1362) (#1377)

Closes #1362.

## What changed

I added a per-socket HTTP/1.1 connection pool in
`internal/libpod/client/pool.rs` keyed by the Podman socket path.
Buffered calls (`get_json`, `post_json`, `post_json_ok`,
`post_empty_ok`, `post_empty_json`, `post_empty_json_unbounded`,
`post_bytes_json`, `put_bytes_ok`, `head_container_path_stat`,
`delete_existed`, `ping`) now acquire a connection, issue one request,
and release it on completion. A response carrying `Connection: close` is
dropped proactively so the next acquire opens a fresh one instead of
paying the wait for a half-closed socket. The default cap is **8
connections**, tunable via `Client::with_pool_size` or the new
`--connection-pool-size` / `PODUP_LIBCOD_POOL` global CLI flag.

Streaming endpoints (`get_stream`, `post_json_stream`,
`post_empty_stream`, `post_bytes_stream`, `post_stream_body`,
`post_json_stream_within`) now take a **dedicated** connection outside
the buffered pool and hold it for the lifetime of the response body; the
connection is released when the `Client` is dropped. That keeps the
public streaming-helper interface (`Result<Response<Incoming>>`) intact
while ensuring a long-lived `logs -f` or interactive `exec` cannot share
its socket with a buffered caller mid-stream and corrupt the wire.

`Client::new`'s signature is unchanged (so the public surface of
`podup::Client` is not a breaking change). The new public items are
`Client::with_pool_size`, `Client::pool_size`,
`Client::DEFAULT_POOL_SIZE`, `podman::connect_with_pool_size`, and
`podman::connect_from_env_with_pool_size` — all additive.

The `Client::new` doc comment is updated to describe the pool contract.
Every existing doc comment on the touched items is preserved.

## Why

`Client::connect` (`internal/libpod/client/mod.rs`) opened a brand-new
hyper HTTP/1.1 connection over the Unix socket for *every* request. The
doc comment justified it as "correct for a CLI tool where API calls are
sequential and infrequent", but `up` is fan-out by construction: a
100-service compose file issues ~5 libpod calls per service plus a
per-level bulk — easily **600+ connections per `up`**. Validation
against the libpod server source confirmed the server DOES allow
connection reuse; the bug was the client side opening a fresh connection
per request.

## Test plan

- **Unit test** with a fake-libpod server that counts incoming
connections (`internal/libpod/client/pool/tests.rs`):
  - Sequential 100 requests → 1 connection.
  - Concurrent 16 requests (pool size 4) → ≤ 4 connections.
- A forcibly-closed (poisoned) connection is replaced on the next
acquire.
  - A new `Client` after a `Drop` opens a fresh connection.
- Pool size is what `Client::pool_size` reports; a cap of 0 is floored
to 1.
  - A `Drop` on the `Client` closes the pool.
- **Existing fake-podman harness tests** (1500+ in `cargo test --lib
--features test-helpers`): all still pass. The harness sends
`Connection: close` on every response, and the new pool discards those
proactively so the second request lands on a fresh socket.
- **CLI**: `podup --help` lists `--connection-pool-size <N>` and the
`PODUP_LIBCOD_POOL` env var; the global flag is forwarded through
`main.rs` and `autostart_cmd.rs` to every
`podman::connect_with_pool_size` call site.
- **Lane**: real-Podman 5 and 6, no regression (run on this PR).

## Validation

- `cargo build --bin podup --features test-helpers` — green.
- `cargo test --lib --features test-helpers` — 1550 tests, all green.
- `cargo test --bins --features test-helpers` — 83 tests, all green.
- `cargo fmt --check` — clean.
- `cargo clippy --bin podup --features test-helpers -- -D warnings` —
clean.

## Limits (intentional)

- Streaming connections are released when the **Client** drops, not when
the stream body drops. The streaming helpers' return type is fixed at
`Response<Incoming>` (per the public-surface constraint), so the body
does not carry a hook to release the underlying connection. In the CLI,
the `Client` lifetime matches the command lifetime, so the two converge
in practice; an embedder holding a `Client` across many operations will
retain streaming connections until the `Client` is dropped.
- The pool is **per-socket-path**. A `Client` instance holds one pool;
the `socket_path` field is immutable after `Client::new`. The "keyed by
socket path" requirement is satisfied because the pool is constructed
from the socket path itself.

---------

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
… unify clap error formatting (#1368) (#1374)

## Test plan

- **Exhaustiveness test**: a compile-time check that every `Commands`
variant appears in either `dispatch.rs` or `rest.rs`, with no catch-all.
The `#[deny(unreachable_patterns)]` lint gives this for free.
- **Integration**: `podup logs --wrong-flag` exits non-zero with the
`podup:` prefix.
- **Unit test** for `is_label_only` future-proofing (covered here as
part of the same shape concern): a `#[non_exhaustive]` annotation on the
enum would catch it at compile time.

---------

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
Closes #1326.

The nightly lane run on Podman 6 already measures coverage (`cargo
llvm-cov --all-features`) and prints `PODUP_COVERAGE=N.MM`, but the
value is only reported to the job summary — never gated. The owner
recommended a 88% nightly floor in #1326 (3 samples 91.5-91.6%, room for
natural drift above the level where a real regression hides), with PR
runs deliberately not gated (a second instrumented build costs ~30 min
per leg, and the nightly already pays for it).

This commit adds the gate to the lane's existing verify step:

- Schedule + Podman 6 only (the leg that represents the latest stable
major and the one that produces a coverage number under
`cargo-llvm-cov`).
- Parses `PODUP_COVERAGE=N.MM`, strips the trailing `%`, and compares as
an integer against 88. Below the floor, the verify step exits non-zero
with a clear error naming #1326.
- PR runs are untouched (they never set `COVERAGE=1`, so the gate
condition is false and the existing path is unchanged).
- workflow_dispatch is also left untouched (a manual run might want to
bypass the gate to test the threshold itself; the nightly schedule is
the gating path).

The lane's matrix condition for `COVERAGE=1` already gates the
measurement to `schedule|workflow_dispatch` (line 279), so this commit
is purely additive: it parses an existing emission and adds a
comparison.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…s label (#1361) (#1382)

The label_file reader split each line on '=' and inserted the (key,
value) pair into SpecGenerator.labels with no control-character or size
check. Podman JSON-encodes the value on the wire, so wire injection was
never the concern — the concern is downstream consumers that re-parse
the label value (logs, ls/inspect UIs, label-based filters), where a
control character in either side lets one entry break out of its
context. The 16 MiB read cap on the file also left the resulting HashMap
size, key length, and value length unconstrained.

This change adds `sanitize_kv_pair`, which validates a single key/value
pair:

- key: non-empty, no ASCII control characters, length ≤ 253 bytes
(Podman's per-label cap);
- value: no ASCII control characters, length ≤ 4 KiB;
- map: capped at 64 distinct entries, with overwrites of an existing key
still allowed at the cap (the cap bounds distinct keys, not insertions).

`build_label_file_labels` now returns `Result<HashMap<String, String>>`
so a rejection surfaces as an `Unsupported` error naming the file and
line, rather than a silently-truncated map.

The `podup.config-files` label joins compose-file paths with `,` and so
could visually merge with the next entry when a path itself contained a
`,`. Each path is now passed through `encode_path_for_label` which
replaces `,` with `%2C`, making the round-trip through any downstream
`,`-split unambiguous.

## Test plan

- Unit tests: `sanitize_kv_pair` — bad input (control chars, oversize
key/value, too many entries) → reject, good input → pass.
- Integration: a 1000-line label file → capped at 64 entries; a label
file with control characters → reject at parse with a clear error; a
path containing `,` in `compose_files` → URL-encoded in the
`podup.config-files` label.
- Live-Podman: the lane (Podman 5 + 6).

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…1356) (#1383)

## Test plan

- **Shell fixture**: a stub `podup` binary that prints different
`--version` outputs. Two cases:
- Reports the resolved tag (with or without leading `v`) → installer
proceeds.
- Reports anything else (older tag, `-dev` suffix, garbage) → installer
refuses with non-zero exit and the staged file is removed.
- **CI**: extend `.github/workflows/asset-contract.yml` (or its
underlying reusable in `Glyndor/.github`) with a step that runs the
shell fixture on Linux, a Windows-runner step for `install.ps1`, and the
existing Rust reference test for parity. The fixture should live in
`tests/fixtures/releases/` or similar.

## Validation

- `shellcheck install.sh` clean.
- `PSScriptAnalyzer install.ps1` clean.
- The new CI job passes on push.
- The Rust reference test (`internal/update/install.rs`) still passes —
no regression.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
Bumps [base64](https://github.com/marshallpierce/rust-base64) from
0.22.1 to 0.23.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md">base64's
changelog</a>.</em></p>
<blockquote>
<h1>0.23.1</h1>
<ul>
<li>Make the tests build again on non-SIMD architectures</li>
</ul>
<h1>0.23.0</h1>
<ul>
<li>Added more consts for preconfigured configs and engines</li>
<li>Make DecodeError::InvalidLastSymbol more clear by including the
decoded value</li>
<li>Added SIMD-accelerated engines behind the default-on
<code>simd-unsafe</code> feature: <code>Simd</code> picks the best
instruction set at runtime (AVX2 on <code>x86_64</code>, NEON on
<code>aarch64</code>) and falls back to the scalar
<code>GeneralPurpose</code> engine, while <code>Avx2</code> and
<code>Neon</code> target one instruction set with no runtime
detection and work in <code>no_std</code>. The engines support the
standard and URL-safe alphabets.</li>
<li>Update MSRV to 1.71.0</li>
<li>Add support for custom padding symbols</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/marshallpierce/rust-base64/commit/069bf7067b949f5c0a92b6ceb82492920502f2c2"><code>069bf70</code></a>
v0.23.1</li>
<li><a
href="https://github.com/marshallpierce/rust-base64/commit/6ab1fb0a5843013557a52c45c84b91f5d1bb87af"><code>6ab1fb0</code></a>
Merge pull request <a
href="https://redirect.github.com/marshallpierce/rust-base64/issues/310">#310</a>
from musicinmybrain/test-on-non-simd-arches</li>
<li><a
href="https://github.com/marshallpierce/rust-base64/commit/7cffce6f971acdf48f83112cbdd63bd61125ba06"><code>7cffce6</code></a>
Fix testing on architectures without unsafe SIMD support</li>
<li><a
href="https://github.com/marshallpierce/rust-base64/commit/e34f9a08c5c89a4641350ac22033f3fa4f5d4d97"><code>e34f9a0</code></a>
Merge pull request <a
href="https://redirect.github.com/marshallpierce/rust-base64/issues/308">#308</a>
from atouchet/com</li>
<li><a
href="https://github.com/marshallpierce/rust-base64/commit/e9240c9a01e0a4934c5619e74740caa6d1f67ce9"><code>e9240c9</code></a>
Remove outdated comment</li>
<li><a
href="https://github.com/marshallpierce/rust-base64/commit/9e9220a4166f628de7c8803289e120ae1e944f78"><code>9e9220a</code></a>
v0.23.0</li>
<li><a
href="https://github.com/marshallpierce/rust-base64/commit/870326ec592eebde9d6bfe4c5d8130c591273e9c"><code>870326e</code></a>
Merge pull request <a
href="https://redirect.github.com/marshallpierce/rust-base64/issues/306">#306</a>
from marshallpierce/mp/trailing-bits-docs</li>
<li><a
href="https://github.com/marshallpierce/rust-base64/commit/fbec5f1050f9fc16e6a826ebabaa2b7b0644bd67"><code>fbec5f1</code></a>
Document no trailing trailing bits</li>
<li><a
href="https://github.com/marshallpierce/rust-base64/commit/0a23549968f059b53cf39e96eba8f46779f322a7"><code>0a23549</code></a>
Merge pull request <a
href="https://redirect.github.com/marshallpierce/rust-base64/issues/305">#305</a>
from marshallpierce/mp/edition-2021</li>
<li><a
href="https://github.com/marshallpierce/rust-base64/commit/f10b7e20614135aa61289140683fc93e5a45d338"><code>f10b7e2</code></a>
Update deps &amp; edition</li>
<li>Additional commits viewable in <a
href="https://github.com/marshallpierce/rust-base64/compare/v0.22.1...v0.23.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=base64&package-manager=cargo&previous-version=0.22.1&new-version=0.23.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…sed, file: secret cleanup) (#1360) (#1384)

I tightened install path integrity across the shell and Rust update
paths. The change preserves target modes from private staging, unifies
Unix and Windows recovery on `target.old`, cleans podup-created native
secrets during `down`, and makes the `dpkg-query` ownership path skip
manifest scanning when the helper is unavailable.

Closes #1360

Validation: `cargo build --locked`, `cargo test --locked`, `shellcheck
install.sh`. PSScriptAnalyzer was not run because `pwsh` is unavailable
in this environment. The Podman 5 and 6 lane remains covered by CI.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
… install (#1367) (#1387)

Refs #1367 (not closing per the issue owner's instruction)

## What's in this PR

Five sub-claims from the issue, addressed as documented:

1. **`cp_to_container` (item 1):** documented the per-project lock +
libpod-validation contract in a new doc comment on `cp_to_container` at
`internal/engine/copy.rs:132-156`. The lock closes the cross-invocation
case (two `podup` processes), libpod's own extraction-directory
validation closes the within-invocation case (a foreign `rm -rf` racing
in is rejected by the second PUT, not silently succeeded). No code
change.

2. **`run_attached` (item 2):** on `post_empty_ok(start_path)` returning
`Err`, drain the hijacked socket's kernel buffer before returning the
error. The drain is bounded by a 2 s budget
(`START_FAILURE_DRAIN_BUDGET`) so a wedged peer cannot pin the CLI. The
drain does not enter raw mode and does not read from stdin (that is the
whole reason `pump_terminal` is gated on a successful start). The
success path is unchanged.

3. **`Engine::new` (item 3):** added a `tracing::warn!` when
`std::env::current_dir()` fails. The CLI already uses `with_base_dir`
exclusively (`internal/main.rs`), so this surfaces the silent loss for
library callers and embedders without changing the CLI's
hard-error-on-missing-base-dir behaviour.

4. **`create_secret` (item 4):** documented the lock + project-scoped
naming contract on the inspect → delete → create sequence at
`internal/engine/secrets/create.rs:103-141`. The cross-invocation case
is closed by the per-project lock; the within-invocation case is closed
by the project-scoped naming (`<project>_...`) and the inspect-time
foreign-secret refusal. No code change.

5. **`install_binary` (item 5):** the L5 fix the issue calls for was
already addressed in #1360 (`chore: tighten install path integrity`).
The current `install_binary` uses `move_target_aside` to rename the
target to a `.old` sibling before the swap and `restore_from_backup` on
self-test failure; the self-test reads from the on-disk backup. Added
one regression test for the chmod-0000 case the issue's test plan calls
out: a binary the operator can no longer read or execute is the
canonical "I have a backup question for the rollback" case, and the L5
path must leave the previous binary in place.

## Test plan

- New unit test:
`install_binary_rolls_back_when_the_target_is_unreadable` at
`internal/update/install/tests.rs` exercises the chmod-0000 → self-test
fails → rollback path.
- 1580 tests pass on `cargo test --lib
--features=watch,completions,update` (1579 + 1 new).
- `cargo build`, `cargo fmt --all -- --check`, `cargo clippy --lib` all
clean. The two pre-existing `-D warnings` dead-code errors on
`internal/libpod/types/container/response.rs` are present on
`origin/develop` and are not introduced by this PR.

## Note on the issue

The issue describes `install_binary` reading the current binary with
`.ok()` and having no on-disk backup. That state was addressed in #1360
(`move_target_aside` + `restore_from_backup`); the current code in this
PR preserves the on-disk `.old` backup through the swap window. The new
chmod-0000 test pins the rollback behaviour for the read-forbidden case.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
…art/kill/rm/pause/unpause (#1363) (#1388)

Closes #1363.

## What's wrong

`stop`, `start`, `restart`, `kill`, `rm`, `pause`, `unpause` each call
`live_service_containers(name)` or `live_replica_names(name, service)`
once **per service** before acting on it. Each call is one
`GET /containers/json` filtered by `podup.service={name}`. A bulk
`list_project_containers_by_service()` exists at
`internal/engine/lifecycle/scale.rs:275-302` and `down` already uses
it; the per-service lifecycle commands do not.

## What I changed

- Renamed the bulk helper to `Engine::live_project_replicas` (the same
  shape `down` was already using), and updated its doc comment to pin
  down the `all=true` requirement — libpod's container-list defaults to
  `runningOnly` when no `status=` filter is supplied, so a bare project
  GET would silently drop the very containers `start`/`restart`/
  `kill`/`rm`/`pause`/`unpause` need to act on.
- Switched the seven per-service lifecycle commands to prefetch the
  project's containers once at the top via the new helper, then read
  per-service slices from the in-memory `HashMap`. The per-service
  `*_one_service` functions now take `Vec<String>` instead of doing
  their own query. For a 100-service project the container-list GET
  count drops from S+1 to 1.
- `stop_container` now returns `Result<bool>` (transitioned vs.
  already-stopped/gone) so the `acted` flag in `stop_one_service`
  stays accurate now that we no longer filter by per-container state
  client-side — the bulk helper returns names without states. The
  existing drop-recheck tests still match the Ok/Err shape and now
  exercise the new bool via the existing "lost stop response"
  fixtures.
- `state_is_active` (formerly the `stop` state filter) is no longer
  used in production code. Kept it with `#[allow(dead_code)]` for the
  unit test that pins its truth table — the test is the only thing
  that still documents the running/paused-vs-anything-else split.

## Tests

- New unit test:
`live_project_replicas_returns_every_project_container_including_stopped`
  in `internal/engine/lifecycle/scale.rs`. Synthesises a 100-service
  project with 5 stopped replicas, asserts the returned map has every
  service (100 entries, 100 container names, the 5 stopped services
  included), AND asserts the bulk GET request shape (carries `all=true`,
  no `status=` filter workaround).
- All 1580 unit tests pass (`cargo test --all-features --lib`).
- All integration tests pass (`cargo test --all-features`).
- `cargo fmt --all -- --check` and
  `cargo clippy --all-features --all-targets -- -D warnings` are
  green.

## Test plan (per the issue)

- **Integration**: a 100-service project with 5 stopped services;
  `stop` issues 1 bulk GET (with `all=true`) and 100 stop POSTs (vs.
  200 GETs + 100 stop POSTs today). The unit test pins both halves of
  that on a fake-libpod server.
- **Lane**: needs real-Podman 5 and 6 to confirm the wire shape matches
  what the fake produced. Tagged `prio:P1` so the live lane runs the
  full lifecycle command surface against the changed code paths.

## Out of scope

- The libpod connection pool is a separate issue (#1362).
- The per-service `create`/`start`/`healthcheck` fan-out is not
  addressable in this shape (different concern, different filter).
- Query commands (`logs`, `inspect`, `ps`) still use the per-service
  `live_replica_names`. They could fold into the same bulk helper
  later, but the issue explicitly scopes this PR to the seven
  state-changing lifecycle commands.

## Lane

I have not run the binary against real Podman in this session — the
fix is unverified at the runtime/lane level. The static review, unit
tests (against a fake libpod shaped per the libpod source), the full
integration suite and clippy are green. The live lane is the place
where the wire-shape match against real libpod 5/6 (which the unit
test pins against the fake) actually gets exercised end-to-end.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
#1389)

Closes #1358.

Three compose fields collapse the isolation between a container and its
host, and `podup up` accepted them in the live engine path with no
warning. Quadlet warned-and-dropped; the live engine did not, so the
same compose file behaved differently between `podup up` and `podup
generate quadlet`. Validation against `podman`'s source surfaced two
additional modes (`userns_mode: host`, `container:<id>` namespace
sharing) that podup should warn on.

A pure `check_host_mode(service) -> Vec<ModeWarning>` helper in
`internal/engine/container/host_mode.rs` scans every active mode and
returns one warning per mode. `Engine::up` (and the `run`/`exec` paths
that surface container config) call it after the spec is built and emit
a `tracing::warn!` per warning. The Quadlet path keeps warn-and-drop for
`pid`/`ipc`/`uts`/`cgroup` (where the unit file semantics require the
drop) and adds a `tracing::warn!` on the host-network and privileged
arms before the emit. The same detector feeds both paths so the messages
match.

A new global `--no-warn` flag suppresses these warnings on
`up`/`create`/`run`/`exec` and on `generate quadlet` — operators who
wrote the compose file deliberately use it to silence the per-run copy.
Default: warn. The live path honours the flag via
`Engine::with_no_warn`; the Quadlet path uses a `NoWarnGuard`
thread-local around `write_quadlet` so `generate_at` stays a
non-breaking free function. `podup config` still surfaces the active
modes at the default log level — that command is the "show me what will
happen" path, where the warning is the whole point.

Tests: each mode present triggers a warning, each mode absent stays
silent, `container:<id>` namespace sharing triggers a warning,
`userns_mode: host` triggers a warning, `--no-warn` suppresses the
engine and Quadlet warnings, and `--no-warn` against `podup config`
still surfaces the modes. Unit coverage is in
`internal/engine/container/host_mode.rs` (9 cases) and
`internal/quadlet/mod.rs::no_warn_tests` (3 cases); CLI end-to-end
coverage is in `tests/host_binding_warnings.rs` (9 cases).

---------

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
## Summary

I split the `walk_dir` / `walk_collect` filesystem helpers out of
`internal/engine/mod.rs` into a new `internal/engine/walk.rs` module so
`mod.rs` falls under the 500-line hard cap the org's `line-limit`
reusable enforces (#1386 follow-up; this same check was the one that
blocked merging #1390 — the per-PR lane flagged the regression after the
`Engine` field additions in the #1364 cache PR pushed it to 503 code
lines).

The change is mechanical: the two functions move verbatim, with the same
semantics and the same private-in-crate visibility, and one new unit
test pins the sorted order on a tempdir.
`internal/engine/build/context.rs` and `internal/engine/watch/sync.rs`
updated to import from the new module.

## Validation

- `cargo build --locked --bin podup --features test-helpers`
- `cargo test --locked --lib --features test-helpers` (1594 passed, 0
failed)
- `cargo test --locked --bins --features test-helpers` (87 passed, 0
failed)
- `cargo fmt --all --check`
- The `line-limit` check the per-PR lane runs now reports `mod.rs` at
486 code lines (under the 500 hard cap), down from 503.

The real-Podman integration lane is the final runtime check for both
supported Podman majors.

Closes the merge blocker on #1390 (the `Engine` field additions for the
cached project label filter in the #1364 PR pushed `mod.rs` over the
line cap, which the per-PR lane then flagged as a required-check
failure).

## Out of scope (deferred)

The next-largest files — `internal/engine/lifecycle/mod.rs` (497) and
`internal/engine/copy/archive.rs` (478) — are within the hard cap; they
stay.

Signed-off-by: Jaro-c <jaroc@glyndor.net>
Co-authored-by: Jaro-c <jaroc@glyndor.net>
## Summary

I split the libpod client methods into focused HTTP verb modules while
keeping the client state, transport plumbing, and status handling in
`internal/libpod/client/mod.rs`. The public `Client` API remains
unchanged and every production file stays below the repository's hard
line limit.

## Validation

- `cargo build --locked --bin podup --features test-helpers`
- `cargo test --locked --lib --features test-helpers`
- `cargo test --locked --bins --features test-helpers`
- `rustfmt --check --edition 2021 internal/libpod/client/mod.rs
internal/libpod/client/get.rs internal/libpod/client/post.rs
internal/libpod/client/put.rs internal/libpod/client/misc.rs
internal/libpod/client/delete.rs`

The real-Podman integration lane remains the final runtime check for
both supported Podman majors.

Closes #1386

Signed-off-by: Jaro-c <jaroc@glyndor.net>
Co-authored-by: Jaro-c <jaroc@glyndor.net>
## Summary

I closed the correctness findings called out in #1369, each as its own
commit so the diff matches the original list:

- `confirm_lost_response` compared `state == Some("running")` literally
— switched to `eq_ignore_ascii_case` so a future libpod returning
`Running` or `RUNNING` does not produce a false negative. The state
field is lowercase today (validated against
`libpod/define/containerstate.go::ContainerStatus.String()`); the
case-insensitive test is forward-compat.
- `is_exec_teardown_noise` swallowed stderr frames on the bare substring
conjunction `unixpacket` + `connection reset by peer` — narrowed to
`read unixpacket` + `connection reset by peer` so a real Go program that
logs `dial unixpacket: connection reset by peer` is no longer
suppressed. The system prefix is the discriminator.
- `libpod_pull_policy` warned and silently fell back to `missing` for an
unknown value — a typo'd `pull_policy: alaways` previously meant the
user got a different artefact than the compose file asked for on every
`up`. The warn-and-default policy was a footgun on the only knob the
user can set here, so an unknown value is now a hard error with the
accepted list in the message.
- `Engine::run_lifecycle_hook` held the stdout lock for the whole stream
— that serialised the hook behind any other writer for its entire
lifetime. Switched to a per-frame lock symmetric with stderr.

## Validation

- `cargo build --locked --bin podup --features test-helpers`
- `cargo test --locked --lib --features test-helpers` (1594 passed, 0
failed; new test `resolved_pull_policy_rejects_an_unknown_value` covers
the typo case)
- `cargo test --locked --bins --features test-helpers` (87 passed, 0
failed)
- `cargo fmt --all --check`
- `cargo clippy --locked --all-targets --all-features -- -D warnings`

The real-Podman integration lane is the final runtime check for both
supported Podman majors.

Closes #1369

## Out of scope (deliberately deferred)

The remaining items in #1369 are tracked separately:

- `XDG_CONFIG_HOME` / `HOME` for Quadlet install trust — see #1356 /
#1360 already in flight; a separate refactor is needed for the install
trust path.
- `PodmanError::Hyper(e).source()` not surfacing the underlying io cause
— already addressed indirectly by `stream_end_kind` and the existing
`body_ended_early` chain walker; the generic `Display` could be widened
but is a larger API change.
- `read_capped` errors losing the cap context — already named by a
separate issue and the `Error` enum change has broader call-site impact
than this PR can carry.
- `DOCKER_HOST` precedence — the existing docs page already lists the
precedence; tightening to a warning would add noise to every CI run that
exports `DOCKER_HOST` and gain little.
- `is_label_only` `#[non_exhaustive]` — clippy is not currently catching
the omission, and `#[non_exhaustive]` is a public-API change that needs
its own issue.

---------

Signed-off-by: Jaro-c <jaroc@glyndor.net>
Co-authored-by: Jaro-c <jaroc@glyndor.net>
…w cannot say (#1370) (#1393)

## Summary

I extended `docs/security-model.md` to codify the trust boundaries, the
limits of static review, and what the live integration lane actually
catches — the four sections the static-review audit flagged as missing
in the public docs (#1370):

- **What podup trusts libpod to defend** — the in-crate validators that
filter every cross-layer transition (names, project names, URL paths,
Quadlet values, signal names, pull policies, timeouts, self-update
bytes).
- **What podup does not defend** — the conscious gaps: `cap_add`/`pid:
host`/`network_mode: host`/`runtime:` forwarded to libpod;
compose-sourced paths unconfined by design (Makefile posture); the
point-in-time lifetime of `file:` secrets; the limits static review
cannot close.
- **What the live integration lane validates** — Fedora qemu VM with
full systemd, per-major matrix, `PODUP_REQUIRE_PODMAN=1` to close the
skip-as-pass green path, the per-major `podman-known-failures-<major>`
as a classification not a count, the `--test-threads` cap that trades
wall-clock for flake control.
- **What static review cannot tell us** — the questions only live
testing answers: whether libpod's per-field validators actually reject
what podup forwards (the integration lane); whether the CDN re-sign
rollback attack works (`install.sh:verify_version_self_test` /
`install.ps1:Test-StagedVersion`); whether the streaming race conditions
are reachable (fuzz targets and the live lane).

## Validation

- The page renders without broken links (`docs/security-model.md` is
already linked from `README.md` line 168).
- A reviewer who has not read the codebase can read the page and know
what podup does and does not defend.
- `cargo fmt --all --check`, `cargo test --locked --lib --features
test-helpers` (1593 passed, 0 failed).

The change is documentation only; no code or test shifts. The README's
existing link to the page is unchanged.

Closes #1370

Signed-off-by: Jaro-c <jaroc@glyndor.net>
Co-authored-by: Jaro-c <jaroc@glyndor.net>
## Summary

I added a scheduled workflow that runs the live-Podman coverage path on
the `develop` branch and applies the same 88% floor the `main` nightly
uses (#1380). The merge path `task branch → develop → release PR → main`
made a regression landing on `develop` invisible to the `main` nightly
until the next release PR; this is the early-warning signal the issue
asked for.

The new file is a thin copy of `.github/workflows/podman-lane.yml` with
three differences:

- `ref: develop` instead of the default branch.
- Cron `0 21 * * *` (12h after the `main` nightly's `0 9 * * *`) so the
two never collide on runner capacity.
- Matrix pinned to the Podman 6 leg only (the full-support cross-check
is still the per-PR lane; Podman 5 and 6 both measure the suite at
~91.5%, so gating the older major would be redundant per #1326's own
recommendation).

## What this is NOT

- Not a replacement for the `main` nightly. The `main` nightly is the
production-truth signal; this is the early-warning signal.
- Not a status check on PRs. PR coverage stays off (a second
instrumented build per PR is too expensive per #1326).
- Not a per-package floor. The lane measures the full suite; per-package
floors would require a different gate shape.
- Not auto-merge of regression fixes from the alert. The floor's job is
to surface the alert, not to fix it.

## Validation

- `cargo fmt --all --check`, `cargo test --locked --lib --features
test-helpers` (1593 passed, 0 failed), `cargo test --locked --bins
--features test-helpers` (87 passed, 0 failed), `cargo clippy --locked
--all-targets --all-features -- -D warnings` — the workflow file is the
only change.
- The workflow itself is verified by the schedule and by the
`workflow_dispatch` path the `main` nightly already uses. The first
nightly fires at 21:00 UTC.

Closes #1380

Signed-off-by: Jaro-c <jaroc@glyndor.net>
Co-authored-by: Jaro-c <jaroc@glyndor.net>
## Summary

I closed the hot-path allocation cleanups called out in the static
review (#1364), each as its own commit so the diff can be reviewed in
the same shape as the original list:

- `interpolate_value` rebuilds every YAML `Mapping` unconditionally
(#1364 — gate the rebuild on real work).
- The progress ticker re-probes terminal capabilities every 100 ms
(cache once at `begin`).
- `identity_style` allocates a `format!("{p}-")` per call (cache the
prefix in `PROJECT_PREFIX`).
- `config_hash` double-serializes the Service via `to_value` then
`to_vec` (load-bearing for map field order — see the `to_vec` test; the
canonical-serialise is required to keep the hash stable).
- `restart_service_set` double-clones service names (return
`Arc<HashSet<String>>` and share the empty-targets half).
- `join_bounded` sorts per dependency level (documented as
sub-microsecond at 100 services).
- `from_utf8_lossy` per log frame in `run` (use `std::str::from_utf8`
first; `write_frame` helper).
- `progress_line` takes a global mutex per emission (`SESSION_OPEN`
`AtomicBool` short-circuit).
- `read_body` copies `Bytes` into a `Vec` (not changed — kept as a `Vec`
because the call site already needs owned bytes for the next step; see
the comment in `read_body`).
- Filter JSON serialized fresh at every list call (cache the URL-encoded
`{"label":["podup.project={name}"]}` on `Engine`; add
`project_label_filter_with` for the dynamic sites that need a second
label).
- `resolve_levels` / `resolve_order` rebuild the same HashMaps per
command (not changed — out of scope for this iteration; left as a
follow-up if the per-call work ever shows up in a profile).

## Validation

- `cargo build --locked --bin podup --features test-helpers`
- `cargo test --locked --lib --features test-helpers` (1602 passed, 0
failed)
- `cargo test --locked --bins --features test-helpers` (87 passed, 0
failed)
- `cargo fmt --all --check`
- `cargo clippy --locked --all-targets --all-features -- -D warnings`

The real-Podman integration lane is the final runtime check for both
supported Podman majors. No production file exceeds the 500-code-line
hard limit; every committed file matches the formatting standard.

Closes #1364

---------

Signed-off-by: Jaro-c <jaroc@glyndor.net>
Co-authored-by: Jaro-c <jaroc@glyndor.net>
## Summary

I re-validated podup against the Podman 6.0.2 baseline the auto-filer
flagged in #1396, then bumped the version files.

Validation:
- Dispatched the `podman-lane` workflow against `develop` (run
31313391002). The Podman 6 leg ran the full integration suite on a fresh
Fedora rawhide cloud image (the `Podman 6.0.2` source packages are now
in rawhide; the lane pulled
`Fedora-Cloud-Base-Generic-Rawhide-20260809.n.0.x86_64.qcow2`). The VM
resolved to `podman 6.1.0-rc1` (rawhide moves fast — the same OS image
carried `6.0.2` a few days ago, per the issue's note).
- Result: `PODUP_SUMMARY pass=179 fail=0 flaky=0` on the Podman 6 leg,
identical to the `6.0.1` baseline. No regressions, no new identities on
the per-major known-failures list.
- The Podman 5 leg also passed (parallel run; no separate test plan
needed — `5.x` is still in support and the per-PR lane runs both).

Bumps:
- `.github/podman-baseline`: `6.0.1` → `6.0.2`.
- `.github/workflows/podman-lane.yml` comment: the nested-virt
measurement note (which the lane itself walks back from) is bumped to
`6.0.2` so the next person who reads the file sees the right baseline.

## Validation

- The lane ran end-to-end green (Pods 5 + 6); the per-PR lane on the
next PR will rerun it for the integration diff, and the
`podman-lane-develop-nightly` (#1394) keeps the `develop` baseline
covered every 24h.
- No code changes; the only files in the diff are the two version bumps.

Closes #1396

Signed-off-by: Jaro-c <jaroc@glyndor.net>
Co-authored-by: Jaro-c <jaroc@glyndor.net>
…mber (#1398)

## Summary

The `podman-lane-develop-nightly` workflow (#1394) runs the full suite
on Podman 6 every 24h, but the coverage number never reached the gate
step. The cause: the script that runs in the VM emits the coverage only
as the last column of the `tail -20 /tmp/cov.log` output, while the
gate's `grep -oE 'PODUP_COVERAGE=[^ ]+'` is looking for a
`PODUP_COVERAGE=<pct>` line of its own. The grep misses, the gate treats
the run as a missing number, and exits 0 with a `::warning::` — so the
early-warning signal #1380 added is silently no-op'd on every run.

Discovered by dispatching the workflow manually after the Podman 6.0.2
baseline bump: the lane reported `success` but the warning was there in
the logs, and the `awk` extracted `91.31%` correctly from the
cargo-llvm-cov summary — the number was computed, just never emitted in
the format the gate parses.

The fix is one `print` and one comment: the `awk` now emits
`PODUP_COVERAGE=91.31%` (not just `91.31%`) so the gate's regex matches,
and the comment names the failure mode so the next person who reads the
file sees it.

## Validation

- Manual dispatch of the workflow after the fix would show the gate
reading the number; the auto-nightly at 21:00 UTC is the next non-manual
test.
- The change is in a workflow that previously exited `success` while
signalling a `::warning::` — the new behaviour is "exit `success`
because the gate ran and read the number", which is the contract #1380
intended.
- No code or test changes; the only file is the workflow YAML.

This is a follow-up to the `develop nightly` workflow from #1394 / PR
#1394, not a change to the per-PR `podman-lane`.

Signed-off-by: Jaro-c <jaroc@glyndor.net>
Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
@Jaro-c Jaro-c closed this Aug 9, 2026
@Jaro-c
Jaro-c deleted the release/3.7.0 branch August 9, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant