Skip to content

Start working on building a layer #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: "nuget" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
4 changes: 3 additions & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: 7.0.100-preview.4.22252.9
dotnet-version: |
6.0.x
7.0.100-preview.6.22352.1
- name: Build
run: dotnet build
- name: Test
Expand Down
8 changes: 4 additions & 4 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageVersion Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageVersion Include="MSTest.TestFramework" Version="2.2.8" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageVersion Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageVersion Include="MSTest.TestFramework" Version="2.2.10" />
<PackageVersion Include="coverlet.collector" Version="3.1.2" />
<PackageVersion Include="GitHubActionsTestLogger" Version="1.4.1" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.0.1" />
</ItemGroup>
</Project>
10 changes: 10 additions & 0 deletions System.Containers/Configuration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace System.Containers;

public static class Configuration
{
public static string ArtifactRoot { get; set; } = Path.Combine(Path.GetTempPath(), "Containers");
public static string ContentRoot
{
get => Path.Combine(ArtifactRoot, "Content");
}
}
68 changes: 68 additions & 0 deletions System.Containers/Descriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Text.Json.Serialization;

namespace System.Containers;

/// <summary>
/// An OCI Content Descriptor describing a component.
/// </summary>
/// <remarks>
/// <see href="https://github.com/opencontainers/image-spec/blob/7b36cea86235157d78528944cb94c3323ee0905c/descriptor.md"/>.
/// </remarks>
public readonly record struct Descriptor
{
/// <summary>
/// Media type of the referenced content.
/// </summary>
/// <remarks>
/// Likely to be an OCI media type defined at <see href="https://github.com/opencontainers/image-spec/blob/7b36cea86235157d78528944cb94c3323ee0905c/media-types.md" />.
/// </remarks>
// TODO: validate against RFC 6838 naming conventions?
[JsonPropertyName("mediaType")]
public string MediaType { get; init; }

/// <summary>
/// Digest of the content, specifying algorithm and value.
/// </summary>
/// <remarks>
/// <see href="https://github.com/opencontainers/image-spec/blob/7b36cea86235157d78528944cb94c3323ee0905c/descriptor.md#digests"/>
/// </remarks>
[JsonPropertyName("digest")]
public string Digest { get; init; }

/// <summary>
/// Size, in bytes, of the raw content.
/// </summary>
[JsonPropertyName("size")]
public long Size { get; init; }

/// <summary>
/// Optional list of URLs where the content may be downloaded.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string[]? Urls { get; init; } = null;

/// <summary>
/// Arbitrary metadata for this descriptor.
/// </summary>
/// <remarks>
/// <see href="https://github.com/opencontainers/image-spec/blob/7b36cea86235157d78528944cb94c3323ee0905c/annotations.md"/>
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, string?>? Annotations { get; init; } = null;

/// <summary>
/// Embedded representation of the referenced content, base-64 encoded.
/// </summary>
/// <remarks>
/// <see href="https://github.com/opencontainers/image-spec/blob/7b36cea86235157d78528944cb94c3323ee0905c/descriptor.md#embedded-content"/>
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Data { get; init; } = null;

public Descriptor(string mediaType, string digest, long size)
{
MediaType = mediaType;
Digest = digest;
Size = size;
}
}
71 changes: 71 additions & 0 deletions System.Containers/Layer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Formats.Tar;
using System.Security.Cryptography;

namespace System.Containers;

public record struct Layer
{
public Descriptor Descriptor { get; private set; }

public string BackingFile { get; private set; }

public static Layer FromDirectory(string directory, string containerPath)
{
// 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.
containerPath = containerPath.TrimStart(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });

DirectoryInfo di = new(directory);

string tempPath = Path.Join(Configuration.ArtifactRoot, "Temp");

Directory.CreateDirectory(tempPath);

long fileSize;
byte[] hash;

string tempTarballPath = Path.Join(tempPath, Path.GetRandomFileName());
using (FileStream fs = File.Create(tempTarballPath))
{
// using (GZipStream gz = new(fs, CompressionMode.Compress)) // TODO: https://github.com/rainersigwald/containers/issues/29
using (TarWriter writer = new(fs, TarEntryFormat.Gnu, leaveOpen: true))
{
foreach (var item in di.GetFileSystemInfos())
{
if (item is FileInfo fi)
{
writer.WriteEntry(fi.FullName, Path.Combine(containerPath, fi.Name).Replace(Path.DirectorySeparatorChar, '/'));
}
}
}

fileSize = fs.Length;

fs.Position = 0;

using SHA256 mySHA256 = SHA256.Create();
hash = mySHA256.ComputeHash(fs);
}

string contentHash = Convert.ToHexString(hash).ToLowerInvariant();

string storedContent = Path.Combine(Configuration.ContentRoot, contentHash);

Directory.CreateDirectory(Configuration.ContentRoot);

File.Move(tempTarballPath, storedContent);

Layer l = new()
{
Descriptor = new()
{
MediaType = "application/vnd.oci.image.layer.v1.tar", // TODO: configurable? gzip always?
Size = fileSize,
Digest = $"sha256:{contentHash}"
},
BackingFile = storedContent,
};

return l;
}
}
62 changes: 62 additions & 0 deletions Test.System.Containers.Filesystem/LayerEndToEnd.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Containers;
using System.Security.Cryptography;

namespace Test.System.Containers.Filesystem;

[TestClass]
public class LayerEndToEnd
{
[TestMethod]
public void SingleFile()
{
using TransientTestFolder folder = new();

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

File.WriteAllText(testFilePath, testString);

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

Console.WriteLine(l.Descriptor);

Assert.AreEqual("application/vnd.oci.image.layer.v1.tar", l.Descriptor.MediaType);
Assert.AreEqual(2048, l.Descriptor.Size);
//Assert.AreEqual("sha256:26140bc75f2fcb3bf5da7d3b531d995c93d192837e37df0eb5ca46e2db953124", l.Descriptor.Digest); // TODO: determinism!

Assert.AreEqual(l.Descriptor.Size, new FileInfo(l.BackingFile).Length);

byte[] hashBytes;

using (SHA256 hasher = SHA256.Create())
using (FileStream fs = File.OpenRead(l.BackingFile))
{
hashBytes = hasher.ComputeHash(fs);
}

Assert.AreEqual(Convert.ToHexString(hashBytes), l.Descriptor.Digest.Substring("sha256:".Length), ignoreCase: true);
}

TransientTestFolder? testSpecificArtifactRoot;
string? priorArtifactRoot;

[TestInitialize]
public void TestInitialize()
{
testSpecificArtifactRoot = new();

priorArtifactRoot = Configuration.ArtifactRoot;

Configuration.ArtifactRoot = testSpecificArtifactRoot.Path;
}

[TestCleanup]
public void TestCleanup()
{
testSpecificArtifactRoot?.Dispose();
if (priorArtifactRoot is not null)
{
Configuration.ArtifactRoot = priorArtifactRoot;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@
<PackageReference Include="GitHubActionsTestLogger" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\System.Containers\System.Containers.csproj" />
</ItemGroup>

</Project>
22 changes: 22 additions & 0 deletions Test.System.Containers.Filesystem/TransientTestFolder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using static System.IO.Path;

namespace Test.System.Containers.Filesystem;

/// <summary>
/// Helper class to clean up after tests that touch the filesystem.
/// </summary>
internal sealed class TransientTestFolder : IDisposable
{
public readonly string Path = Combine(GetTempPath(), GetRandomFileName());
public readonly DirectoryInfo DirectoryInfo;

public TransientTestFolder()
{
DirectoryInfo = Directory.CreateDirectory(Path);
}

public void Dispose()
{
Directory.Delete(Path, recursive: true);
}
}
10 changes: 0 additions & 10 deletions Test.System.Containers.Filesystem/UnitTest1.cs

This file was deleted.

27 changes: 27 additions & 0 deletions Test.System.Containers/DescriptorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Containers;
using System.Text.Json;

namespace Test.System.Containers;

[TestClass]
public class DescriptorTests
{
[TestMethod]
public void BasicConstructor()
{
Descriptor d = new(
mediaType: "application/vnd.oci.image.manifest.v1+json",
digest: "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
size: 7682);

Console.WriteLine(JsonSerializer.Serialize(d, new JsonSerializerOptions { WriteIndented = true }));

Assert.AreEqual("application/vnd.oci.image.manifest.v1+json", d.MediaType);
Assert.AreEqual("sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", d.Digest);
Assert.AreEqual(7_682, d.Size);

Assert.IsNull(d.Annotations);
Assert.IsNull(d.Data);
Assert.IsNull(d.Urls);
}
}
4 changes: 4 additions & 0 deletions Test.System.Containers/Test.System.Containers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@
<PackageReference Include="GitHubActionsTestLogger" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\System.Containers\System.Containers.csproj" />
</ItemGroup>

</Project>
10 changes: 0 additions & 10 deletions Test.System.Containers/UnitTest1.cs

This file was deleted.