Skip to content
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

Default the ContainerUser to rootless users on .NET 8 and above #393

Merged
merged 3 commits into from
Mar 21, 2023
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
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();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this as a helper while debugging tests.


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 () {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to dispose the ProjectCollection to ensure that the binlogs get written, again to help with debugging.

[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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the ProjectInitializer to use TFM instead of the private property, and 'generate' the private property as part of the method call. This seemed less surprising.

["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")]
Comment on lines +160 to +165
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our boundary conditions are:

  • 2 OS's
  • 3 ranges - less than 8.0, 8.0 exactly, and greater than 8.0

So these 6 tests should cover all permutations.

[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