From 95de4b37bb4909343c3650da44bce216b896fd37 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 3 Mar 2023 12:26:54 -0600 Subject: [PATCH] Add support for setting the `User` metadata item (#374) --- .../ContainerBuilder.cs | 21 +++++++---- .../ImageBuilder.cs | 2 + Microsoft.NET.Build.Containers/ImageConfig.cs | 12 +++++- .../PublicAPI/net472/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net7.0/PublicAPI.Unshipped.txt | 4 +- .../Tasks/CreateNewImage.Interface.cs | 10 +++++ .../Tasks/CreateNewImageToolTask.cs | 9 ++++- containerize/Program.cs | 37 +++++++++++-------- .../Microsoft.NET.Build.Containers.targets | 3 +- 9 files changed, 72 insertions(+), 28 deletions(-) diff --git a/Microsoft.NET.Build.Containers/ContainerBuilder.cs b/Microsoft.NET.Build.Containers/ContainerBuilder.cs index b6c80d08..614bdd75 100644 --- a/Microsoft.NET.Build.Containers/ContainerBuilder.cs +++ b/Microsoft.NET.Build.Containers/ContainerBuilder.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json; -using System.Threading.Tasks; using Microsoft.NET.Build.Containers.Resources; namespace Microsoft.NET.Build.Containers; @@ -16,16 +15,17 @@ public static async Task ContainerizeAsync( string baseName, string baseTag, string[] entrypoint, - string[] entrypointArgs, + string[]? entrypointArgs, string imageName, string[] imageTags, string? outputRegistry, - string[] labels, - Port[] exposedPorts, - string[] envVars, + string[]? labels, + Port[]? exposedPorts, + string[]? envVars, string containerRuntimeIdentifier, string ridGraphPath, string localContainerDaemon, + string? containerUser, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -57,7 +57,7 @@ public static async Task ContainerizeAsync( imageBuilder.SetEntryPoint(entrypoint, entrypointArgs); - foreach (string label in labels) + foreach (string label in labels ?? Array.Empty()) { string[] labelPieces = label.Split('='); @@ -65,19 +65,24 @@ public static async Task ContainerizeAsync( imageBuilder.AddLabel(labelPieces[0], TryUnquote(labelPieces[1])); } - foreach (string envVar in envVars) + foreach (string envVar in envVars ?? Array.Empty() ) { string[] envPieces = envVar.Split('=', 2); imageBuilder.AddEnvironmentVariable(envPieces[0], TryUnquote(envPieces[1])); } - foreach ((int number, PortType type) in exposedPorts) + foreach ((int number, PortType type) in exposedPorts ?? Array.Empty()) { // ports are validated by System.CommandLine API imageBuilder.ExposePort(number, type); } + if (containerUser is { } user) + { + imageBuilder.SetUser(user); + } + BuiltImage builtImage = imageBuilder.Build(); cancellationToken.ThrowIfCancellationRequested(); diff --git a/Microsoft.NET.Build.Containers/ImageBuilder.cs b/Microsoft.NET.Build.Containers/ImageBuilder.cs index 0b2f9adb..0c626e84 100644 --- a/Microsoft.NET.Build.Containers/ImageBuilder.cs +++ b/Microsoft.NET.Build.Containers/ImageBuilder.cs @@ -83,4 +83,6 @@ internal void AddLayer(Layer l) /// Sets an entry point for the image. /// internal void SetEntryPoint(string[] executableArgs, string[]? args = null) => _baseImageConfig.SetEntryPoint(executableArgs, args); + + internal void SetUser(string user) => _baseImageConfig.SetUser(user); } diff --git a/Microsoft.NET.Build.Containers/ImageConfig.cs b/Microsoft.NET.Build.Containers/ImageConfig.cs index faec4b1f..73a77f01 100644 --- a/Microsoft.NET.Build.Containers/ImageConfig.cs +++ b/Microsoft.NET.Build.Containers/ImageConfig.cs @@ -17,6 +17,7 @@ internal sealed class ImageConfig private readonly Dictionary _environmentVariables; private string? _newWorkingDirectory; private (string[] ExecutableArgs, string[]? Args)? _newEntryPoint; + private string? _user; /// /// Models the file system of the image. Typically has a key 'type' with value 'layers' and a key 'diff_ids' with a list of layer digests. @@ -45,8 +46,10 @@ internal ImageConfig(JsonNode config) _architecture = GetArchitecture(); _os = GetOs(); _history = GetHistory(); + _user = GetUser(); } + private string? GetUser() => _config["config"]?["User"]?.ToString(); private List GetHistory() => _config["history"]?.AsArray().Select(node => node.Deserialize()!).ToList() ?? new List(); private string GetOs() => _config["os"]?.ToString() ?? throw new ArgumentException("Base image configuration should contain an 'os' property."); private string GetArchitecture() => _config["architecture"]?.ToString() ?? throw new ArgumentException("Base image configuration should contain an 'architecture' property."); @@ -90,9 +93,14 @@ internal string BuildConfig() } } + if (_user is not null) + { + newConfig["User"] = _user; + } + // These fields aren't (yet) supported by the task layer, but we should // preserve them if they're already set in the base image. - foreach (string propertyName in new [] { "User", "Volumes", "StopSignal" }) + foreach (string propertyName in new [] { "Volumes", "StopSignal" }) { if (_config["config"]?[propertyName] is JsonNode propertyValue) { @@ -186,6 +194,8 @@ internal void AddLayer(Layer l) _rootFsLayers.Add(l.Descriptor.UncompressedDigest!); } + internal void SetUser(string user) => _user = user; + private HashSet GetExposedPorts() { HashSet ports = new(); diff --git a/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt b/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt index 5d542535..a49e582f 100644 --- a/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/Microsoft.NET.Build.Containers/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -30,6 +30,8 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerizeDirectory.get -> Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerizeDirectory.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerRuntimeIdentifier.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerRuntimeIdentifier.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerUser.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerUser.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.CreateNewImage() -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Entrypoint.get -> Microsoft.Build.Framework.ITaskItem![]! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Entrypoint.set -> void diff --git a/Microsoft.NET.Build.Containers/PublicAPI/net7.0/PublicAPI.Unshipped.txt b/Microsoft.NET.Build.Containers/PublicAPI/net7.0/PublicAPI.Unshipped.txt index ba0ad9cc..ed5c5975 100644 --- a/Microsoft.NET.Build.Containers/PublicAPI/net7.0/PublicAPI.Unshipped.txt +++ b/Microsoft.NET.Build.Containers/PublicAPI/net7.0/PublicAPI.Unshipped.txt @@ -1,6 +1,7 @@ const Microsoft.NET.Build.Containers.KnownDaemonTypes.Docker = "Docker" -> string! Microsoft.NET.Build.Containers.BaseImageNotFoundException Microsoft.NET.Build.Containers.Constants +static Microsoft.NET.Build.Containers.ContainerBuilder.ContainerizeAsync(System.IO.DirectoryInfo! folder, string! workingDir, string! registryName, string! baseName, string! baseTag, string![]! entrypoint, string![]? entrypointArgs, string! imageName, string![]! imageTags, string? outputRegistry, string![]? labels, Microsoft.NET.Build.Containers.Port[]? exposedPorts, string![]? envVars, string! containerRuntimeIdentifier, string! ridGraphPath, string! localContainerDaemon, string? containerUser, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static readonly Microsoft.NET.Build.Containers.Constants.Version -> string! Microsoft.NET.Build.Containers.ContainerBuilder Microsoft.NET.Build.Containers.ContainerHelpers @@ -118,6 +119,8 @@ Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerizeDirectory.get -> Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerizeDirectory.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerRuntimeIdentifier.get -> string! Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerRuntimeIdentifier.set -> void +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerUser.get -> string! +Microsoft.NET.Build.Containers.Tasks.CreateNewImage.ContainerUser.set -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.CreateNewImage() -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Dispose() -> void Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Entrypoint.get -> Microsoft.Build.Framework.ITaskItem![]! @@ -173,7 +176,6 @@ Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerReg Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.ParsedContainerTag.get -> string! override Microsoft.NET.Build.Containers.Tasks.CreateNewImage.Execute() -> bool override Microsoft.NET.Build.Containers.Tasks.ParseContainerProperties.Execute() -> bool -static Microsoft.NET.Build.Containers.ContainerBuilder.ContainerizeAsync(System.IO.DirectoryInfo! folder, string! workingDir, string! registryName, string! baseName, string! baseTag, string![]! entrypoint, string![]! entrypointArgs, string! imageName, string![]! imageTags, string? outputRegistry, string![]! labels, Microsoft.NET.Build.Containers.Port[]! exposedPorts, string![]! envVars, string! containerRuntimeIdentifier, string! ridGraphPath, string! localContainerDaemon, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.NET.Build.Containers.ContainerHelpers.TryParsePort(string! input, out Microsoft.NET.Build.Containers.Port? port, out Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError? error) -> bool static Microsoft.NET.Build.Containers.ContainerHelpers.TryParsePort(string? portNumber, string? portType, out Microsoft.NET.Build.Containers.Port? port, out Microsoft.NET.Build.Containers.ContainerHelpers.ParsePortError? error) -> bool static readonly Microsoft.NET.Build.Containers.KnownDaemonTypes.SupportedLocalDaemonTypes -> string![]! diff --git a/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs b/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs index 6c5b6b28..f40c3955 100644 --- a/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs +++ b/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs @@ -114,6 +114,15 @@ partial class CreateNewImage [Required] public string RuntimeIdentifierGraphPath { get; set; } + /// + /// The username or UID which is a platform-specific structure that allows specific control over which user the process run as. + /// This acts as a default value to use when the value is not specified when creating a container. + /// For Linux based systems, all of the following are valid: user, uid, user:group, uid:gid, uid:group, user:gid. + /// If group/gid is not specified, the default group and supplementary groups of the given user/uid in /etc/passwd and /etc/group from the container are applied. + /// If group/gid is specified, supplementary groups from the container are ignored. + /// + public string ContainerUser { get; set; } + [Output] public string GeneratedContainerManifest { get; set; } @@ -141,6 +150,7 @@ public CreateNewImage() ContainerRuntimeIdentifier = ""; RuntimeIdentifierGraphPath = ""; LocalContainerDaemon = ""; + ContainerUser = ""; GeneratedContainerConfiguration = ""; GeneratedContainerManifest = ""; diff --git a/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs b/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs index e3950c90..45f99d71 100644 --- a/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs +++ b/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs @@ -113,7 +113,7 @@ internal string GenerateCommandLineCommandsInt() builder.AppendSwitchIfNotNull("--workingdirectory ", WorkingDirectory); ITaskItem[] sanitizedEntryPoints = Entrypoint.Where(e => !string.IsNullOrWhiteSpace(e.ItemSpec)).ToArray(); builder.AppendSwitchIfNotNull("--entrypoint ", sanitizedEntryPoints, delimiter: " "); - + //optional options if (!string.IsNullOrWhiteSpace(BaseImageTag)) { @@ -185,10 +185,17 @@ internal string GenerateCommandLineCommandsInt() { builder.AppendSwitchIfNotNull("--rid ", ContainerRuntimeIdentifier); } + if (!string.IsNullOrWhiteSpace(RuntimeIdentifierGraphPath)) { builder.AppendSwitchIfNotNull("--ridgraphpath ", RuntimeIdentifierGraphPath); } + + if (!string.IsNullOrWhiteSpace(ContainerUser)) + { + builder.AppendSwitchIfNotNull("--container-user ", ContainerUser); + } + return builder.ToString(); } diff --git a/containerize/Program.cs b/containerize/Program.cs index d1e90a88..c568678f 100644 --- a/containerize/Program.cs +++ b/containerize/Program.cs @@ -6,7 +6,7 @@ using System.CommandLine.Parsing; using System.Text; -#pragma warning disable CA1852 +#pragma warning disable CA1852 var publishDirectoryArg = new Argument( name: "PublishDirectory", @@ -164,6 +164,8 @@ var ridGraphPathOpt = new Option(name: "--ridgraphpath", description: "Path to the RID graph file."); +var containerUserOpt = new Option(name: "--container-user", description: "User to run the container as."); + RootCommand root = new RootCommand("Containerize an application without Docker.") { publishDirectoryArg, @@ -181,27 +183,29 @@ envVarsOpt, ridOpt, ridGraphPathOpt, - localContainerDaemonOpt + localContainerDaemonOpt, + containerUserOpt }; root.SetHandler(async (context) => { DirectoryInfo _publishDir = context.ParseResult.GetValueForArgument(publishDirectoryArg); - string _baseReg = context.ParseResult.GetValueForOption(baseRegistryOpt) ?? ""; - string _baseName = context.ParseResult.GetValueForOption(baseImageNameOpt) ?? ""; - string _baseTag = context.ParseResult.GetValueForOption(baseImageTagOpt) ?? ""; + string _baseReg = context.ParseResult.GetValueForOption(baseRegistryOpt)!; + string _baseName = context.ParseResult.GetValueForOption(baseImageNameOpt)!; + string _baseTag = context.ParseResult.GetValueForOption(baseImageTagOpt)!; string? _outputReg = context.ParseResult.GetValueForOption(outputRegistryOpt); - string _name = context.ParseResult.GetValueForOption(imageNameOpt) ?? ""; - string[] _tags = context.ParseResult.GetValueForOption(imageTagsOpt) ?? Array.Empty(); - string _workingDir = context.ParseResult.GetValueForOption(workingDirectoryOpt) ?? ""; - string[] _entrypoint = context.ParseResult.GetValueForOption(entrypointOpt) ?? Array.Empty(); - string[] _entrypointArgs = context.ParseResult.GetValueForOption(entrypointArgsOpt) ?? Array.Empty(); - string[] _labels = context.ParseResult.GetValueForOption(labelsOpt) ?? Array.Empty(); - Port[] _ports = context.ParseResult.GetValueForOption(portsOpt) ?? Array.Empty(); - string[] _envVars = context.ParseResult.GetValueForOption(envVarsOpt) ?? Array.Empty(); - string _rid = context.ParseResult.GetValueForOption(ridOpt) ?? ""; - string _ridGraphPath = context.ParseResult.GetValueForOption(ridGraphPathOpt) ?? ""; - string _localContainerDaemon = context.ParseResult.GetValueForOption(localContainerDaemonOpt) ?? ""; + string _name = context.ParseResult.GetValueForOption(imageNameOpt)!; + string[] _tags = context.ParseResult.GetValueForOption(imageTagsOpt)!; + string _workingDir = context.ParseResult.GetValueForOption(workingDirectoryOpt)!; + string[] _entrypoint = context.ParseResult.GetValueForOption(entrypointOpt)!; + string[]? _entrypointArgs = context.ParseResult.GetValueForOption(entrypointArgsOpt); + string[]? _labels = context.ParseResult.GetValueForOption(labelsOpt); + Port[]? _ports = context.ParseResult.GetValueForOption(portsOpt); + string[]? _envVars = context.ParseResult.GetValueForOption(envVarsOpt); + string _rid = context.ParseResult.GetValueForOption(ridOpt)!; + string _ridGraphPath = context.ParseResult.GetValueForOption(ridGraphPathOpt)!; + string _localContainerDaemon = context.ParseResult.GetValueForOption(localContainerDaemonOpt)!; + string? _containerUser = context.ParseResult.GetValueForOption(containerUserOpt); await ContainerBuilder.ContainerizeAsync( _publishDir, _workingDir, @@ -219,6 +223,7 @@ await ContainerBuilder.ContainerizeAsync( _rid, _ridGraphPath, _localContainerDaemon, + _containerUser, context.GetCancellationToken()).ConfigureAwait(false); }); diff --git a/packaging/build/Microsoft.NET.Build.Containers.targets b/packaging/build/Microsoft.NET.Build.Containers.targets index e8aff5f1..4058b4c2 100644 --- a/packaging/build/Microsoft.NET.Build.Containers.targets +++ b/packaging/build/Microsoft.NET.Build.Containers.targets @@ -185,8 +185,9 @@ ExposedPorts="@(ContainerPort)" ContainerEnvironmentVariables="@(ContainerEnvironmentVariables)" ContainerRuntimeIdentifier="$(ContainerRuntimeIdentifier)" + ContainerUser="$(ContainerUser)" RuntimeIdentifierGraphPath="$(RuntimeIdentifierGraphPath)"> - +