Do not require os in recipe manifest Platform blocks (#974) - #1158
Closed
allanli4 wants to merge 1 commit into
Closed
Do not require os in recipe manifest Platform blocks (#974)#1158allanli4 wants to merge 1 commit into
allanli4 wants to merge 1 commit into
Conversation
The recipe reference documents os and architecture as optional, but manifest_selection() used the presence of the os key to gate the entire platform-match and lifecycle-selection body. A manifest omitting os was therefore skipped rather than matching any OS, so such a recipe failed artifact resolution, unit file generation and lifecycle execution. Treat an absent os as a wildcard, matching how an absent architecture is already handled. This also makes the architecture type check reachable when os is omitted.
Member
Author
|
Superseded by #1160, which carries this same change plus the This PR was raised from a fork branch, and Closing in favour of #1160. No review had started here, so no review effort is lost. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue
#974 —
"ggdeploymentd: Do not require os and architecture fields in component recipes"
Reported by: @aws-kevinrickard
Problem
The AWS component recipe reference documents
osandarchitectureas optionalattributes of a manifest
Platformblock, but nucleus lite behaves as ifosisrequired. A manifest such as:
is silently treated as incompatible with the current platform. Expected
behaviour, per the reporter, is to match the non-lite nucleus and impose no
requirement when the fields are absent.
Scope
The error quoted in the issue —
doesn't claim platform {runtime=aws_nucleus_lite, os=linux, architecture=amd64} compatibility— is emitted by cloud-side
ResolveComponentCandidates, which is not in thisrepository. This PR fixes the local platform-matching path, where the same
documented contract is violated independently and observably: a recipe omitting
oscurrently fails artifact resolution, systemd unit generation, and lifecycleexecution on-device. The platform attributes nucleus lite sends to the cloud
(
modules/ggdeploymentd/src/deployment_handler.c:1152-1157) are unchanged — theyalready populate all three fields.
Acceptance criteria
All criteria are DERIVED — the issue states none explicitly.
Platformwith a lite-compatibleruntimebut noosis compatible on Linuxrecipe.c:399-411; testselect_manifest_platform_without_osPlatformwith noarchitectureimposes no architecture requirement (no regression)architecture.len == 0branch preserved byte-identical; testselect_manifest_platform_without_architecturePlatform: { runtime: "*" }(both omitted — the issue's exact input) is selected on Linuxselect_manifest_platform_without_os_or_architecture— assertsGG_ERR_OKand deep map equality of the selected manifestruntime; non-literuntime; explicit non-matchingos; explicit non-matchingarchitecture— all still skippedwithout_runtime,with_other_runtime,with_other_os,with_other_architectureos/architecturestill yieldsGG_ERR_INVALIDwith_invalid_os,with_invalid_architecture. See "Behaviour changes" — forarchitecturethis is a strictness increaseGG_TEST_DEFINEin the module's own#ifdef GG_SDK_TESTINGblock, run byctestasggl-recipe-inline-test. No new framework or targetRoot cause
manifest_selection()inmodules/ggl-recipe/src/recipe.ccarries a two-channelcontract: the return value answers "did I see malformed input?", while the
out-parameter answers "did this manifest match?".
GG_ERR_OKis thereforeoverloaded — it is returned both on a match and on a deliberate skip — so any
path that exits without writing the out-parameter is read by callers as "skip
this manifest". Skip is reached by omission rather than by decision.
At the base revision,
if (gg_map_get(platform, GG_STR("os"), &os_obj))(
recipe.c:400) used the presence of theoskey as the gate around the entireremaining compatibility-and-selection body (
recipe.c:401-484) — which containsevery statement that can assign the out-parameter. With
osabsent, the wholebody was skipped, the function returned
GG_ERR_OKhaving selected nothing, thecaller's error guard did not fire, and the post-loop
NULLcheck producedNo Manifest was found for linux(recipe.c:609) →GG_ERR_FAILURE.The comment at
recipe.c:398—// If OS is not provided then do nothing—described the code accurately and the requirement inaccurately: "impose no OS
requirement" was implemented as "do nothing", and in this function doing nothing
means being skipped. Tellingly,
architectureone level down was alreadyimplemented correctly (absent → zero-length buffer → accepted at
recipe.c:434),but only reachable when
oswas present.The prediction this hypothesis made, and it held: making an absent
osyielda wildcard instead of skipping the body, with everything downstream semantically
unchanged, makes the failure disappear. It did, on the first attempt.
Set aside, one line each:
platform_is_compatible()predicate (the alternative fix shapeproposed during localization). Rejected: it bundles a refactor with a bug fix,
which
docs/CONTRIBUTING.mdexplicitly asks contributors not to do, andmanifest_selection()already carries// TODO: Refactor it, so maintainershave scoped that separately. Its one correctness argument — the unreachable
architecturetype check — is delivered by this fix anyway. Offered as afollow-up below.
manifest_selection()never readsarchitecture.detail, though nucleus liteadvertises it. A real defect, but a different one — not folded in.
The fix
One file:
modules/ggl-recipe/src/recipe.c.The production change is four edits inside
manifest_selection():GgObject *os_obj;→GgObject *os_obj = NULL;so absence is representable.gg_map_get(... "os" ...)block now contains only the non-string typecheck; it no longer gates anything.
osdefaults toGG_STR("*"), i.e. "no OS requirement", appliedwith the same optional-fetch shape the adjacent
architectureblock alreadyuses.
recipe.c:332already doesGgBuffer arch_detail = GG_STR("");, so thisis established local style.
Why this is minimal: the wildcard default means the OS predicate itself needs
no edit at all. The runtime gate, the OS predicate, the architecture
fetch/validate/default, the architecture predicate, the
Lifecyclepick, theSelectionspick, and thealldefault are all byte-identical apart fromindentation —
git diff -wreduces the production change to exactly the fouritems above. The remainder of the diff is the unavoidable re-indent plus tests.
No public signature changes;
manifest_selectionisstatic, andmodules/ggl-recipe/include/ggl/recipe.his untouched.Blast radius of the bug being fixed — one root cause, four entry points:
modules/ggdeploymentd/src/deployment_handler.c:722ggl_get_recipe_artifacts_for_platformmodules/recipe2unit/src/unit_file_generator.c:499select_linux_lifecyclemodules/recipe-runner/src/runner.c:385select_linux_lifecyclemodules/ggl-docker-client/src/docker_artifact_cleanup.c:116,:260ggl_get_recipe_artifacts_for_platformBehaviour changes worth accepting knowingly
Both follow from the fix being correct, and both are user-visible:
Manifests: [{runtime: "*"}, {runtime: "*", os: linux, architecture: amd64}]the first entry used to be skipped and the second selected; the first is now
selected. That is correct first-match semantics and matches classic nucleus,
but it changes which
Lifecycle/Artifactsblock a real deployment picks.Platform: {runtime: "*", architecture: 42}with noosused to be silentlyskipped, letting a later valid manifest win. Because the
architecturetypecheck was also nested inside the
osgate, de-nesting makes it reachable, sothis now returns
GG_ERR_INVALIDand fails the recipe. Consistent with how amalformed
osalready behaved, and it closes criterion 5, but it is astrictness increase beyond the reported defect.
Evidence
Oracle — the failing test that now passes:
Test:
modules/ggl-recipe/src/recipe.c::select_manifest_platform_without_os_or_architectureCommand:
ctest --test-dir <build> -R ggl-recipe-inline-test --output-on-failureBefore the patch, at base revision
b33c36822f2f5f9b8ea873954142bfa98dff7aaa:After the patch: passes. That symptom no longer occurs.
ggl-recipe-inline-testreports19 Tests 0 Failures 0 Ignored / OK.Reproduction was deterministic (1/1); the defect is a pure control-flow branch on
map-key presence, with no timing or I/O dependence.
Regression run — build and test:
cmake --build <build> -j$(nproc)ctest --test-dir <build> --output-on-failure100% tests passed, 0 tests failed out of 10nix flake check -Lsuite was clean at base, so there are none in the recorded command set. Two
environment-specific ones were found by extra checks and left alone; see below.
Checks that could NOT be run, and why — this is the honest gap in
verification, and a maintainer with CI should simply let the gate answer it:
nixis not installed on the build host and cannot be, so the repository's realCI entrypoint
nix flake check -Lnever ran. That leaves unverified:clang-tidy,iwyu,cmake-lint,spelling(cspell),build-clang, andbuild-musl-pi. Two partial substitutes were run instead:clang-format(v22.1.8, obtained separately since the host has none): thechanged file's only violation is a pre-existing one at
is_recipe_variable_valid_three_part, which this patch does not touch andwhich is present at base revision. Zero violations in the changed region. A
control run at base revision found that same violation and zero across 40 other
module files, so this version is very nearly — not exactly — the one CI uses. It
was used only as a check, never as a blanket
-ireformat, and the pre-existingviolation was deliberately left alone rather than "fixed".
-D ENABLE_WERROR=1(what CI'sbuild-clangenables):fails with 2 errors, both
'noreturn' function does returnin_deps/unity-src/src/unity.c— vendored third-party Unity source, notrepository code. Proven pre-existing by re-running the identical build with the
patch stashed at base revision: same 2 errors, same 2 lines. It is gcc-specific
(gcc 11.4) and invisible in CI because CI's
-Werrorchecks use clang/muslwhile its
unit-testscheck setsBUILD_TESTINGwithoutENABLE_WERROR, sothat combination never occurs upstream. No repository source file emitted a
single warning under
-Werror, including the patched file.The most material unverified check is
clang-tidy:.clang-tidyhasreadability-function-cognitive-complexityactive andmanifest_selection()still carries a
NOLINTNEXTLINEfor it. This patch removes one nesting level, sothe suppression is either still valid or now unnecessary — that is reasoned, not
measured.
Reviewer verdict
An independent critic reviewed the diff, the acceptance criteria, the rejected
alternative hypothesis, and the test results, without access to the patch
author's self-assessment.
concurAdvisory notes, surfaced rather than acted on. They did not block delivery by
design — the reviewer is advisory, and it is the test run that verified the patch.
os:is still silently skipped — same symptom, onetoken from being fixed. The reviewer reports that the YAML decoder
(
modules/ggl-yaml/src/yaml_decode.c) renders every scalar as a buffer, soos:with no value becomes a zero-length buffer, fails the OS predicate, andstill produces the exact error from this issue — while
architecture:leftempty is honoured as "no requirement". Two sibling optional fields, same
spelling, opposite outcomes. The remedy is one token (accept
os.len == 0, orapply the wildcard when the value is absent or empty). This was left as a
deliberate design decision for maintainers, because no acceptance criterion
covers it, it is not a regression (base revision behaves identically), and
"absent" versus "explicitly empty" is a semantic call this change should not
make unilaterally. If it is intentionally left out, a
GG_LOGDnaming theempty value would at least make the next report diagnosable. This is the one
item worth a maintainer decision in this thread.
use single-manifest fixtures and
GG_TEST_ASSERT_BAD(which only asserts!= GG_ERR_OK), so nothing asserts that a non-matching manifest is skippedwhile the loop continues rather than aborting the whole recipe. A
[non-matching, matching]fixture would close that and lock in first-matchprecedence.
osmismatch and the architecture mismatch say nothing, and the architecturemismatch exits by falling through ~50 lines to the terminal
return GG_ERR_OK.A
GG_LOGDon each skip would have made this issue self-diagnosing.the implicit fall-through into an explicit
else { return GG_ERR_OK; }wouldremove it at near-zero diff cost — without adopting the full predicate
extraction.
select_lifecycle_platform_without_os_or_architectureasserts onlyselected.len > 0where deep equality would be stronger and consistent;assertion style is mixed across the new tests; and encoding "no
osrequirement" as the sentinel
"*"makes the fix depend on"*"remaining inthe predicate, where a
bool os_specifiedwould state the intent directly(a deliberate trade for a smaller diff).
the new assertions; its discrimination claims rest on control-flow reasoning
plus per-test log paths, not on observed red-on-mutation.
For the human reviewer
Focus here:
an explicitly empty
os:be treated as "no requirement" (consistent witharchitecture:) or as a non-match (stricter)? Domain knowledge about non-litenucleus behaviour settles this instantly and would change one line.
hard-failing malformed recipe — are correct consequences of the fix, but they
affect real deployments. Please accept them knowingly.
split into its own change.
clang-tidyagainstthe existing
NOLINTNEXTLINE, andcspellagainst the new identifiers(all ordinary words or existing repository terms).
chosen:
docs/CONTRIBUTING.mdasks for adev/-prefixed branch and, forexternal contributors, a PR against an internally created
dev/branch. Thisbranch is named
wt/LiAllanPersonalAICapabilities-t6and targetsmaindirectly. Happy to re-target or rename.
Branch:
wt/LiAllanPersonalAICapabilities-t6— base revisionb33c36822f2f5f9b8ea873954142bfa98dff7aaa