Skip to content

refactor: Extract Index Child Classification Behind A Registry - #524

Merged
bupd merged 7 commits into
mainfrom
refactor/index-child-classifier
Aug 17, 2026
Merged

bupd merged 7 commits into
mainfrom
refactor/index-child-classifier

Conversation

@Vad1mo

@Vad1mo Vad1mo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The in-toto attestation handling from #85 was called inline from abstractIndexMetadata and implemented as two methods on the abstractor struct. This moves it behind a ChildClassifier seam in a new src/controller/artifact/manifest package.

The index loop now asks the classifier chain whether a child descriptor is a platform child of the index or an accessory of one of its siblings. No attestation-specific branch remains in the loop.

No behaviour change — the same accessories are produced from the same inputs.

Before / after

A two-platform image built with --provenance --sbom, viewed as the children of the index.

Before — four children, two of them unknown/unknown:

before

After — two platform children, each with its attestation nested as an attestation.intoto accessory:

after

What this opens up

  • Index children get a seam. Anything that appears inside an index but is not a platform image — attestations today, SBOMs, signatures or referrers-style layouts tomorrow — is classified in one place instead of by another if in the index loop.
  • The classifier is testable and reusable. It carries its own artifact.Manager and registry.Client via an exported constructor rather than being welded to abstractor, so it can be driven directly with mocks.
  • It shrinks our upstream divergence. The fork's delta here goes from a patch inside abstractIndexMetadata plus two struct-bound methods to a single registered file, which survives the twice-daily upstream cherry-pick job far better. It is also the piece we can offer upstream against BuildKit attestation manifests shown as unknown/unknown in multi-arch artifact list goharbor/harbor#22848 on its own merits.

Why this ordering

goharbor/harbor#22848 is listed as depending on the manifest-abstractor refactor (goharbor/harbor#22847, now goharbor/harbor#23647). It does not: attestation manifests are ordinary OCI image manifests appearing as index children, identified by the vnd.docker.reference.type annotation. They introduce no new manifest media type, so a media-type registry does not unlock them. Descriptor-level classification — this change — does.

Resource bounds

Classification is one call per index rather than one per child, so the sibling list is walked once instead of rebuilt per descriptor. Without that, a 4 MiB index body (common.MaxManifestBodySize) holds thousands of descriptors and makes the walk quadratic in time and allocations, for a request that needs no uploads to construct.

Reading an in-toto payload to resolve a subject is bounded per index at min(len(children), 32) lookups. A well-formed index carries at most one attestation per platform child, so an index with one platform child and 14,000 attestation descriptors gets exactly one lookup. Worst case per index is 64 registry round trips and 128 MiB of reads. Attestations past the budget stay children of the index rather than being dropped.

Measured upstream on 1 platform child + 512 attestations: 12.5 ms / 34.1 MB / 8,230 allocs before, 1.76 ms / 0.74 MB / 7,726 allocs after. A realistic two-platform build is unchanged at one extra allocation.

Raised by Copilot on the upstream PR: goharbor/harbor#23648 (comment).

Deletion and the API contract

Because the attestation is no longer an artifact_reference child, the FK guard that refused deleting an index child directly no longer covers it. controller.Delete therefore rejects deleting an attestation.intoto accessory while its subject exists, with the same 412 the reference row used to produce; deleting the subject or the index cascades it away as before, and orphaned accessory rows stay deletable. Scoped to the attestation type: cosign/notation deletion is unchanged, since those are not index children.

API contract note: tooling that enumerates an index's children via Harbor's API (references[]) no longer sees attestation manifests there — they appear as accessories on the platform child they attest. The registry API is unaffected.

Verified live on a dev instance: attestation DELETE → 412, index delete → full cascade, GC delete_untagged=true leaves attestations of tagged indexes untouched, repository copy attaches accessories to the correct subjects, plain multi-arch indexes unaffected, and the duplicate-attestation index that previously 404ed on push now pushes cleanly.

Naming

Buildah and Podman emit the same attestation layout, so buildKit* identifiers are renamed onto the spec axis, matching the attestation.intoto accessory type that was already generic. The vnd.docker.reference.* constants and all behaviour are unchanged.

Fixed in passing

  • The in-toto payload size is now rejected from the layer descriptor before pulling the blob, instead of after.
  • defer blob.Close() is no longer inside a for loop. It was safe only because the loop returned on the first in-toto layer.
  • Ambiguous subject digests no longer resolve nondeterministically. A subject names one artifact under several algorithms and the digest map iterates in unspecified order, so two entries matching different index children could attach the attestation to an arbitrary artifact. Resolution now requires a single unique match, mirroring how subject-name matching already rejects ambiguity.

Testing

go test ./controller/artifact/ -run TestAbstractorTestSuite green, both attestation cases still exercised end to end. go test ./controller/artifact/manifest/... green, covering the ported helpers, the digest-ambiguity regression, and TestClassifyBoundsSubjectLookups, which pins both the child-count budget and the ceiling. golangci-lint, gofmt and go vet clean.

manifest/attestation.go is byte-identical to the copy in goharbor/harbor#23648, with no //nolint:nilnil on either side: partitioning per index removed the (nil, nil) returns that needed suppressing.

Copilot AI review requested due to automatic review settings July 30, 2026 12:25
@github-actions github-actions Bot added the tests label Jul 30, 2026
@gitar-bot

gitar-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR replaces BuildKit-specific attestation handling with a generic in-toto manifest classifier. It integrates the classifier into artifact abstraction, generalizes attestation tests, and separates accessory-linking errors from artifact conflict recovery.

Changes

Manifest Accessory Classification

Layer / File(s) Summary
In-toto attestation classification
src/controller/artifact/manifest/attestation.go, src/controller/artifact/manifest/attestation_test.go
Adds bounded statement loading, digest and platform-name subject resolution, ambiguity handling, media-type and size validation, and accessory candidate creation. Tests cover classification, lookup limits, unresolved attestations, and deterministic matching.
Artifact metadata integration
src/controller/artifact/abstractor.go, src/controller/artifact/abstractor_test.go
Constructs the in-toto classifier and uses it for index child classification. Attestation fixtures and resolution tests use generic terminology.
Accessory error handling
src/controller/artifact/controller.go
Returns accessory-linking errors separately instead of treating them as retryable parent-artifact conflicts.

Sequence Diagram(s)

sequenceDiagram
  participant Abstractor
  participant InTotoAttestationClassifier
  participant RegistryClient
  participant ArtifactManager
  Abstractor->>InTotoAttestationClassifier: classify index descriptors
  InTotoAttestationClassifier->>RegistryClient: load attestation manifest and statement blob
  InTotoAttestationClassifier->>ArtifactManager: retrieve attestation and target artifacts
  InTotoAttestationClassifier-->>Abstractor: return references and accessory candidate
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main refactoring: extracting index child classification behind a registry.
Description check ✅ Passed The description thoroughly explains the refactoring, behavior, performance bounds, API impact, testing, and related changes.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/index-child-classifier
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch refactor/index-child-classifier

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Refactors OCI index child handling by introducing a ChildClassifier registry/chain in src/controller/artifact/manifest, moving in-toto attestation classification out of the abstractor and into a dedicated, registerable classifier.

Changes:

  • Added a ChildClassifier interface + global classifier chain (ChildClassifiers) with RegisterChildClassifier / ClassifyChild.
  • Implemented and auto-registered an InTotoAttestationClassifier to classify in-toto attestation manifests as accessories.
  • Updated index abstraction to delegate child classification to the classifier chain and removed the previous BuildKit-specific implementation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/controller/artifact/manifest/classifier.go Introduces the classifier interface, global chain, and chain evaluation function.
src/controller/artifact/manifest/classifier_test.go Adds tests for default registration and classifier chain behavior.
src/controller/artifact/manifest/attestation.go Adds the in-toto attestation classifier and registers it in init().
src/controller/artifact/manifest/attestation_test.go Updates/ports helper tests to the new manifest package and renamed identifiers.
src/controller/artifact/buildkit_attestation.go Removes the old BuildKit-specific attestation implementation previously tied to abstractor.
src/controller/artifact/abstractor.go Switches index child handling to call manifest.ClassifyChild and avoids manifest identifier shadowing.
src/controller/artifact/abstractor_test.go Updates fixtures/names and wires tests through the new classifier chain.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/controller/artifact/abstractor_test.go Outdated
Comment thread src/controller/artifact/manifest/classifier.go Outdated

@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/controller/artifact/abstractor_test.go`:
- Around line 410-415: Update the test setup around manifest.ChildClassifiers to
save the existing classifier chain before replacing it, then register an
a.T().Cleanup callback that restores the saved chain after the test. Keep the
mock-backed classifier assignment unchanged during the test.

In `@src/controller/artifact/manifest/attestation.go`:
- Around line 188-194: Update the subject-digest matching logic around
subjectDigests and digestInIndex to collect unique matching digest references
rather than returning the first match; proceed only when exactly one sibling
target is found, and reject or return no result for ambiguous matches. Add a
regression test covering two matching digest algorithms that resolve to
different siblings.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4dff5ed6-2690-4dd5-80f7-2d6f684d90ef

📥 Commits

Reviewing files that changed from the base of the PR and between 83f8201 and a96997f.

📒 Files selected for processing (7)
  • src/controller/artifact/abstractor.go
  • src/controller/artifact/abstractor_test.go
  • src/controller/artifact/buildkit_attestation.go
  • src/controller/artifact/manifest/attestation.go
  • src/controller/artifact/manifest/attestation_test.go
  • src/controller/artifact/manifest/classifier.go
  • src/controller/artifact/manifest/classifier_test.go
💤 Files with no reviewable changes (1)
  • src/controller/artifact/buildkit_attestation.go

Comment thread src/controller/artifact/abstractor_test.go Outdated
Comment thread src/controller/artifact/manifest/attestation.go

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/controller/artifact/manifest/classifier.go Outdated
Comment thread src/controller/artifact/manifest/attestation.go
Comment thread src/controller/artifact/abstractor_test.go Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Preview images for this PR are available in 8gears.container-registry.com/8gcr-pr with tag pr-524.

  • 8gears.container-registry.com/8gcr-pr/harbor-core:pr-524
  • 8gears.container-registry.com/8gcr-pr/harbor-jobservice:pr-524
  • 8gears.container-registry.com/8gcr-pr/harbor-registryctl:pr-524
  • 8gears.container-registry.com/8gcr-pr/harbor-exporter:pr-524
  • 8gears.container-registry.com/8gcr-pr/harbor-portal:pr-524
  • 8gears.container-registry.com/8gcr-pr/harbor-registry:pr-524
  • 8gears.container-registry.com/8gcr-pr/trivy-adapter:pr-524

Verify a preview image:

cosign verify \
  --certificate-identity-regexp="https://github.com/container-registry/harbor-next/.github/workflows/pr-ci.yml@.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  8gears.container-registry.com/8gcr-pr/harbor-core:pr-524

Verify SBOM attestation:

cosign verify-attestation \
  --certificate-identity-regexp="https://github.com/container-registry/harbor-next/.github/workflows/pr-ci.yml@.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  --type spdxjson \
  8gears.container-registry.com/8gcr-pr/harbor-core:pr-524

@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: 1

🤖 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/controller/artifact/manifest/classifier.go`:
- Around line 41-49: The RegisterChildClassifier guard must reject typed-nil
ChildClassifier implementations, not only a nil interface; detect nil underlying
pointers before appending, while preserving the existing log-and-return
behavior. Add coverage in
src/controller/artifact/manifest/classifier_test.go:127-137 by registering a
typed-nil ChildClassifier and asserting it is not appended.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 94abfb25-2689-487b-8693-bd81b9fcf51d

📥 Commits

Reviewing files that changed from the base of the PR and between a96997f and cc0db78.

📒 Files selected for processing (5)
  • src/controller/artifact/abstractor_test.go
  • src/controller/artifact/manifest/attestation.go
  • src/controller/artifact/manifest/attestation_test.go
  • src/controller/artifact/manifest/classifier.go
  • src/controller/artifact/manifest/classifier_test.go

Comment thread src/controller/artifact/manifest/classifier.go Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/controller/artifact/manifest/attestation.go Outdated
The attestation handling added in #85 was implemented as methods on the
abstractor struct and called inline from abstractIndexMetadata, which welds it
to a struct upstream is restructuring in goharbor/harbor#23647.

It becomes a standalone InTotoAttestationClassifier in a new manifest package,
carrying its own artifact.Manager and registry.Client through an exported
constructor. The abstractor holds one and asks it about each index child.

Attestation manifests are not a BuildKit invention - Buildah and Podman emit
the same layout - so the identifiers move onto the spec axis, matching the
attestation.intoto accessory type that was already generic.

Resolve the reference annotation before loading the in-toto payload. Every
normally annotated attestation was pulling a manifest and a blob it did not
need.

Treat a statement naming several index children as ambiguous rather than
taking the first match, and reject ambiguous digests within a single subject,
whose map iterates in unspecified order.

Reject the in-toto payload on its advertised size before pulling the blob, and
stop deferring the blob Close inside a loop.

Keep an accessory failure from being retried as a lost parent-artifact race:
the recovery path re-reads the artifact by digest, which cannot succeed when
the transaction that would have created it has just rolled back.

Signed-off-by: Vad1mo <vadim@8gears.com>
@Vad1mo
Vad1mo force-pushed the refactor/index-child-classifier branch from 87deacc to cfb7f76 Compare July 30, 2026 23:40

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/controller/artifact/abstractor_test.go (1)

599-630: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test doesn't exercise the scenario it claims to test.

The doc comment says this verifies "annotation-based subject resolution works when in-toto subject loading fails," but attestationIndexAnnotationOnly's vnd.docker.reference.digest (cad250bb...) matches the platform child's digest directly. In Classify, if !digestInIndex(children, targetDigest) is therefore false, so loadSubjects/PullManifest is never invoked - the mocked failure on Line 609 is dead code, and this test actually exercises the direct-annotation-hit fast path, not the load-failure fallback.

Note also that the claimed scenario is unreachable as implemented: if the annotation digest isn't in the index and the subsequent load fails, resolveSubjectFromStatement gets no subjects and returns "", yielding no candidate - not the "candidate found" result this test asserts.

Suggest renaming the test/comment to reflect the actual fast-path scenario being tested, and dropping the now-misleading PullManifest failure mock.

🤖 Prompt for 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.

In `@src/controller/artifact/abstractor_test.go` around lines 599 - 630, The test
TestAbstractMetadataOfIndexResolvesFromAnnotation actually covers direct
annotation-based resolution, not subject loading failure. Rename the test and
its comment to describe the direct annotation fast path, and remove the unused
PullManifest failure mock for the attestation digest while preserving the
existing candidate assertions.
🧹 Nitpick comments (1)
src/controller/artifact/abstractor.go (1)

27-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

manifest import shadowed by a local variable of the same name.

This import's default identifier is manifest, but AbstractMetadata (Line 63, unchanged) already declares a local manifest, _, err := a.regCli.PullManifest(...) that shadows it for that function's scope. It compiles today since the package isn't referenced there, but it's a latent trap for future edits within that function.

♻️ Suggested fix: alias the import
-	"github.com/goharbor/harbor/src/controller/artifact/manifest"
+	childmanifest "github.com/goharbor/harbor/src/controller/artifact/manifest"

(and update the two usages accordingly: childmanifest.NewInTotoAttestationClassifier, *childmanifest.InTotoAttestationClassifier)

🤖 Prompt for 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.

In `@src/controller/artifact/abstractor.go` at line 27, Alias the manifest package
import to avoid collision with the local manifest variable in AbstractMetadata,
then update the childmanifest.NewInTotoAttestationClassifier and
*childmanifest.InTotoAttestationClassifier references to use the alias.
🤖 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.

Outside diff comments:
In `@src/controller/artifact/abstractor_test.go`:
- Around line 599-630: The test
TestAbstractMetadataOfIndexResolvesFromAnnotation actually covers direct
annotation-based resolution, not subject loading failure. Rename the test and
its comment to describe the direct annotation fast path, and remove the unused
PullManifest failure mock for the attestation digest while preserving the
existing candidate assertions.

---

Nitpick comments:
In `@src/controller/artifact/abstractor.go`:
- Line 27: Alias the manifest package import to avoid collision with the local
manifest variable in AbstractMetadata, then update the
childmanifest.NewInTotoAttestationClassifier and
*childmanifest.InTotoAttestationClassifier references to use the alias.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9c90fae7-3cc5-4729-8fd1-32b57c6ec18d

📥 Commits

Reviewing files that changed from the base of the PR and between 87deacc and cfb7f76.

📒 Files selected for processing (6)
  • src/controller/artifact/abstractor.go
  • src/controller/artifact/abstractor_test.go
  • src/controller/artifact/buildkit_attestation.go
  • src/controller/artifact/controller.go
  • src/controller/artifact/manifest/attestation.go
  • src/controller/artifact/manifest/attestation_test.go
💤 Files with no reviewable changes (1)
  • src/controller/artifact/buildkit_attestation.go

…Child

The classifier was called for every descriptor and handed the full sibling
slice each time, so platformChildren rebuilt that slice on every call. A
4MiB index body holds thousands of descriptors, which made the walk
quadratic in both time and allocations for a request an attacker fully
controls and does not have to back with any upload.

Each attestation whose annotation named no child also pulled its manifest
and up to 4MiB of in-toto payload, with a cap per statement but none per
index, so the same index could force thousands of synchronous registry
round trips and gigabytes of reads during one push.

Turn Classify into a single call that partitions the children, so the
sibling slice is built once and the payload lookups share one budget of
min(len(children), 32) - a well-formed index carries at most one
attestation per platform child. Attestations beyond the budget stay
children of the index rather than being dropped.

Measured upstream on an index of 1 platform child and 512 attestations,
abstraction goes from 12.5ms and 34.1MB to 1.76ms and 0.74MB. A realistic
two-platform build is unchanged at one extra allocation.

Classify no longer returns (nil, nil), so the three //nolint:nilnil
suppressions are gone and attestation.go is byte-identical to the upstream
copy in goharbor/harbor#23648.

Signed-off-by: Vadim Bauer <vb@8gears.com>
Signed-off-by: Vad1mo <vadim@8gears.com>

@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: 1

🤖 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/controller/artifact/manifest/attestation.go`:
- Around line 44-49: The maxSubjectLookups limit currently bounds only payload
lookups, while annotation-resolved candidates can still be created repeatedly
for the same subject. Update Classify and its candidate handling to track
claimed sub-artifact digests, allow only the first candidate for each target,
and classify later duplicate annotation targets as references instead of issuing
additional GetByDigest lookups.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f8f49ed-2db7-431e-af80-37487a0121b0

📥 Commits

Reviewing files that changed from the base of the PR and between cfb7f76 and 1d08b91.

📒 Files selected for processing (3)
  • src/controller/artifact/abstractor.go
  • src/controller/artifact/manifest/attestation.go
  • src/controller/artifact/manifest/attestation_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/controller/artifact/abstractor.go

Comment thread src/controller/artifact/manifest/attestation.go Outdated

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/controller/artifact/manifest/attestation.go">

<violation number="1" location="src/controller/artifact/manifest/attestation.go:88">
P2: Payload-resolved attestations after this budget are recorded as ordinary index references instead of accessories. A one-platform index with multiple missing/invalid subject annotations already triggers this after the first descriptor; preserve their classification or fail abstraction explicitly rather than silently changing stored metadata based on descriptor order.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/controller/artifact/manifest/attestation.go Outdated
Vad1mo added 5 commits July 31, 2026 13:29
Take the upstream wording for the AccessoryCandidates field and the
AccessoryCandidate type verbatim, so pkg/artifact/model.go is byte-identical
to the copy in goharbor/harbor#23648 and later merges have nothing to
reconcile here.

The upstream text is the better of the two: it says why the abstractor
cannot persist the accessory itself, which is the constraint the whole
candidate indirection exists for. The struct re-indents because a comment
at field indentation splits gofmt's alignment group; there is no code
change.

Signed-off-by: Vadim Bauer <vb@8gears.com>
Signed-off-by: Vad1mo <vadim@8gears.com>
…ream

goharbor/harbor#23648 grew these two cases after review pointed out that
nothing exercised the accessory write from the controller side. The fork
had the controller.go hunk but not the tests, so the branch that carries
the seam had no coverage of it.

TestEnsurePersistsAccessoryCandidates asserts the candidates the abstractor
emits reach accessoryMgr.Ensure inside the artifact-create transaction.
TestEnsureReturnsAccessoryError asserts an accessory conflict surfaces as
itself instead of being mistaken for the parent-artifact race the recovery
path below it handles.

The bodies are upstream's verbatim; each gains the setupXxx calls this
suite requires, since SetupTest here nils every mock and each test opts
into the ones it needs.

Signed-off-by: Vadim Bauer <vb@8gears.com>
Signed-off-by: Vad1mo <vadim@8gears.com>
…ubjects

Two follow-ups from bot review of the previous commit.

Deriving the budget from len(children) assumed one attestation per platform
child. Tools are free to publish SBOM and provenance as separate manifests
against the same image, and a single-platform index of those would have
resolved only the first, silently turning the rest into ordinary children
based on their order in the index. Since min(len(children), 32) never
exceeded 32 anyway, dropping the first term leaves the worst case identical
and stops penalising legitimate layouts.

Resolving the subject went through artMgr on every attestation, so an index
attaching several attestations to one image repeated the same query. Cache
the resolved subjects for the duration of one index, taking a repository
with n attestations on a shared subject from 2n lookups to n+1.

Signed-off-by: Vadim Bauer <vb@8gears.com>
Signed-off-by: Vad1mo <vadim@8gears.com>
Classify built the platform child list before it could tell whether the
index contained any attestation at all, so every ordinary multi-arch push
paid an allocation of one descriptor slice for nothing. Almost no index
carries an attestation, so rule that out with an allocation-free scan
first.

An eight-platform index abstracts with 1024 fewer bytes and one fewer
allocation; a two-platform index with 240 fewer. Indexes that do carry
attestations are unchanged, having simply moved the same scan earlier.

Signed-off-by: Vadim Bauer <vb@8gears.com>
Signed-off-by: Vad1mo <vadim@8gears.com>
Review found that classifying an attestation as an accessory removed the
artifact_reference row whose foreign-key guard used to refuse deleting an
index child directly. DELETE on the attestation digest then succeeded, and
the next GC removed a manifest the still-present index references.

Refuse deleting an artifact that is an in-toto attestation of a subject
that still exists, with the same PRECONDITION error the reference row used
to produce. The check is scoped to the attestation type: an attestation is
by construction listed in an index, while other hard accessories such as
cosign signatures are not, and deleting those alone stays legitimate.
Orphaned accessory rows are let through so their artifacts stay deletable.

Also pin two behaviours the review leaned on. The accessory dedupe key is
the accessory side alone, which is what keeps the copy path from
re-attaching an already-linked accessory under the wrong subject. And
degenerate index layouts - attestations naming themselves, each other, or
appearing twice - stay ordinary children instead of looping or attaching
wrongly.

Matches goharbor/harbor#23648 commit for commit; attestation_test.go stays
byte-identical.

Signed-off-by: Vadim Bauer <vb@8gears.com>
Signed-off-by: Vad1mo <vadim@8gears.com>

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/controller/artifact/controller.go">

<violation number="1" location="src/controller/artifact/controller.go:406">
P2: Deleting an index whose platform child is tagged leaves its attestation behind, but that attestation can then never be deleted independently: this guard only checks whether its subject exists, not whether any index still references it. Preserve/query the index-to-attestation relationship (or otherwise allow deletion once no such index remains) so removing the index does not permanently retain the accessory.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

}
return err
}
return errors.New(nil).WithCode(errors.ViolateForeignKeyConstraintCode).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Deleting an index whose platform child is tagged leaves its attestation behind, but that attestation can then never be deleted independently: this guard only checks whether its subject exists, not whether any index still references it. Preserve/query the index-to-attestation relationship (or otherwise allow deletion once no such index remains) so removing the index does not permanently retain the accessory.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/controller/artifact/controller.go, line 406:

<comment>Deleting an index whose platform child is tagged leaves its attestation behind, but that attestation can then never be deleted independently: this guard only checks whether its subject exists, not whether any index still references it. Preserve/query the index-to-attestation relationship (or otherwise allow deletion once no such index remains) so removing the index does not permanently retain the accessory.</comment>

<file context>
@@ -388,6 +388,24 @@ func (c *controller) Delete(ctx context.Context, id int64) error {
+			}
+			return err
+		}
+		return errors.New(nil).WithCode(errors.ViolateForeignKeyConstraintCode).
+			WithMessage("the artifact is an in-toto attestation referenced by an OCI index, delete its subject image or the index instead")
+	}
</file context>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Suppressed comments (5)

src/controller/artifact/manifest/attestation.go:110

  • Each attestation performs a linear scan of every platform child here; subject resolution repeats the same pattern through digestInIndex and name matching. An index split roughly evenly between platform and attestation descriptors therefore remains O(n²), despite the PR's claim that children are walked once. Build digest/platform lookup indexes once per Classify call and use constant-time membership checks in the loop and resolution helpers.
		targetDigest := descriptor.Annotations[referenceDigestAnnotation]
		if !digestInIndex(children, targetDigest) {

src/controller/artifact/manifest/attestation.go:117

  • This message says resolution falls back to the annotation, but this branch is entered only after the annotation failed to identify a child and targetDigest was cleared. On this error the descriptor remains an index child; report that outcome instead of a fallback that cannot occur.
					log.G(ctx).Debugf("could not load attestation subjects for %s@%s, falling back to annotation-based resolution: %v", repository, descriptor.Digest.String(), err)

src/controller/artifact/manifest/attestation.go:240

  • uniqueDigestInIndex returns the same empty value for “no digest matched” and “multiple children matched,” so an ambiguous digest set falls through to name matching. If conflicting digests identify two children but the names identify one, this attaches the attestation despite the stated requirement to reject ambiguous payloads. Preserve the distinction and return immediately when more than one unique digest matches.
	if digestRef := uniqueDigestInIndex(children, byDigest); digestRef != "" {
		return digestRef
	}

src/controller/artifact/abstractor.go:58

  • The advertised ChildClassifier registry/chain is not present: abstractor depends directly on the concrete in-toto classifier, and the only Classify implementation in the tree is called directly. Adding the SBOM/signature classifiers described by the PR would still require editing this struct, constructor, and index abstraction path rather than registering a classifier. Introduce a ChildClassifier interface plus registry/chain and inject that abstraction here, or align the PR title/description if extensibility is not part of this change.
	attestation *manifest.InTotoAttestationClassifier

src/controller/artifact/manifest/attestation.go:92

  • This applies the absolute ceiling even when the index has fewer platform children. For the documented one-platform/14,000-attestation case, the code performs 32 payload lookups rather than min(len(children), 32) == 1, contradicting the stated resource bound. Derive the lookup limit from both values, update the warning to report that effective limit, and correct the tests that currently expect the absolute ceiling.

This issue also appears in the following locations of the same file:

  • line 109
  • line 117
  • line 238
	lookups := maxSubjectLookups

Comment on lines +292 to +293
if accessoryErr != nil {
return false, nil, accessoryErr
@bupd
bupd merged commit 311c2aa into main Aug 17, 2026
42 checks passed
@bupd
bupd deleted the refactor/index-child-classifier branch August 17, 2026 18:53
@bupd

bupd commented Aug 17, 2026

Copy link
Copy Markdown
Member

/backport v2.15

@github-actions

Copy link
Copy Markdown
Contributor

Backport to release-2.15 has conflicts. Please cherry-pick 311c2aadf4f7c60f59c3d2e92467ecbc379afe4d manually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants