Repository navigation
Library for building and pushing images to ECR without a container runtime #2602
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
base: master
Are you sure you want to change the base?
Changes from all commits
c0e0a68
d2c6402
669231d
5504c50
0fc2534
ed26046
e91548b
e8f5ad5
9fee31d
fcd219c
f831260
20013b6
7bdb22f
1728773
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
| ] | ||
| } | ||
| ] | ||
| } |
| 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" | ||
| ] | ||
| } | ||
| } |
| 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>AWS.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> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| using System.Text.Json.Serialization; | ||
|
|
||
| namespace AWS.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> | ||
| internal 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:<hex>" 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; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| namespace AWS.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 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 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. | ||
|
Comment on lines
+62
to
+65
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Please ship a thread-safe sink that queues events for the caller to poll, with a method that drains them into an array ( |
||
| /// </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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| using System.Collections; | ||
|
|
||
| namespace AWS.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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buildtools/common.propsturns offGenerateAssemblyVersionAttributeandGenerateAssemblyFileVersionAttribute, so the assembly version is0.0.0.0and there's no file version; only the informational version (0.5.0+<sha>) is set. Please turn them back on here, asAmazon.Lambda.RuntimeSupportandSnapshotRestore.Registrydo.