Skip to content

Commit

Permalink
Merge pull request #393 from dotnet/dotnet-8-container-user
Browse files Browse the repository at this point in the history
  • Loading branch information
baronfel committed Mar 21, 2023
2 parents a402cc9 + 51cb1ff commit 1628927
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class CapturingLogger : ILogger
private List<BuildErrorEventArgs> _errors = new();
public IReadOnlyList<BuildErrorEventArgs> Errors {get { return _errors; } }

public List<string> AllMessages => Errors.Select(e => e.Message!).Concat(Warnings.Select(w => w.Message!)).Concat(Messages.Select(m => m.Message!)).ToList();

public void Initialize(IEventSource eventSource)
{
eventSource.MessageRaised += (o, e) => _messages.Add(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ public class ParseContainerPropertiesTests
[DockerDaemonAvailableFact]
public void Baseline()
{
var (project, _) = ProjectInitializer.InitProject(new () {
var (project, _, d) = ProjectInitializer.InitProject(new () {
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
[ContainerRegistry] = "localhost:5010",
[ContainerImageName] = "dotnet/testimage",
[ContainerImageTags] = "7.0;latest"
});
using var _ = d;
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
Assert.True(instance.Build(new[]{ComputeContainerConfig}, null, null, out var outputs));

Expand All @@ -37,11 +38,11 @@ public void Baseline()
[DockerDaemonAvailableFact]
public void SpacesGetReplacedWithDashes()
{
var (project, _) = ProjectInitializer.InitProject(new () {
var (project, _, d) = ProjectInitializer.InitProject(new () {
[ContainerBaseImage] = "mcr microsoft com/dotnet runtime:7.0",
[ContainerRegistry] = "localhost:5010"
});

using var _ = d;
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
Assert.True(instance.Build(new[]{ComputeContainerConfig}, null, null, out var outputs));

Expand All @@ -53,13 +54,13 @@ public void SpacesGetReplacedWithDashes()
[DockerDaemonAvailableFact]
public void RegexCatchesInvalidContainerNames()
{
var (project, logs) = ProjectInitializer.InitProject(new () {
var (project, logs, d) = ProjectInitializer.InitProject(new () {
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
[ContainerRegistry] = "localhost:5010",
[ContainerImageName] = "dotnet testimage",
[ContainerImageTag] = "5.0"
});

using var _ = d;
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
Assert.True(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs));
Assert.Contains(logs.Messages, m => m.Message?.Contains("'ContainerImageName' was not a valid container image name, it was normalized to 'dotnet-testimage'") == true);
Expand All @@ -68,13 +69,13 @@ public void RegexCatchesInvalidContainerNames()
[DockerDaemonAvailableFact]
public void RegexCatchesInvalidContainerTags()
{
var (project, logs) = ProjectInitializer.InitProject(new () {
var (project, logs, d) = ProjectInitializer.InitProject(new () {
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
[ContainerRegistry] = "localhost:5010",
[ContainerImageName] = "dotnet/testimage",
[ContainerImageTag] = "5 0"
});

using var _ = d;
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
Assert.False(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs));

Expand All @@ -85,14 +86,14 @@ public void RegexCatchesInvalidContainerTags()
[DockerDaemonAvailableFact]
public void CanOnlySupplyOneOfTagAndTags()
{
var (project, logs) = ProjectInitializer.InitProject(new () {
var (project, logs, d) = ProjectInitializer.InitProject(new () {
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
[ContainerRegistry] = "localhost:5010",
[ContainerImageName] = "dotnet/testimage",
[ContainerImageTag] = "5.0",
[ContainerImageTags] = "latest;oldest"
});

using var _ = d;
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
Assert.False(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ private static string CombineFiles(string propsFile, string targetsFile)
return tempTargetLocation;
}

public static (Project, CapturingLogger) InitProject(Dictionary<string, string> bonusProps, [CallerMemberName]string projectName = "")
public static (Project, CapturingLogger, IDisposable) InitProject(Dictionary<string, string> bonusProps, [CallerMemberName]string projectName = "")
{
var props = new Dictionary<string, string>();
// required parameters
props["TargetFileName"] = "foo.dll";
props["AssemblyName"] = "foo";
props["_TargetFrameworkVersionWithoutV"] = "7.0";
props["TargetFrameworkVersion"] = "v7.0";
props["_NativeExecutableExtension"] = ".exe"; //TODO: windows/unix split here
props["Version"] = "1.0.0"; // TODO: need to test non-compliant version strings here
props["NetCoreSdkVersion"] = "7.0.100"; // TODO: float this to current SDK?
// test setup parameters so that we can load the props/targets/tasks
// test setup parameters so that we can load the props/targets/tasks
props["ContainerCustomTasksAssembly"] = Path.GetFullPath(Path.Combine(".", "Microsoft.NET.Build.Containers.dll"));
props["_IsTest"] = "true";

Expand All @@ -63,6 +63,8 @@ public static (Project, CapturingLogger) InitProject(Dictionary<string, string>
{
props[kvp.Key] = kvp.Value;
}
return (collection.LoadProject(_combinedTargetsLocation, props, null), logs);
// derived properties, since these might be set by bonusProps
props["_TargetFrameworkVersionWithoutV"] = props["TargetFrameworkVersion"].TrimStart('v');
return (collection.LoadProject(_combinedTargetsLocation, props, null), logs, collection);
}
}
72 changes: 50 additions & 22 deletions Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Xunit;
using Microsoft.NET.Build.Containers.IntegrationTests;
using Microsoft.NET.Build.Containers.UnitTests;
using System.Linq;

namespace Microsoft.NET.Build.Containers.Targets.IntegrationTests;

Expand All @@ -18,10 +19,11 @@ public class TargetsTests
[DockerDaemonAvailableTheory]
public void CanSetEntrypointArgsToUseAppHost(bool useAppHost, params string[] entrypointArgs)
{
var (project, _) = ProjectInitializer.InitProject(new()
var (project, _, d) = ProjectInitializer.InitProject(new()
{
[UseAppHost] = useAppHost.ToString()
}, projectName: $"{nameof(CanSetEntrypointArgsToUseAppHost)}_{useAppHost}_{String.Join("_", entrypointArgs)}");
using var _ = d;
Assert.True(project.Build(ComputeContainerConfig));
var computedEntrypointArgs = project.GetItems(ContainerEntrypoint).Select(i => i.EvaluatedInclude).ToArray();
foreach (var (First, Second) in entrypointArgs.Zip(computedEntrypointArgs))
Expand All @@ -38,10 +40,11 @@ public void CanSetEntrypointArgsToUseAppHost(bool useAppHost, params string[] en
[DockerDaemonAvailableTheory]
public void CanNormalizeInputContainerNames(string projectName, string expectedContainerImageName, bool shouldPass)
{
var (project, _) = ProjectInitializer.InitProject(new()
var (project, _, d) = ProjectInitializer.InitProject(new()
{
[AssemblyName] = projectName
}, projectName: $"{nameof(CanNormalizeInputContainerNames)}_{projectName}_{expectedContainerImageName}_{shouldPass}");
using var _ = d;
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
instance.Build(new[] { ComputeContainerConfig }, null, null, out var outputs).Should().Be(shouldPass, "Build should have succeeded");
Assert.Equal(expectedContainerImageName, instance.GetPropertyValue(ContainerImageName));
Expand All @@ -56,11 +59,12 @@ public void CanNormalizeInputContainerNames(string projectName, string expectedC
[DockerDaemonAvailableTheory]
public void CanWarnOnInvalidSDKVersions(string sdkVersion, bool isAllowed)
{
var (project, _) = ProjectInitializer.InitProject(new()
var (project, _, d) = ProjectInitializer.InitProject(new()
{
["NETCoreSdkVersion"] = sdkVersion,
["PublishProfile"] = "DefaultContainer"
}, projectName: $"{nameof(CanWarnOnInvalidSDKVersions)}_{sdkVersion}_{isAllowed}");
using var _ = d;
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
var derivedIsAllowed = Boolean.Parse(project.GetProperty("_IsSDKContainerAllowedVersion").EvaluatedValue);
// var buildResult = instance.Build(new[]{"_ContainerVerifySDKVersion"}, null, null, out var outputs);
Expand All @@ -72,10 +76,11 @@ public void CanWarnOnInvalidSDKVersions(string sdkVersion, bool isAllowed)
[DockerDaemonAvailableTheory]
public void GetsConventionalLabelsByDefault(bool shouldEvaluateLabels)
{
var (project, _) = ProjectInitializer.InitProject(new()
var (project, _, d) = ProjectInitializer.InitProject(new()
{
[ContainerGenerateLabels] = shouldEvaluateLabels.ToString()
}, projectName: $"{nameof(GetsConventionalLabelsByDefault)}_{shouldEvaluateLabels}");
using var _ = d;
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
instance.Build(new[] { ComputeContainerConfig }, null, null, out var outputs).Should().BeTrue("Build should have succeeded");
if (shouldEvaluateLabels)
Expand All @@ -98,14 +103,15 @@ public void ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn(bool includeSour
var commitHash = "abcdef";
var repoUrl = "https://git.cosmere.com/shard/whimsy.git";

var (project, _) = ProjectInitializer.InitProject(new()
var (project, logger, d) = ProjectInitializer.InitProject(new()
{
["PublishRepositoryUrl"] = includeSourceControl.ToString(),
["PrivateRepositoryUrl"] = repoUrl,
["SourceRevisionId"] = commitHash
}, projectName: $"{nameof(ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn)}_{includeSourceControl}");
using var _ = d;
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
instance.Build(new[] { ComputeContainerConfig }, null, null, out var outputs).Should().BeTrue("Build should have succeeded");
instance.Build(new[] { ComputeContainerConfig }, null, null, out var outputs).Should().BeTrue("Build should have succeeded but failed due to {0}", String.Join("\n", logger.AllMessages));
var labels = instance.GetItems(ContainerLabel);
if (includeSourceControl)
{
Expand All @@ -121,32 +127,54 @@ public void ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn(bool includeSour
};
}

[InlineData("7.0.100", "7.0", "7.0")]
[InlineData("7.0.100-preview.7", "7.0", "7.0")]
[InlineData("7.0.100-rc.1", "7.0", "7.0")]
[InlineData("8.0.100", "8.0", "8.0")]
[InlineData("8.0.100", "7.0", "7.0")]
[InlineData("8.0.100-preview.7", "8.0", "8.0-preview.7")]
[InlineData("8.0.100-rc.1", "8.0", "8.0-rc.1")]
[InlineData("8.0.100-rc.1", "7.0", "7.0")]
[InlineData("8.0.200", "8.0", "8.0")]
[InlineData("8.0.200", "7.0", "7.0")]
[InlineData("8.0.200-preview3", "7.0", "7.0")]
[InlineData("8.0.200-preview3", "8.0", "8.0")]
[InlineData("6.0.100", "6.0", "6.0")]
[InlineData("6.0.100-preview.1", "6.0", "6.0")]
[InlineData("7.0.100", "v7.0", "7.0")]
[InlineData("7.0.100-preview.7", "v7.0", "7.0")]
[InlineData("7.0.100-rc.1", "v7.0", "7.0")]
[InlineData("8.0.100", "v8.0", "8.0")]
[InlineData("8.0.100", "v7.0", "7.0")]
[InlineData("8.0.100-preview.7", "v8.0", "8.0-preview.7")]
[InlineData("8.0.100-rc.1", "v8.0", "8.0-rc.1")]
[InlineData("8.0.100-rc.1", "v7.0", "7.0")]
[InlineData("8.0.200", "v8.0", "8.0")]
[InlineData("8.0.200", "v7.0", "7.0")]
[InlineData("8.0.200-preview3", "v7.0", "7.0")]
[InlineData("8.0.200-preview3", "v8.0", "8.0")]
[InlineData("6.0.100", "v6.0", "6.0")]
[InlineData("6.0.100-preview.1", "v6.0", "6.0")]
[Theory]
public void CanComputeTagsForSupportedSDKVersions(string sdkVersion, string tfm, string expectedTag)
{
var (project, logger) = ProjectInitializer.InitProject(new()
var (project, logger, d) = ProjectInitializer.InitProject(new()
{
["NETCoreSdkVersion"] = sdkVersion,
["_TargetFrameworkVersionWithoutV"] = tfm,
["TargetFrameworkVersion"] = tfm,
["PublishProfile"] = "DefaultContainer"
}, projectName: $"{nameof(CanComputeTagsForSupportedSDKVersions)}_{sdkVersion}_{tfm}_{expectedTag}");
using var _ = d;
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
instance.Build(new[]{"_ComputeContainerBaseImageTag"}, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
var computedTag = instance.GetProperty("_ContainerBaseImageTag").EvaluatedValue;
computedTag.Should().Be(expectedTag);
}

[InlineData("v8.0", "linux-x64", "64198")]
[InlineData("v8.0", "win-x64", "ContainerUser")]
[InlineData("v7.0", "linux-x64", null)]
[InlineData("v7.0", "win-x64", null)]
[InlineData("v9.0", "linux-x64", "64198")]
[InlineData("v9.0", "win-x64", "ContainerUser")]
[Theory]
public void CanComputeContainerUser(string tfm, string rid, string expectedUser)
{
var (project, logger, d) = ProjectInitializer.InitProject(new()
{
["TargetFrameworkVersion"] = tfm,
["ContainerRuntimeIdentifier"] = rid
}, projectName: $"{nameof(CanComputeTagsForSupportedSDKVersions)}_{tfm}_{rid}_{expectedUser}");
using var _ = d;
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
instance.Build(new[]{ComputeContainerConfig}, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
var computedTag = instance.GetProperty("ContainerUser")?.EvaluatedValue;
computedTag.Should().Be(expectedUser);
}
}
21 changes: 21 additions & 0 deletions docs/ContainerCustomization.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,27 @@ ContainerEntrypointArg items have one property:
</ItemGroup>
```

## ContainerUser

This item controls the default user that the container will run as. This is often used to run the container as a non-root user, which is a best practice for security. There are a few constraints to know about this field:

* It can take a variety of forms - user name, linux user ids, group name, linux group id, `username:groupname`, id variants of the above
* There is no verification that the user or group specified exists on the image
* Changing the user can alter the behavior of the application, especially in regards to things like File System permissions

The default value of this field varies by project TFM and target operating system:

* if you are targeting .NET 8 or higher and using the Microsoft runtime images, then
* on Linux the rootless user `app` will be used (though it will be referenced by its user id)
* on Windows the rootless user `ContainerUser` will be used
* otherwise no default `ContainerUser` will be used

```xml
<PropertyGroup>
<ContainerUser>my-existing-app-user</ContainerUser>
</PropertyGroup>
```

## Default container labels

Labels are often used to provide consistent metadata on container images. This package provides some default labels to encourage better maintainability of the generated images, drawn from the set defined as part of the [OCI Image specification](https://github.com/opencontainers/image-spec/blob/main/annotations.md). Where possible, we use the values of common [NuGet Project Properties](https://learn.microsoft.com/en-us/nuget/reference/msbuild-targets#pack-target) as defaults for these annotations, though we also provide more specific properties for each of these labels.
Expand Down
Loading

0 comments on commit 1628927

Please sign in to comment.