Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .autover/autover.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@
{
"Name": "Amazon.Lambda.AppSyncEvents",
"Path": "Libraries/src/Amazon.Lambda.AppSyncEvents/Amazon.Lambda.AppSyncEvents.csproj"
},
{
"Name": "AWS.OCI.Builder",
"Path": "Libraries/src/AWS.OCI.Builder/AWS.OCI.Builder.csproj"
}
],
"UseCommitsForChangelog": false,
Expand Down
11 changes: 11 additions & 0 deletions .autover/changes/1df94c4d-8841-4630-89ca-9324e3bcaf79.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "AWS.OCI.Builder",
"Type": "Minor",
"ChangelogMessages": [
"Initial release of AWS library for programmatically building OCI images without a container runtime"
]
}
]
}
10 changes: 10 additions & 0 deletions Libraries/AWS.OCI.Builder.slnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"solution": {
"path": "Libraries.sln",
"projects": [
"src\\AWS.OCI.Builder\\AWS.OCI.Builder.csproj",
"test\\AWS.OCI.Builder.IntegrationTests\\AWS.OCI.Builder.IntegrationTests.csproj",
"test\\AWS.OCI.Builder.Tests\\AWS.OCI.Builder.Tests.csproj"
]
}
}
45 changes: 45 additions & 0 deletions Libraries/Libraries.sln
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestDurableServerlessApp",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestDurableServerlessApp.IntegrationTests", "test\TestDurableServerlessApp.IntegrationTests\TestDurableServerlessApp.IntegrationTests.csproj", "{A89D56F9-63CC-4B47-BFED-693E3410993D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.OCI.Builder", "src\AWS.OCI.Builder\AWS.OCI.Builder.csproj", "{BB37A746-2351-9685-2D1F-1571F8F23E06}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.OCI.Builder.Tests", "test\AWS.OCI.Builder.Tests\AWS.OCI.Builder.Tests.csproj", "{7636C459-D2A3-30DB-4964-CAF22E6BA256}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.OCI.Builder.IntegrationTests", "test\AWS.OCI.Builder.IntegrationTests\AWS.OCI.Builder.IntegrationTests.csproj", "{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1109,6 +1115,42 @@ Global
{A89D56F9-63CC-4B47-BFED-693E3410993D}.Release|x64.Build.0 = Release|Any CPU
{A89D56F9-63CC-4B47-BFED-693E3410993D}.Release|x86.ActiveCfg = Release|Any CPU
{A89D56F9-63CC-4B47-BFED-693E3410993D}.Release|x86.Build.0 = Release|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Debug|x64.ActiveCfg = Debug|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Debug|x64.Build.0 = Debug|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Debug|x86.ActiveCfg = Debug|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Debug|x86.Build.0 = Debug|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Release|Any CPU.Build.0 = Release|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Release|x64.ActiveCfg = Release|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Release|x64.Build.0 = Release|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Release|x86.ActiveCfg = Release|Any CPU
{BB37A746-2351-9685-2D1F-1571F8F23E06}.Release|x86.Build.0 = Release|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Debug|x64.ActiveCfg = Debug|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Debug|x64.Build.0 = Debug|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Debug|x86.ActiveCfg = Debug|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Debug|x86.Build.0 = Debug|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Release|Any CPU.Build.0 = Release|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Release|x64.ActiveCfg = Release|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Release|x64.Build.0 = Release|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Release|x86.ActiveCfg = Release|Any CPU
{7636C459-D2A3-30DB-4964-CAF22E6BA256}.Release|x86.Build.0 = Release|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Debug|x64.ActiveCfg = Debug|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Debug|x64.Build.0 = Debug|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Debug|x86.ActiveCfg = Debug|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Debug|x86.Build.0 = Debug|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Release|Any CPU.Build.0 = Release|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Release|x64.ActiveCfg = Release|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Release|x64.Build.0 = Release|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Release|x86.ActiveCfg = Release|Any CPU
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1195,6 +1237,9 @@ Global
{0460CF9D-B5CC-47C5-9B30-CEA84695AB3B} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{616E108A-9ED5-4282-B1D4-7FD49BC6DA2E} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{A89D56F9-63CC-4B47-BFED-693E3410993D} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{BB37A746-2351-9685-2D1F-1571F8F23E06} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12}
{7636C459-D2A3-30DB-4964-CAF22E6BA256} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{39FD406D-D77B-9593-CF95-B1BE32F4B7F9} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB}
Expand Down
43 changes: 43 additions & 0 deletions Libraries/src/AWS.OCI.Builder/AWS.OCI.Builder.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="..\..\..\buildtools\common.props" />

<PropertyGroup>
<TargetFrameworks>$(DefaultPackageTargets)</TargetFrameworks>
<AssemblyName>AWS.OCI.Builder</AssemblyName>
<PackageId>AWS.OCI.Builder</PackageId>
<RootNamespace>Amazon.OCI.Builder</RootNamespace>
<Description>A .NET library for building OCI container images programmatically — overlay local files as a new layer on a base image and push the result to Amazon ECR, without a Docker daemon.</Description>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>

<!-- Native AOT / trimming: mark the assembly trim-safe and turn on the analyzers so trim/AOT
warnings surface at build time. The library has no reflection-based serialization left;
all System.Text.Json goes through the source-generated OciJsonContext. -->
<IsTrimmable>true</IsTrimmable>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<IsAotCompatible>true</IsAotCompatible>

<!-- NuGet package metadata -->
<Version>0.5.0</Version>
<Product>AWS.OCI.Builder</Product>
<PackageTags>aws;ecr;oci;container;docker;image;registry</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/aws/aws-lambda-dotnet</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageProjectUrl>https://github.com/aws/aws-lambda-dotnet</PackageProjectUrl>
</PropertyGroup>

<ItemGroup>
<!-- Ship the README inside the package so it renders on the NuGet gallery. -->
<None Include=".\README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<!-- Internal types (RealmValidator, etc.) are exercised directly by the test project. -->
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>AWS.OCI.Builder.Tests, PublicKey="0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4"</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions Libraries/src/AWS.OCI.Builder/Descriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;

namespace Amazon.OCI.Builder;

/// <summary>
/// An OCI content descriptor: a typed, content-addressable reference to a blob
/// (a layer, a config, or a manifest) by its media type, size, and digest.
/// See the OCI Image Specification "descriptor" definition.
/// </summary>
public sealed record Descriptor
{
/// <summary>The media type of the referenced content (for example an image config or a gzipped layer).</summary>
[JsonPropertyName("mediaType")]
public required string MediaType { get; init; }

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

/// <summary>
/// The digest of the referenced content, in "sha256:&lt;hex&gt;" form. For a layer this is the
/// digest of the <em>compressed</em> (gzipped) tarball — the value used to address the blob in the registry.
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }

/// <summary>Optional alternate download locations for the content.</summary>
[JsonPropertyName("urls")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string[]? Urls { get; init; }
}
84 changes: 84 additions & 0 deletions Libraries/src/AWS.OCI.Builder/EcrPushOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
namespace Amazon.OCI.Builder;

/// <summary>
/// Options controlling how an image is pushed to Amazon ECR.
/// </summary>
/// <remarks>
/// All properties are optional. The ECR authorization token and target region are not configured here: the
/// token is passed directly to <see cref="OciImageBuilder.PushToEcrAsync"/>, and the region is taken from the
/// ECR host in the target reference.
/// </remarks>
public sealed class EcrPushOptions
{
/// <summary>
/// The default value of <see cref="MaxInMemoryLayerBytes"/>: 100&#160;MiB (binary megabytes). Chosen so that
/// common base images — whose individual layers run to tens of MiB — are copied entirely in memory, while
/// genuinely large layers spill to disk.
/// </summary>
public const long DefaultMaxInMemoryLayerBytes = 100L * 1024 * 1024;

/// <summary>
/// The size threshold, in bytes, that decides how a base-image layer is copied to ECR during a push. A base
/// layer whose size is at or below this value is buffered in memory; one that exceeds it is streamed through a
/// temporary file on disk instead, bounding peak memory for very large base images. Defaults to
/// <see cref="DefaultMaxInMemoryLayerBytes"/> (100&#160;MiB, binary).
/// </summary>
/// <remarks>
/// <para>
/// This affects only the copy of <em>base-image</em> layers (which are downloaded from the source registry and
/// re-uploaded to ECR). Layers built by this library are always written to a temporary file when they are added
/// (see <see cref="OciImageBuilder.AddLayerAsync"/>) and uploaded from that file during the push, regardless
/// of this setting.
/// </para>
/// <para>
/// Data integrity is preserved on both paths: the in-memory path verifies the downloaded bytes against the
/// layer's digest before upload, and the spill-to-disk path verifies the digest while streaming to disk
/// <em>and</em> re-reads the file from disk to verify it again before upload — so a faulty disk write that
/// corrupts the spilled layer is caught and the push fails rather than committing corrupt bytes to ECR. The
/// temporary file is deleted after the upload completes (and on failure or cancellation).
/// </para>
/// <para>
/// The value is interpreted as binary bytes. Set it very high to always buffer in memory, or to <c>0</c> to
/// always spill to disk. A negative value is rejected at push time.
/// </para>
/// </remarks>
public long MaxInMemoryLayerBytes { get; init; } = DefaultMaxInMemoryLayerBytes;

/// <summary>
/// An optional sink that receives a <see cref="LayerPushProgress"/> event as each layer is pushed, letting a
/// caller observe push progress (for example to render a per-layer progress bar). Defaults to
/// <see langword="null"/>, meaning no progress is reported.
/// </summary>
/// <remarks>
/// <para>
/// Each layer reports a <see cref="LayerPushPhase.Started"/> event, zero or more
/// <see cref="LayerPushPhase.Uploading"/> events as its bytes are sent, and a terminal
/// <see cref="LayerPushPhase.Completed"/> event; a layer the registry already has reports a single
/// <see cref="LayerPushPhase.Completed"/> event with <see cref="LayerPushProgress.IsSkipped"/> set. Each event
/// carries the layer's stable <see cref="LayerPushProgress.LayerIndex"/> / <see cref="LayerPushProgress.Digest"/>,
/// so the contract holds even if layers are later uploaded concurrently — see <see cref="LayerPushProgress"/>.
/// </para>
/// <para>
/// Use <see cref="System.Progress{T}"/> for the common case: it captures the current
/// <see cref="System.Threading.SynchronizationContext"/> and raises callbacks on it, which keeps UI updates on
/// the UI thread. Any custom implementation must be thread-safe, since a future parallel push may report from
/// multiple threads at once.
/// </para>
/// </remarks>
public IProgress<LayerPushProgress>? Progress { get; init; }

/// <summary>
/// The HTTP timeout applied to the registry client used for the push. Because a whole layer blob is sent in a
/// single <c>PATCH</c>, the default 100-second <see cref="System.Net.Http.HttpClient"/> timeout would cancel a
/// large-layer upload on a slow connection even while the caller's cancellation token is still valid. Defaults
/// to <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> (no client-imposed timeout), so an upload is
/// bounded only by the <see cref="System.Threading.CancellationToken"/> passed to
/// <see cref="OciImageBuilder.PushToEcrAsync"/> — pass a token with your own deadline to bound it. Set a finite
/// value to impose a per-request timeout instead.
/// </summary>
/// <remarks>
/// A finite value must be positive, or <see cref="System.Threading.Timeout.InfiniteTimeSpan"/>; any other value
/// (zero or negative) is rejected at push time.
/// </remarks>
public TimeSpan UploadTimeout { get; init; } = System.Threading.Timeout.InfiniteTimeSpan;
}
85 changes: 85 additions & 0 deletions Libraries/src/AWS.OCI.Builder/FileManifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Collections;

namespace Amazon.OCI.Builder;

/// <summary>
/// Describes a set of local files and/or directories to add to an image, each mapped to an absolute
/// destination path inside the container. A single <see cref="FileManifest"/> becomes a single image layer.
/// </summary>
/// <remarks>
/// <para>Collection-initializer syntax is supported, for example:</para>
/// <code>
/// var files = new FileManifest
/// {
/// { "/local/app.dll", "/app/app.dll" },
/// { "/local/config", "/app/config" },
/// };
/// </code>
/// <para>
/// File permissions in the resulting layer: when an entry's <see cref="FileManifestEntry.Mode"/> is not set,
/// the layer builder derives the Unix mode from the source file on Linux/macOS (preserving an executable bit),
/// and falls back to a standard <c>0644</c> for files / <c>0755</c> for directories on Windows, which has no Unix
/// mode bits to read. To guarantee a file is executable in the container regardless of the build host — the only
/// way to get the executable bit when building on Windows — use <see cref="AddExecutable"/> or pass an explicit
/// <see cref="System.IO.UnixFileMode"/> to <see cref="Add(string, string, System.IO.UnixFileMode)"/>.
/// </para>
/// </remarks>
public sealed class FileManifest : IEnumerable<FileManifestEntry>
{
private readonly List<FileManifestEntry> _entries = new();

/// <summary>The entries that make up this manifest.</summary>
public IReadOnlyList<FileManifestEntry> Entries => _entries;

/// <summary>
/// Adds a local file or directory to be placed at <paramref name="containerPath"/> inside the container, with
/// its Unix mode derived from the source (on Linux/macOS) or defaulted (on Windows).
/// </summary>
/// <param name="localPath">An absolute or relative path to a file or directory on the local filesystem.</param>
/// <param name="containerPath">The absolute destination path inside the container (for example <c>/app/app.dll</c>).</param>
public void Add(string localPath, string containerPath)
=> _entries.Add(new FileManifestEntry(localPath, containerPath));

/// <summary>
/// Adds a local file or directory with an explicit Unix <paramref name="mode"/> (for example
/// <c>(UnixFileMode)0b111_101_101</c> for <c>0755</c>). The explicit mode is applied to the mapped file, or to
/// every file produced by a mapped directory, overriding any source-derived or default mode.
/// </summary>
/// <param name="localPath">An absolute or relative path to a file or directory on the local filesystem.</param>
/// <param name="containerPath">The absolute destination path inside the container.</param>
/// <param name="mode">The Unix file mode to stamp on the entry.</param>
public void Add(string localPath, string containerPath, UnixFileMode mode)
=> _entries.Add(new FileManifestEntry(localPath, containerPath, mode));

/// <summary>
/// Adds a local file (or directory of files) and marks it executable (<c>0755</c>) in the container. Use this
/// for scripts and binaries so they are runnable regardless of the build host — on Windows the source file has
/// no executable bit to preserve, so this is the only way to produce an executable entry there.
/// </summary>
/// <param name="localPath">An absolute or relative path to a file or directory on the local filesystem.</param>
/// <param name="containerPath">The absolute destination path inside the container.</param>
public void AddExecutable(string localPath, string containerPath)
=> _entries.Add(new FileManifestEntry(localPath, containerPath, FileManifestEntry.ExecutableMode));

/// <inheritdoc />
public IEnumerator<FileManifestEntry> GetEnumerator() => _entries.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

/// <summary>A single local-to-container path mapping within a <see cref="FileManifest"/>.</summary>
/// <param name="LocalPath">The source path on the local filesystem.</param>
/// <param name="ContainerPath">The absolute destination path inside the container.</param>
/// <param name="Mode">
/// An explicit Unix file mode for the entry, or <see langword="null"/> to derive it from the source (on
/// Linux/macOS) or use the default (on Windows). For a directory mapping, an explicit mode applies to the files
/// it contains.
/// </param>
public readonly record struct FileManifestEntry(string LocalPath, string ContainerPath, UnixFileMode? Mode = null)
{
/// <summary>A standard executable file mode: <c>0755</c> (rwxr-xr-x).</summary>
public const UnixFileMode ExecutableMode =
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
}
Loading
Loading