Geo: ContainerRepositorySync raises NoMethodError on manifests without layers/manifests/blobs
Summary
Geo::ContainerRepositorySync#list_blobs raises
NoMethodError: undefined method 'filter_map' for nil when a container
manifest has none of layers, manifests, or blobs set. The error is
swallowed by the per-tag rescue StandardError at
L29-32,
so sync continues for other tags — but the affected tag never replicates.
The failure surfaces only as an error log line.
Error in production Geo logs
class: Geo::ContainerRepositorySync
severity: ERROR
message: Error while syncing tag <tag-name>: undefined method `filter_map' for nilRoot cause
ee/app/services/geo/container_repository_sync.rb#L107-113:
def list_blobs(manifest)
blobs = (manifest['layers'] || manifest['manifests'] || manifest['blobs']).filter_map do |blob|
blob['digest'] unless foreign_layer?(blob)
end
blobs.push(manifest.dig('config', 'digest')).compact
endIf manifest['layers'], manifest['manifests'], and manifest['blobs']
are all absent, the || chain evaluates to nil, and .filter_map is
called on nil.
Manifest shapes that can hit this:
- Cosign / Notation signatures and attestations — minimal artifact
manifests, often with just
config+subject+annotations, no layers/blobs array. - OCI 1.1 referrers — referrer manifests that point at a
subjectand carry the payload elsewhere. - Empty / config-only manifests produced by some artifact tooling.
- Any non-image, non-index manifest type not anticipated by the code.
The tag name pattern observed in the affected environment
(<version>-<build>) is consistent with build-attached signature or
attestation artifacts.
Proposed fix
Default the descriptor list to [] so the method tolerates manifests
without these arrays — treat as "no blobs to pre-sync" rather than
crashing. push_manifest at line 74 will still run for the tag itself,
and config.digest (line 112) is still picked up if present:
def list_blobs(manifest)
descriptors = manifest['layers'] || manifest['manifests'] || manifest['blobs'] || []
blobs = descriptors.filter_map do |blob|
blob['digest'] unless foreign_layer?(blob)
end
blobs.push(manifest.dig('config', 'digest')).compact
endTest gap
Existing tests in
ee/spec/services/geo/container_repository_sync_spec.rb
cover Docker V2 manifest, OCI image manifest, Docker manifest list, OCI
image index, buildkit cache, and OCI artifact-with-manifest — all of
which have at least one of layers / manifests / blobs populated.
Add a spec for the "none of the above" shape, e.g.:
let(:bare_artifact_manifest) do
%(
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": { "mediaType": "application/vnd.example", "digest": "sha256:abc", "size": 12 },
"subject": { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:def", "size": 34 }
}
)
endAssert sync completes without raising and push_manifest is called for
the tag with the correct Content-Type. Optionally assert that the
config.digest blob is synced.
Recovery for already-affected tags
Once the fix is deployed, mark the relevant Geo::ContainerRepositoryRegistry
rows as pending (or trigger via Geo admin UI / API) to re-sync the
previously-failing tags.
Related
- #600486 (closed) — Geo container repository sync silently skips OCI image index tags (same file, distinct symptom; both are robustness issues in the same service and could land in coordinated MRs).