dap: improve determination of the proper parent for certain ops

Improves the determination of the proper parent for exec and file ops.
With file ops, it will only consider inputs and ignore secondary inputs.
This prevents the following case:

```
FROM busybox AS build1
RUN echo foo > /hello

FROM scratch
COPY --from=build1 /hello .
```

Previously, `build1` would be considered the parent of the copy
instruction. Now, copy properly does not have a parent.

If there are multiple file ops and the operations disagree on the
canonical "parent", we give up on trying to find a canonical parent and
assume there is none.

For exec operations, whichever input is associated with the root mount
is considered the primary parent.

For all other operations, the first parent is considered the primary
parent if it exists.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
Jonathan A. Sternberg
2025-08-18 09:27:02 -05:00
parent 10605b8c35
commit 5c97696d64
+44 -3
View File
@@ -163,7 +163,12 @@ func (t *thread) createBranch(last *step) (first *step) {
op := t.ops[first.dgst]
if len(op.Inputs) > 0 {
for i := len(op.Inputs) - 1; i > 0; i-- {
parent := t.determineParent(op)
for i := len(op.Inputs) - 1; i >= 0; i-- {
if i == parent {
// Skip the direct parent.
continue
}
inp := op.Inputs[i]
// Create a pseudo-step that acts as an exit point for this
@@ -188,8 +193,10 @@ func (t *thread) createBranch(last *step) (first *step) {
}
// Set the digest of the parent input on the first step associated
// with this step.
prev.dgst = digest.Digest(op.Inputs[0].Digest)
// with this step if it exists.
if parent >= 0 {
prev.dgst = digest.Digest(op.Inputs[parent].Digest)
}
}
// New first is the step we just created.
@@ -217,6 +224,40 @@ func (t *thread) getStackFrame(dgst digest.Digest) *frame {
return f
}
func (t *thread) determineParent(op *pb.Op) int {
// Another section should have already checked this but
// double check here just in case we forget somewhere else.
// The rest of this method assumes there's at least one parent
// at index zero.
n := len(op.Inputs)
if n == 0 {
return -1
}
switch op := op.Op.(type) {
case *pb.Op_Exec:
for _, m := range op.Exec.Mounts {
if m.Dest == "/" {
return int(m.Input)
}
}
return -1
case *pb.Op_File:
// Use the first input where the index is from one of the inputs.
for _, action := range op.File.Actions {
if input := int(action.Input); input >= 0 && input < n {
return input
}
}
// Default to having no parent.
return -1
default:
// Default to index zero.
return 0
}
}
func (t *thread) reset() {
t.c = nil
t.ref = nil