177 lines
7.1 KiB
C#
177 lines
7.1 KiB
C#
using System;
|
|
using System.Formats.Tar;
|
|
using System.IO;
|
|
using System.Text;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Scanner.EntryTrace.Tests;
|
|
|
|
public sealed class LayeredRootFileSystemTests : IDisposable
|
|
{
|
|
private readonly string _tempRoot;
|
|
|
|
public LayeredRootFileSystemTests()
|
|
{
|
|
_tempRoot = Path.Combine(Path.GetTempPath(), $"entrytrace-layerfs-{Guid.NewGuid():n}");
|
|
Directory.CreateDirectory(_tempRoot);
|
|
}
|
|
|
|
[Fact]
|
|
public void FromDirectories_HandlesWhiteoutsAndResolution()
|
|
{
|
|
var layer1 = CreateLayerDirectory("layer1");
|
|
var layer2 = CreateLayerDirectory("layer2");
|
|
|
|
var usrBin1 = Path.Combine(layer1, "usr", "bin");
|
|
Directory.CreateDirectory(usrBin1);
|
|
var entrypointPath = Path.Combine(usrBin1, "entrypoint.sh");
|
|
File.WriteAllText(entrypointPath, "#!/bin/sh\necho layer1\n");
|
|
|
|
var optDirectory1 = Path.Combine(layer1, "opt");
|
|
Directory.CreateDirectory(optDirectory1);
|
|
File.WriteAllText(Path.Combine(optDirectory1, "setup.sh"), "echo setup\n");
|
|
|
|
var optDirectory2 = Path.Combine(layer2, "opt");
|
|
Directory.CreateDirectory(optDirectory2);
|
|
File.WriteAllText(Path.Combine(optDirectory2, ".wh.setup.sh"), string.Empty);
|
|
|
|
var fs = LayeredRootFileSystem.FromDirectories(new[]
|
|
{
|
|
new LayeredRootFileSystem.LayerDirectory("sha256:layer1", layer1),
|
|
new LayeredRootFileSystem.LayerDirectory("sha256:layer2", layer2)
|
|
});
|
|
|
|
Assert.True(fs.TryResolveExecutable("entrypoint.sh", new[] { "/usr/bin" }, out var descriptor));
|
|
Assert.Equal("/usr/bin/entrypoint.sh", descriptor.Path);
|
|
Assert.Equal("sha256:layer1", descriptor.LayerDigest);
|
|
|
|
Assert.True(fs.TryReadAllText("/usr/bin/entrypoint.sh", out var textDescriptor, out var content));
|
|
Assert.Equal(descriptor.Path, textDescriptor.Path);
|
|
Assert.Contains("echo layer1", content);
|
|
|
|
Assert.False(fs.TryReadAllText("/opt/setup.sh", out _, out _));
|
|
|
|
var optEntries = fs.EnumerateDirectory("/opt");
|
|
Assert.DoesNotContain(optEntries, entry => entry.Path.EndsWith("setup.sh", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void FromArchives_ResolvesSymlinkAndWhiteout()
|
|
{
|
|
var layer1Path = Path.Combine(_tempRoot, "layer1.tar");
|
|
var layer2Path = Path.Combine(_tempRoot, "layer2.tar");
|
|
|
|
CreateArchive(layer1Path, writer =>
|
|
{
|
|
var scriptEntry = new PaxTarEntry(TarEntryType.RegularFile, "usr/local/bin/start.sh");
|
|
scriptEntry.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
|
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
|
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
|
|
scriptEntry.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("#!/bin/sh\necho start\n"));
|
|
writer.WriteEntry(scriptEntry);
|
|
|
|
var oldScript = new PaxTarEntry(TarEntryType.RegularFile, "opt/old.sh");
|
|
oldScript.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
|
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
|
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
|
|
oldScript.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("echo old\n"));
|
|
writer.WriteEntry(oldScript);
|
|
});
|
|
|
|
CreateArchive(layer2Path, writer =>
|
|
{
|
|
var symlinkEntry = new PaxTarEntry(TarEntryType.SymbolicLink, "usr/bin/start.sh");
|
|
symlinkEntry.LinkName = "/usr/local/bin/start.sh";
|
|
writer.WriteEntry(symlinkEntry);
|
|
|
|
var whiteout = new PaxTarEntry(TarEntryType.RegularFile, "opt/.wh.old.sh");
|
|
whiteout.DataStream = new MemoryStream(Array.Empty<byte>());
|
|
writer.WriteEntry(whiteout);
|
|
});
|
|
|
|
var fs = LayeredRootFileSystem.FromArchives(new[]
|
|
{
|
|
new LayeredRootFileSystem.LayerArchive("sha256:base", layer1Path),
|
|
new LayeredRootFileSystem.LayerArchive("sha256:update", layer2Path)
|
|
});
|
|
|
|
Assert.True(fs.TryResolveExecutable("start.sh", new[] { "/usr/bin" }, out var descriptor));
|
|
Assert.Equal("/usr/local/bin/start.sh", descriptor.Path);
|
|
Assert.Equal("sha256:base", descriptor.LayerDigest);
|
|
|
|
Assert.True(fs.TryReadAllText("/usr/bin/start.sh", out var resolvedDescriptor, out var content));
|
|
Assert.Equal(descriptor.Path, resolvedDescriptor.Path);
|
|
Assert.Contains("echo start", content);
|
|
|
|
Assert.False(fs.TryReadAllText("/opt/old.sh", out _, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void FromArchives_ResolvesHardLinkContent()
|
|
{
|
|
var baseLayer = Path.Combine(_tempRoot, "base.tar");
|
|
var hardLinkLayer = Path.Combine(_tempRoot, "hardlink.tar");
|
|
|
|
CreateArchive(baseLayer, writer =>
|
|
{
|
|
var baseEntry = new PaxTarEntry(TarEntryType.RegularFile, "usr/bin/tool.sh");
|
|
baseEntry.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
|
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
|
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
|
|
baseEntry.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("#!/bin/sh\necho tool\n"));
|
|
writer.WriteEntry(baseEntry);
|
|
});
|
|
|
|
CreateArchive(hardLinkLayer, writer =>
|
|
{
|
|
var hardLink = new PaxTarEntry(TarEntryType.HardLink, "bin/tool.sh")
|
|
{
|
|
LinkName = "/usr/bin/tool.sh",
|
|
Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
|
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
|
UnixFileMode.OtherRead | UnixFileMode.OtherExecute
|
|
};
|
|
writer.WriteEntry(hardLink);
|
|
});
|
|
|
|
var fs = LayeredRootFileSystem.FromArchives(new[]
|
|
{
|
|
new LayeredRootFileSystem.LayerArchive("sha256:base", baseLayer),
|
|
new LayeredRootFileSystem.LayerArchive("sha256:hardlink", hardLinkLayer)
|
|
});
|
|
|
|
Assert.True(fs.TryReadAllText("/bin/tool.sh", out var descriptor, out var content));
|
|
Assert.Equal("/usr/bin/tool.sh", descriptor.Path);
|
|
Assert.Contains("echo tool", content);
|
|
}
|
|
|
|
private string CreateLayerDirectory(string name)
|
|
{
|
|
var path = Path.Combine(_tempRoot, name);
|
|
Directory.CreateDirectory(path);
|
|
return path;
|
|
}
|
|
|
|
private static void CreateArchive(string path, Action<TarWriter> writerAction)
|
|
{
|
|
using var stream = File.Create(path);
|
|
using var writer = new TarWriter(stream, leaveOpen: false);
|
|
writerAction(writer);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
if (Directory.Exists(_tempRoot))
|
|
{
|
|
Directory.Delete(_tempRoot, recursive: true);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore cleanup failures
|
|
}
|
|
}
|
|
}
|