Skip to content

Commit

Permalink
Generate Windows layers based on base image OS
Browse files Browse the repository at this point in the history
  • Loading branch information
kusc-leica committed Mar 3, 2023
1 parent d838d43 commit fe559a7
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public async Task ApiEndToEndWithRegistryPushAndPull()

Assert.NotNull(imageBuilder);

Layer l = Layer.FromDirectory(publishDirectory, "/app");
Layer l = Layer.FromDirectory(publishDirectory, "/app", false);

imageBuilder.AddLayer(l);

Expand Down Expand Up @@ -90,7 +90,7 @@ public async Task ApiEndToEndWithLocalLoad()
cancellationToken: default).ConfigureAwait(false);
Assert.NotNull(imageBuilder);

Layer l = Layer.FromDirectory(publishDirectory, "/app");
Layer l = Layer.FromDirectory(publishDirectory, "/app", false);

imageBuilder.AddLayer(l);

Expand Down Expand Up @@ -293,7 +293,7 @@ public async Task CanPackageForAllSupportedContainerRIDs(string dockerPlatform,
cancellationToken: default).ConfigureAwait(false);
Assert.NotNull(imageBuilder);

Layer l = Layer.FromDirectory(publishDirectory, "/app");
Layer l = Layer.FromDirectory(publishDirectory, "/app", false);

imageBuilder.AddLayer(l);
imageBuilder.SetWorkingDirectory(workingDir);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void SingleFileInFolder()

File.WriteAllText(testFilePath, testString);

Layer l = Layer.FromDirectory(directory: folder.Path, containerPath: "/app");
Layer l = Layer.FromDirectory(directory: folder.Path, containerPath: "/app", false);

Console.WriteLine(l.Descriptor);

Expand All @@ -42,9 +42,37 @@ public void SingleFileInFolder()
VerifyDescriptorInfo(l);

var allEntries = LoadAllTarEntries(l.BackingFile);
Assert.True(allEntries.TryGetValue("app/", out var appEntryType) && appEntryType == TarEntryType.Directory, "Missing app directory entry");
Assert.True(allEntries.TryGetValue("app/TestFile.txt", out var fileEntryType) && fileEntryType == TarEntryType.RegularFile, "Missing TestFile.txt file entry");
Assert.True(allEntries.TryGetValue("app/", out var appEntry) && appEntry.EntryType == TarEntryType.Directory, "Missing app directory entry");
Assert.True(allEntries.TryGetValue("app/TestFile.txt", out var fileEntry) && fileEntry.EntryType == TarEntryType.RegularFile, "Missing TestFile.txt file entry");
}

[Fact]
public void SingleFileInFolderWindows()
{
using TransientTestFolder folder = new();

string testFilePath = Path.Join(folder.Path, "TestFile.txt");
string testString = $"Test content for {nameof(SingleFileInFolder)}";

File.WriteAllText(testFilePath, testString);

Layer l = Layer.FromDirectory(directory: folder.Path, containerPath: "C:\\app", true);

var allEntries = LoadAllTarEntries(l.BackingFile);
Assert.True(allEntries.TryGetValue("Files/", out var filesEntry) && filesEntry.EntryType == TarEntryType.Directory, "Missing Files/ directory entry");
Assert.True(allEntries.TryGetValue("Files/app/", out var appEntry) && appEntry.EntryType == TarEntryType.Directory, "Missing Files/app/ directory entry");
Assert.True(allEntries.TryGetValue("Files/app/TestFile.txt", out var fileEntry) && fileEntry.EntryType == TarEntryType.RegularFile, "Missing Files/app/TestFile.txt file entry");
Assert.True(allEntries.TryGetValue("Hives/", out var hivesEntry) && hivesEntry.EntryType == TarEntryType.Directory, "Missing Hives/ directory entry");

// Enable after https://github.com/dotnet/runtime/issues/81699 is resolved
// foreach (var entry in allEntries.Values)
// {
// Assert.IsInstanceOfType(entry, typeof(PaxTarEntry));
// var pax = (PaxTarEntry)entry;
// Assert.IsTrue(pax.ExtendedAttributes.ContainsKey("MSWINDOWS.rawsd"),
// "Missing MSWINDOWS.rawsd definition for " + entry.Name);
// }
}

[Fact]
public void TwoFilesInTwoFolders()
Expand All @@ -64,7 +92,7 @@ public void TwoFilesInTwoFolders()
{
(testFilePath, "/app/TestFile.txt"),
(testFilePath2, "/app/subfolder/TestFile.txt"),
});
}, false);

Console.WriteLine(l.Descriptor);

Expand All @@ -75,10 +103,10 @@ public void TwoFilesInTwoFolders()
VerifyDescriptorInfo(l);

var allEntries = LoadAllTarEntries(l.BackingFile);
Assert.True(allEntries.TryGetValue("app/", out var appEntryType) && appEntryType == TarEntryType.Directory, "Missing app directory entry");
Assert.True(allEntries.TryGetValue("app/TestFile.txt", out var fileEntryType) && fileEntryType == TarEntryType.RegularFile, "Missing TestFile.txt file entry");
Assert.True(allEntries.TryGetValue("app/subfolder/", out var subfolderType) && subfolderType == TarEntryType.Directory, "Missing subfolder directory entry");
Assert.True(allEntries.TryGetValue("app/subfolder/TestFile.txt", out var subfolderFileEntryType) && subfolderFileEntryType == TarEntryType.RegularFile, "Missing subfolder/TestFile.txt file entry");
Assert.True(allEntries.TryGetValue("app/", out var appEntry) && appEntry.EntryType == TarEntryType.Directory, "Missing app directory entry");
Assert.True(allEntries.TryGetValue("app/TestFile.txt", out var fileEntry) && fileEntry.EntryType == TarEntryType.RegularFile, "Missing TestFile.txt file entry");
Assert.True(allEntries.TryGetValue("app/subfolder/", out var subfolderEntry) && subfolderEntry.EntryType == TarEntryType.Directory, "Missing subfolder directory entry");
Assert.True(allEntries.TryGetValue("app/subfolder/TestFile.txt", out var subfolderFileEntry) && subfolderFileEntry.EntryType == TarEntryType.RegularFile, "Missing subfolder/TestFile.txt file entry");
}

private static void VerifyDescriptorInfo(Layer l)
Expand Down Expand Up @@ -115,18 +143,19 @@ public void Dispose()
ContentStore.ArtifactRoot = priorArtifactRoot;
}
}

private static Dictionary<string, TarEntryType> LoadAllTarEntries(string file)


private static IDictionary<string, TarEntry> LoadAllTarEntries(string file)
{
using var gzip = new GZipStream(File.OpenRead(file), CompressionMode.Decompress);
using var tar = new TarReader(gzip);

var entries = new Dictionary<string, TarEntryType>();

var entries = new Dictionary<string, TarEntry>();
TarEntry? entry;
while ((entry = tar.GetNextEntry()) != null)
{
entries[entry.Name] = entry.EntryType;
entries[entry.Name] = entry;
}

return entries;
Expand Down
2 changes: 1 addition & 1 deletion Microsoft.NET.Build.Containers/ContainerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static async Task ContainerizeAsync(
WriteIndented = true,
};

Layer l = Layer.FromDirectory(folder.FullName, workingDir);
Layer l = Layer.FromDirectory(folder.FullName, workingDir, imageBuilder.IsWindows);

imageBuilder.AddLayer(l);

Expand Down
5 changes: 5 additions & 0 deletions Microsoft.NET.Build.Containers/ImageBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ internal ImageBuilder(ManifestV2 manifest, ImageConfig baseImageConfig)
_baseImageConfig = baseImageConfig;
}

/// <summary>
/// Gets a value indicating whether the base image is has a Windows operating system.
/// </summary>
public bool IsWindows => _baseImageConfig.IsWindows;

/// <summary>
/// Builds the image configuration <see cref="BuiltImage"/> ready for further processing.
/// </summary>
Expand Down
9 changes: 7 additions & 2 deletions Microsoft.NET.Build.Containers/ImageConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ internal ImageConfig(JsonNode config)
_environmentVariables = GetEnvironmentVariables();
}

/// <summary>
/// Gets a value indicating whether the base image is has a Windows operating system.
/// </summary>
public bool IsWindows => "windows".Equals(_config["os"]?.GetValue<string>(), StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Builds in additional configuration and returns updated image configuration in JSON format as string.
/// </summary>
Expand All @@ -43,7 +48,7 @@ internal string BuildConfig()

//update creation date
_config["created"] = DateTime.UtcNow;

config["ExposedPorts"] = CreatePortMap();
config["Env"] = CreateEnvironmentVariablesMapping();
config["Labels"] = CreateLabelMap();
Expand Down Expand Up @@ -137,7 +142,7 @@ private Dictionary<string, string> GetLabels()
{
labels[propertyName] = propertyValue.ToString();
}
}
}
}
return labels;
}
Expand Down
123 changes: 95 additions & 28 deletions Microsoft.NET.Build.Containers/Layer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ namespace Microsoft.NET.Build.Containers;

internal record struct Layer
{
// NOTE: The SID string below was created using the following snippet. As the code is Windows only we keep the constant
// private static string CreateUserOwnerAndGroupSID()
// {
// var descriptor = new RawSecurityDescriptor(
// ControlFlags.SelfRelative,
// new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null),
// new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null),
// null,
// null
// );
//
// var raw = new byte[descriptor.BinaryLength];
// descriptor.GetBinaryForm(raw, 0);
// return Convert.ToBase64String(raw);
// }

private const string BuiltinUsersSecurityDescriptor = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA==";

public Descriptor Descriptor { get; private set; }

public string BackingFile { get; private set; }
Expand All @@ -24,7 +42,7 @@ public static Layer FromDescriptor(Descriptor descriptor)
};
}

public static Layer FromDirectory(string directory, string containerPath)
public static Layer FromDirectory(string directory, string containerPath, bool isWindowsLayer)
{
var fileList =
new DirectoryInfo(directory)
Expand All @@ -34,16 +52,75 @@ public static Layer FromDirectory(string directory, string containerPath)
string destinationPath = Path.Join(containerPath, Path.GetRelativePath(directory, fsi.FullName)).Replace(Path.DirectorySeparatorChar, '/');
return (fsi.FullName, destinationPath);
});
return FromFiles(fileList);
return FromFiles(fileList, isWindowsLayer);
}

public static Layer FromFiles(IEnumerable<(string path, string containerPath)> fileList)
public static Layer FromFiles(IEnumerable<(string path, string containerPath)> fileList, bool isWindowsLayer)
{
long fileSize;
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
Span<byte> uncompressedHash = stackalloc byte[SHA256.HashSizeInBytes];

// this factory helps us creating the Tar entries with the right attributes
PaxTarEntry CreateTarEntry(TarEntryType entryType, string containerPath)
{
var extendedAttributes = new Dictionary<string, string>();
if (isWindowsLayer)
{
// We grant all users access to the application directory
// https://github.com/buildpacks/rfcs/blob/main/text/0076-windows-security-identifiers.md
extendedAttributes["MSWINDOWS.rawsd"] = BuiltinUsersSecurityDescriptor;
return new PaxTarEntry(entryType, containerPath, extendedAttributes);
}

var entry = new PaxTarEntry(entryType, containerPath, extendedAttributes)
{
Mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute
};
return entry;
}

string SanitizeContainerPath(string containerPath)
{
// no leading slashes
containerPath = containerPath.TrimStart(PathSeparators);

// For Windows layers we need to put files into a "Files" directory without drive letter.
if (isWindowsLayer)
{
// Cut of drive letter: /* C:\ */
if (containerPath[1] == ':')
{
containerPath = containerPath[3..];
}

containerPath = "Files/" + containerPath;
}

return containerPath;
}

// Ensures that all directory entries for the given segments are created within the tar.
var directoryEntries = new HashSet<string>();
void EnsureDirectoryEntries(TarWriter tar,
IReadOnlyList<string> filePathSegments)
{
var pathBuilder = new StringBuilder();
for (int i = 0; i < filePathSegments.Count - 1; i++)
{
pathBuilder.Append($"{filePathSegments[i]}/");

string fullPath = pathBuilder.ToString();
if (!directoryEntries.Contains(fullPath))
{
tar.WriteEntry(CreateTarEntry(TarEntryType.Directory, fullPath));
directoryEntries.Add(fullPath);
}
}
}


string tempTarballPath = ContentStore.GetTempFile();
using (FileStream fs = File.Create(tempTarballPath))
Expand All @@ -56,12 +133,24 @@ public static Layer FromFiles(IEnumerable<(string path, string containerPath)> f
{
// Docker treats a COPY instruction that copies to a path like `/app` by
// including `app/` as a directory, with no leading slash. Emulate that here.
string containerPath = item.containerPath.TrimStart(PathSeparators);
string containerPath = SanitizeContainerPath(item.containerPath);

EnsureDirectoryEntries(writer, containerPath.Split(PathSeparators));

EnsureDirectoryEntries(writer, directoryEntries, containerPath.Split(PathSeparators));
using var fileStream = File.OpenRead(item.path);
var entry = CreateTarEntry(TarEntryType.RegularFile, containerPath);
entry.DataStream = fileStream;

writer.WriteEntry(item.path, containerPath);
writer.WriteEntry(entry);
}

// Windows layers need a Hives folder, we do not need to create any Registry Hive deltas inside
if (isWindowsLayer)
{
var entry = CreateTarEntry(TarEntryType.Directory, "Hives/");
writer.WriteEntry(entry);
}

} // Dispose of the TarWriter before getting the hash so the final data get written to the tar stream

int bytesWritten = gz.GetCurrentUncompressedHash(uncompressedHash);
Expand Down Expand Up @@ -103,28 +192,6 @@ public static Layer FromFiles(IEnumerable<(string path, string containerPath)> f

private readonly static char[] PathSeparators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };

/// <summary>
/// Ensures that all directory entries for the given segments are created within the tar.
/// </summary>
/// <param name="tar">The tar into which to add the directory entries.</param>
/// <param name="directoryEntries">The lookup of all known directory entries. </param>
/// <param name="filePathSegments">The segments of the file within the tar for which to create the folders</param>
private static void EnsureDirectoryEntries(TarWriter tar, HashSet<string> directoryEntries, IReadOnlyList<string> filePathSegments)
{
var pathBuilder = new StringBuilder();
for (var i = 0; i < filePathSegments.Count - 1; i++)
{
pathBuilder.Append($"{filePathSegments[i]}/");

var fullPath = pathBuilder.ToString();
if (!directoryEntries.Contains(fullPath))
{
tar.WriteEntry(new PaxTarEntry(TarEntryType.Directory, fullPath));
directoryEntries.Add(fullPath);
}
}
}

/// <summary>
/// A stream capable of computing the hash digest of raw uncompressed data while also compressing it.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ internal async Task<bool> ExecuteAsync(CancellationToken cancellationToken)

SafeLog("Building image '{0}' with tags {1} on top of base image {2}", ImageName, String.Join(",", ImageTags), sourceImageReference);

Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory);
Layer newLayer = Layer.FromDirectory(PublishDirectory, WorkingDirectory, imageBuilder.IsWindows);
imageBuilder.AddLayer(newLayer);
imageBuilder.SetWorkingDirectory(WorkingDirectory);
imageBuilder.SetEntryPoint(Entrypoint.Select(i => i.ItemSpec).ToArray(), EntrypointArgs.Select(i => i.ItemSpec).ToArray());
Expand Down
Loading

0 comments on commit fe559a7

Please sign in to comment.