Skip to content

Commit 716fa36

Browse files
committed
Let FileRoot copy targets of symlinks contained in output directories
It looks like rules_oci creates a symlink farm for container layers. We should make sure that if any packaging actions that depend on these symlink farms are run, the blobs of the layers are also present.
1 parent 6f5934e commit 716fa36

4 files changed

Lines changed: 421 additions & 25 deletions

File tree

pkg/model/analysis/file_root.go

Lines changed: 148 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,25 @@ func (r *changeTrackingDirectorySymlinkFollowingResolver[TReference, TMetadata])
202202
return r, nil
203203
}
204204

205-
func copyFileAndDependencies[TReference object.BasicReference, TMetadata model_core.ReferenceMetadata](
205+
// targetActionResultCopier is responsible for copying files out of
206+
// the output root directories of target action results to a new
207+
// directory hierarchy. This is used to extract individual outputs out
208+
// of target action results (e.g., just "foo.o", even though the action
209+
// also yields a "foo.d").
210+
type targetActionResultCopier[TReference object.BasicReference, TMetadata model_core.ReferenceMetadata] struct {
211+
loadOptions *changeTrackingDirectoryLoadOptions[TReference]
212+
directoriesScanned map[*changeTrackingDirectory[TReference, TMetadata]]struct{}
213+
}
214+
215+
// copyFileAndDependencies extracts a single file or directory from the
216+
// output root directory and places it in a new directory hierarchy.
217+
func (c *targetActionResultCopier[TReference, TMetadata]) copyFileAndDependencies(
206218
originalOutputDirectory *changeTrackingDirectory[TReference, TMetadata],
207219
filePath string,
208220
fileType model_starlark_pb.File_Type,
209-
loadOptions *changeTrackingDirectoryLoadOptions[TReference],
210221
) (*changeTrackingDirectory[TReference, TMetadata], error) {
211222
resolver := changeTrackingDirectorySymlinkFollowingResolver[TReference, TMetadata]{
212-
loadOptions: loadOptions,
223+
loadOptions: c.loadOptions,
213224
stack: util.NewNonEmptyStack(originalOutputDirectory),
214225
}
215226
var trimmedOutputRootDirectory changeTrackingDirectory[TReference, TMetadata]
@@ -235,7 +246,7 @@ func copyFileAndDependencies[TReference object.BasicReference, TMetadata model_c
235246
return nil, errors.New("target action output is a directory, while a file was expected")
236247
}
237248
if err := symlinkRecordingComponentWalker.stack.Peek().setFile(
238-
loadOptions,
249+
c.loadOptions,
239250
*symlinkRecordingComponentWalker.terminalName,
240251
resolver.file,
241252
); err != nil {
@@ -245,10 +256,9 @@ func copyFileAndDependencies[TReference object.BasicReference, TMetadata model_c
245256
if resolver.file != nil {
246257
return nil, errors.New("target action output is a file, while a directory was expected")
247258
}
248-
if err := symlinkRecordingComponentWalker.stack.Peek().setDirectory(
249-
loadOptions,
250-
*symlinkRecordingComponentWalker.terminalName,
251-
resolver.stack.Peek(),
259+
if err := c.copyDirectoryAndDependencies(
260+
resolver.stack,
261+
&symlinkRecordingComponentWalker,
252262
); err != nil {
253263
return nil, err
254264
}
@@ -258,6 +268,126 @@ func copyFileAndDependencies[TReference object.BasicReference, TMetadata model_c
258268
return &trimmedOutputRootDirectory, nil
259269
}
260270

271+
// copyDirectoryAndDependencies is invoked by copyFileAndDependencies()
272+
// to copy a directory from the target action output root into the new
273+
// directory hierarchy. It also traverses the resulting directory to
274+
// copy any files referenced via symbolic links contained within the
275+
// directory.
276+
func (c *targetActionResultCopier[TReference, TMetadata]) copyDirectoryAndDependencies(
277+
originalOutputDirectories util.NonEmptyStack[*changeTrackingDirectory[TReference, TMetadata]],
278+
trimmedOutputComponentWalker *symlinkRecordingComponentWalker[TReference, TMetadata],
279+
) error {
280+
trimmedOutputDirectories := trimmedOutputComponentWalker.stack
281+
var newDirectory *changeTrackingDirectory[TReference, TMetadata]
282+
if terminalName := trimmedOutputComponentWalker.terminalName; terminalName == nil {
283+
// Symbolic link pointing to this directory contained a
284+
// trailing slash, meaning we're already within the
285+
// right directory.
286+
newDirectory = trimmedOutputDirectories.Peek()
287+
} else {
288+
// Symbolic link pointing to this directory did not
289+
// contain a trailing slash. This means the target
290+
// directory did not get created for us.
291+
var err error
292+
newDirectory, err = trimmedOutputComponentWalker.stack.Peek().getOrCreateDirectory(*trimmedOutputComponentWalker.terminalName)
293+
if err != nil {
294+
return err
295+
}
296+
trimmedOutputDirectories = trimmedOutputDirectories.Copy()
297+
trimmedOutputDirectories.Push(newDirectory)
298+
}
299+
if err := newDirectory.mergeDirectory(originalOutputDirectories.Peek(), c.loadOptions); err != nil {
300+
return err
301+
}
302+
return c.scanDirectoryDependencies(originalOutputDirectories, trimmedOutputDirectories, 0)
303+
}
304+
305+
// scanDirectoryDependencies visits all symbolic links in a directory
306+
// hierarchy and ensures their targets are copied over into the newly
307+
// created directory hierarchy as well.
308+
func (c *targetActionResultCopier[TReference, TMetadata]) scanDirectoryDependencies(
309+
originalOutputDirectories util.NonEmptyStack[*changeTrackingDirectory[TReference, TMetadata]],
310+
trimmedOutputDirectories util.NonEmptyStack[*changeTrackingDirectory[TReference, TMetadata]],
311+
maximumEscapementLevels uint32,
312+
) error {
313+
// Prevent cyclic scanning of directories.
314+
trimmedOutputDirectory := trimmedOutputDirectories.Peek()
315+
if _, ok := c.directoriesScanned[trimmedOutputDirectory]; ok {
316+
return nil
317+
}
318+
c.directoriesScanned[trimmedOutputDirectory] = struct{}{}
319+
320+
if trimmedOutputDirectory.maximumSymlinkEscapementLevelsAtMost(maximumEscapementLevels) {
321+
// This directory does not contain any symlinks that
322+
// escape the directory that was copied. This means that
323+
// there is no need to traverse it.
324+
return nil
325+
}
326+
327+
originalOutputDirectory := originalOutputDirectories.Peek()
328+
if err := originalOutputDirectory.maybeLoadContents(c.loadOptions); err != nil {
329+
return err
330+
}
331+
if err := trimmedOutputDirectory.maybeLoadContents(c.loadOptions); err != nil {
332+
return err
333+
}
334+
335+
for name, trimmedOutputChildDirectory := range trimmedOutputDirectory.directories {
336+
originalOutputDirectories.Push(originalOutputDirectory.directories[name])
337+
trimmedOutputDirectories.Push(trimmedOutputChildDirectory)
338+
if err := c.scanDirectoryDependencies(
339+
originalOutputDirectories,
340+
trimmedOutputDirectories,
341+
maximumEscapementLevels+1,
342+
); err != nil {
343+
return err
344+
}
345+
if _, ok := originalOutputDirectories.PopSingle(); !ok {
346+
panic("bad directory stack handling")
347+
}
348+
if _, ok := trimmedOutputDirectories.PopSingle(); !ok {
349+
panic("bad directory stack handling")
350+
}
351+
}
352+
353+
for name, target := range trimmedOutputDirectory.symlinks {
354+
resolver := changeTrackingDirectorySymlinkFollowingResolver[TReference, TMetadata]{
355+
loadOptions: c.loadOptions,
356+
stack: originalOutputDirectories.Copy(),
357+
}
358+
symlinkRecordingComponentWalker := symlinkRecordingComponentWalker[TReference, TMetadata]{
359+
base: &resolver,
360+
stack: trimmedOutputDirectories.Copy(),
361+
}
362+
if err := path.Resolve(
363+
target,
364+
path.NewLoopDetectingScopeWalker(
365+
path.NewRelativeScopeWalker(&symlinkRecordingComponentWalker),
366+
),
367+
); err != nil {
368+
return fmt.Errorf("failed to resolve symlink %#v: %w", name.String(), err)
369+
}
370+
371+
if resolver.file != nil {
372+
if err := symlinkRecordingComponentWalker.stack.Peek().setFile(
373+
c.loadOptions,
374+
*symlinkRecordingComponentWalker.terminalName,
375+
resolver.file,
376+
); err != nil {
377+
return err
378+
}
379+
} else {
380+
if err := c.copyDirectoryAndDependencies(
381+
resolver.stack,
382+
&symlinkRecordingComponentWalker,
383+
); err != nil {
384+
return err
385+
}
386+
}
387+
}
388+
return nil
389+
}
390+
261391
func (c *baseComputer[TReference, TMetadata]) ComputeFileRootValue(ctx context.Context, key model_core.Message[*model_analysis_pb.FileRoot_Key, TReference], e FileRootEnvironment[TReference, TMetadata]) (PatchedFileRootValue, error) {
262392
f := model_core.Nested(key, key.Message.File)
263393
if f.Message == nil {
@@ -337,7 +467,11 @@ func (c *baseComputer[TReference, TMetadata]) ComputeFileRootValue(ctx context.C
337467
directoryContentsReader: directoryReaders.DirectoryContents,
338468
leavesReader: directoryReaders.Leaves,
339469
}
340-
trimmedOutputRootDirectory, err := copyFileAndDependencies(&originalOutputRootDirectory, filePath, f.Message.Type, loadOptions)
470+
copier := targetActionResultCopier[TReference, TMetadata]{
471+
loadOptions: loadOptions,
472+
directoriesScanned: map[*changeTrackingDirectory[TReference, TMetadata]]struct{}{},
473+
}
474+
trimmedOutputRootDirectory, err := copier.copyFileAndDependencies(&originalOutputRootDirectory, filePath, f.Message.Type)
341475
if err != nil {
342476
if !errors.Is(err, errChangeTrackingDirectorySymlinkFollowingResolverFileNotFound) {
343477
return PatchedFileRootValue{}, err
@@ -379,7 +513,11 @@ func (c *baseComputer[TReference, TMetadata]) ComputeFileRootValue(ctx context.C
379513
return PatchedFileRootValue{}, err
380514
}
381515

382-
trimmedOutputRootDirectory, err = copyFileAndDependencies(&originalOutputRootDirectory, filePath, f.Message.Type, loadOptions)
516+
copier := targetActionResultCopier[TReference, TMetadata]{
517+
loadOptions: loadOptions,
518+
directoriesScanned: map[*changeTrackingDirectory[TReference, TMetadata]]struct{}{},
519+
}
520+
trimmedOutputRootDirectory, err = copier.copyFileAndDependencies(&originalOutputRootDirectory, filePath, f.Message.Type)
383521
if err != nil {
384522
return PatchedFileRootValue{}, err
385523
}

0 commit comments

Comments
 (0)