Skip to content

Commit 1628927

Browse files
authored
Merge pull request #393 from dotnet/dotnet-8-container-user
2 parents a402cc9 + 51cb1ff commit 1628927

File tree

6 files changed

+107
-40
lines changed

6 files changed

+107
-40
lines changed

Microsoft.NET.Build.Containers.IntegrationTests/CapturingLogger.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public class CapturingLogger : ILogger
1919
private List<BuildErrorEventArgs> _errors = new();
2020
public IReadOnlyList<BuildErrorEventArgs> Errors {get { return _errors; } }
2121

22+
public List<string> AllMessages => Errors.Select(e => e.Message!).Concat(Warnings.Select(w => w.Message!)).Concat(Messages.Select(m => m.Message!)).ToList();
23+
2224
public void Initialize(IEventSource eventSource)
2325
{
2426
eventSource.MessageRaised += (o, e) => _messages.Add(e);

Microsoft.NET.Build.Containers.IntegrationTests/ParseContainerPropertiesTests.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ public class ParseContainerPropertiesTests
1616
[DockerDaemonAvailableFact]
1717
public void Baseline()
1818
{
19-
var (project, _) = ProjectInitializer.InitProject(new () {
19+
var (project, _, d) = ProjectInitializer.InitProject(new () {
2020
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
2121
[ContainerRegistry] = "localhost:5010",
2222
[ContainerImageName] = "dotnet/testimage",
2323
[ContainerImageTags] = "7.0;latest"
2424
});
25+
using var _ = d;
2526
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
2627
Assert.True(instance.Build(new[]{ComputeContainerConfig}, null, null, out var outputs));
2728

@@ -37,11 +38,11 @@ public void Baseline()
3738
[DockerDaemonAvailableFact]
3839
public void SpacesGetReplacedWithDashes()
3940
{
40-
var (project, _) = ProjectInitializer.InitProject(new () {
41+
var (project, _, d) = ProjectInitializer.InitProject(new () {
4142
[ContainerBaseImage] = "mcr microsoft com/dotnet runtime:7.0",
4243
[ContainerRegistry] = "localhost:5010"
4344
});
44-
45+
using var _ = d;
4546
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
4647
Assert.True(instance.Build(new[]{ComputeContainerConfig}, null, null, out var outputs));
4748

@@ -53,13 +54,13 @@ public void SpacesGetReplacedWithDashes()
5354
[DockerDaemonAvailableFact]
5455
public void RegexCatchesInvalidContainerNames()
5556
{
56-
var (project, logs) = ProjectInitializer.InitProject(new () {
57+
var (project, logs, d) = ProjectInitializer.InitProject(new () {
5758
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
5859
[ContainerRegistry] = "localhost:5010",
5960
[ContainerImageName] = "dotnet testimage",
6061
[ContainerImageTag] = "5.0"
6162
});
62-
63+
using var _ = d;
6364
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
6465
Assert.True(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs));
6566
Assert.Contains(logs.Messages, m => m.Message?.Contains("'ContainerImageName' was not a valid container image name, it was normalized to 'dotnet-testimage'") == true);
@@ -68,13 +69,13 @@ public void RegexCatchesInvalidContainerNames()
6869
[DockerDaemonAvailableFact]
6970
public void RegexCatchesInvalidContainerTags()
7071
{
71-
var (project, logs) = ProjectInitializer.InitProject(new () {
72+
var (project, logs, d) = ProjectInitializer.InitProject(new () {
7273
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
7374
[ContainerRegistry] = "localhost:5010",
7475
[ContainerImageName] = "dotnet/testimage",
7576
[ContainerImageTag] = "5 0"
7677
});
77-
78+
using var _ = d;
7879
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
7980
Assert.False(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs));
8081

@@ -85,14 +86,14 @@ public void RegexCatchesInvalidContainerTags()
8586
[DockerDaemonAvailableFact]
8687
public void CanOnlySupplyOneOfTagAndTags()
8788
{
88-
var (project, logs) = ProjectInitializer.InitProject(new () {
89+
var (project, logs, d) = ProjectInitializer.InitProject(new () {
8990
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
9091
[ContainerRegistry] = "localhost:5010",
9192
[ContainerImageName] = "dotnet/testimage",
9293
[ContainerImageTag] = "5.0",
9394
[ContainerImageTags] = "latest;oldest"
9495
});
95-
96+
using var _ = d;
9697
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
9798
Assert.False(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs));
9899

Microsoft.NET.Build.Containers.IntegrationTests/ProjectInitializer.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,17 @@ private static string CombineFiles(string propsFile, string targetsFile)
3535
return tempTargetLocation;
3636
}
3737

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

@@ -63,6 +63,8 @@ public static (Project, CapturingLogger) InitProject(Dictionary<string, string>
6363
{
6464
props[kvp.Key] = kvp.Value;
6565
}
66-
return (collection.LoadProject(_combinedTargetsLocation, props, null), logs);
66+
// derived properties, since these might be set by bonusProps
67+
props["_TargetFrameworkVersionWithoutV"] = props["TargetFrameworkVersion"].TrimStart('v');
68+
return (collection.LoadProject(_combinedTargetsLocation, props, null), logs, collection);
6769
}
6870
}

Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Xunit;
88
using Microsoft.NET.Build.Containers.IntegrationTests;
99
using Microsoft.NET.Build.Containers.UnitTests;
10+
using System.Linq;
1011

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

@@ -18,10 +19,11 @@ public class TargetsTests
1819
[DockerDaemonAvailableTheory]
1920
public void CanSetEntrypointArgsToUseAppHost(bool useAppHost, params string[] entrypointArgs)
2021
{
21-
var (project, _) = ProjectInitializer.InitProject(new()
22+
var (project, _, d) = ProjectInitializer.InitProject(new()
2223
{
2324
[UseAppHost] = useAppHost.ToString()
2425
}, projectName: $"{nameof(CanSetEntrypointArgsToUseAppHost)}_{useAppHost}_{String.Join("_", entrypointArgs)}");
26+
using var _ = d;
2527
Assert.True(project.Build(ComputeContainerConfig));
2628
var computedEntrypointArgs = project.GetItems(ContainerEntrypoint).Select(i => i.EvaluatedInclude).ToArray();
2729
foreach (var (First, Second) in entrypointArgs.Zip(computedEntrypointArgs))
@@ -38,10 +40,11 @@ public void CanSetEntrypointArgsToUseAppHost(bool useAppHost, params string[] en
3840
[DockerDaemonAvailableTheory]
3941
public void CanNormalizeInputContainerNames(string projectName, string expectedContainerImageName, bool shouldPass)
4042
{
41-
var (project, _) = ProjectInitializer.InitProject(new()
43+
var (project, _, d) = ProjectInitializer.InitProject(new()
4244
{
4345
[AssemblyName] = projectName
4446
}, projectName: $"{nameof(CanNormalizeInputContainerNames)}_{projectName}_{expectedContainerImageName}_{shouldPass}");
47+
using var _ = d;
4548
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
4649
instance.Build(new[] { ComputeContainerConfig }, null, null, out var outputs).Should().Be(shouldPass, "Build should have succeeded");
4750
Assert.Equal(expectedContainerImageName, instance.GetPropertyValue(ContainerImageName));
@@ -56,11 +59,12 @@ public void CanNormalizeInputContainerNames(string projectName, string expectedC
5659
[DockerDaemonAvailableTheory]
5760
public void CanWarnOnInvalidSDKVersions(string sdkVersion, bool isAllowed)
5861
{
59-
var (project, _) = ProjectInitializer.InitProject(new()
62+
var (project, _, d) = ProjectInitializer.InitProject(new()
6063
{
6164
["NETCoreSdkVersion"] = sdkVersion,
6265
["PublishProfile"] = "DefaultContainer"
6366
}, projectName: $"{nameof(CanWarnOnInvalidSDKVersions)}_{sdkVersion}_{isAllowed}");
67+
using var _ = d;
6468
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
6569
var derivedIsAllowed = Boolean.Parse(project.GetProperty("_IsSDKContainerAllowedVersion").EvaluatedValue);
6670
// var buildResult = instance.Build(new[]{"_ContainerVerifySDKVersion"}, null, null, out var outputs);
@@ -72,10 +76,11 @@ public void CanWarnOnInvalidSDKVersions(string sdkVersion, bool isAllowed)
7276
[DockerDaemonAvailableTheory]
7377
public void GetsConventionalLabelsByDefault(bool shouldEvaluateLabels)
7478
{
75-
var (project, _) = ProjectInitializer.InitProject(new()
79+
var (project, _, d) = ProjectInitializer.InitProject(new()
7680
{
7781
[ContainerGenerateLabels] = shouldEvaluateLabels.ToString()
7882
}, projectName: $"{nameof(GetsConventionalLabelsByDefault)}_{shouldEvaluateLabels}");
83+
using var _ = d;
7984
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
8085
instance.Build(new[] { ComputeContainerConfig }, null, null, out var outputs).Should().BeTrue("Build should have succeeded");
8186
if (shouldEvaluateLabels)
@@ -98,14 +103,15 @@ public void ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn(bool includeSour
98103
var commitHash = "abcdef";
99104
var repoUrl = "https://git.cosmere.com/shard/whimsy.git";
100105

101-
var (project, _) = ProjectInitializer.InitProject(new()
106+
var (project, logger, d) = ProjectInitializer.InitProject(new()
102107
{
103108
["PublishRepositoryUrl"] = includeSourceControl.ToString(),
104109
["PrivateRepositoryUrl"] = repoUrl,
105110
["SourceRevisionId"] = commitHash
106111
}, projectName: $"{nameof(ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn)}_{includeSourceControl}");
112+
using var _ = d;
107113
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
108-
instance.Build(new[] { ComputeContainerConfig }, null, null, out var outputs).Should().BeTrue("Build should have succeeded");
114+
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));
109115
var labels = instance.GetItems(ContainerLabel);
110116
if (includeSourceControl)
111117
{
@@ -121,32 +127,54 @@ public void ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn(bool includeSour
121127
};
122128
}
123129

124-
[InlineData("7.0.100", "7.0", "7.0")]
125-
[InlineData("7.0.100-preview.7", "7.0", "7.0")]
126-
[InlineData("7.0.100-rc.1", "7.0", "7.0")]
127-
[InlineData("8.0.100", "8.0", "8.0")]
128-
[InlineData("8.0.100", "7.0", "7.0")]
129-
[InlineData("8.0.100-preview.7", "8.0", "8.0-preview.7")]
130-
[InlineData("8.0.100-rc.1", "8.0", "8.0-rc.1")]
131-
[InlineData("8.0.100-rc.1", "7.0", "7.0")]
132-
[InlineData("8.0.200", "8.0", "8.0")]
133-
[InlineData("8.0.200", "7.0", "7.0")]
134-
[InlineData("8.0.200-preview3", "7.0", "7.0")]
135-
[InlineData("8.0.200-preview3", "8.0", "8.0")]
136-
[InlineData("6.0.100", "6.0", "6.0")]
137-
[InlineData("6.0.100-preview.1", "6.0", "6.0")]
130+
[InlineData("7.0.100", "v7.0", "7.0")]
131+
[InlineData("7.0.100-preview.7", "v7.0", "7.0")]
132+
[InlineData("7.0.100-rc.1", "v7.0", "7.0")]
133+
[InlineData("8.0.100", "v8.0", "8.0")]
134+
[InlineData("8.0.100", "v7.0", "7.0")]
135+
[InlineData("8.0.100-preview.7", "v8.0", "8.0-preview.7")]
136+
[InlineData("8.0.100-rc.1", "v8.0", "8.0-rc.1")]
137+
[InlineData("8.0.100-rc.1", "v7.0", "7.0")]
138+
[InlineData("8.0.200", "v8.0", "8.0")]
139+
[InlineData("8.0.200", "v7.0", "7.0")]
140+
[InlineData("8.0.200-preview3", "v7.0", "7.0")]
141+
[InlineData("8.0.200-preview3", "v8.0", "8.0")]
142+
[InlineData("6.0.100", "v6.0", "6.0")]
143+
[InlineData("6.0.100-preview.1", "v6.0", "6.0")]
138144
[Theory]
139145
public void CanComputeTagsForSupportedSDKVersions(string sdkVersion, string tfm, string expectedTag)
140146
{
141-
var (project, logger) = ProjectInitializer.InitProject(new()
147+
var (project, logger, d) = ProjectInitializer.InitProject(new()
142148
{
143149
["NETCoreSdkVersion"] = sdkVersion,
144-
["_TargetFrameworkVersionWithoutV"] = tfm,
150+
["TargetFrameworkVersion"] = tfm,
145151
["PublishProfile"] = "DefaultContainer"
146152
}, projectName: $"{nameof(CanComputeTagsForSupportedSDKVersions)}_{sdkVersion}_{tfm}_{expectedTag}");
153+
using var _ = d;
147154
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
148155
instance.Build(new[]{"_ComputeContainerBaseImageTag"}, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
149156
var computedTag = instance.GetProperty("_ContainerBaseImageTag").EvaluatedValue;
150157
computedTag.Should().Be(expectedTag);
151158
}
159+
160+
[InlineData("v8.0", "linux-x64", "64198")]
161+
[InlineData("v8.0", "win-x64", "ContainerUser")]
162+
[InlineData("v7.0", "linux-x64", null)]
163+
[InlineData("v7.0", "win-x64", null)]
164+
[InlineData("v9.0", "linux-x64", "64198")]
165+
[InlineData("v9.0", "win-x64", "ContainerUser")]
166+
[Theory]
167+
public void CanComputeContainerUser(string tfm, string rid, string expectedUser)
168+
{
169+
var (project, logger, d) = ProjectInitializer.InitProject(new()
170+
{
171+
["TargetFrameworkVersion"] = tfm,
172+
["ContainerRuntimeIdentifier"] = rid
173+
}, projectName: $"{nameof(CanComputeTagsForSupportedSDKVersions)}_{tfm}_{rid}_{expectedUser}");
174+
using var _ = d;
175+
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
176+
instance.Build(new[]{ComputeContainerConfig}, null, null, out var outputs).Should().BeTrue(String.Join(Environment.NewLine, logger.Errors));
177+
var computedTag = instance.GetProperty("ContainerUser")?.EvaluatedValue;
178+
computedTag.Should().Be(expectedUser);
179+
}
152180
}

docs/ContainerCustomization.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,27 @@ ContainerEntrypointArg items have one property:
207207
</ItemGroup>
208208
```
209209

210+
## ContainerUser
211+
212+
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:
213+
214+
* It can take a variety of forms - user name, linux user ids, group name, linux group id, `username:groupname`, id variants of the above
215+
* There is no verification that the user or group specified exists on the image
216+
* Changing the user can alter the behavior of the application, especially in regards to things like File System permissions
217+
218+
The default value of this field varies by project TFM and target operating system:
219+
220+
* if you are targeting .NET 8 or higher and using the Microsoft runtime images, then
221+
* on Linux the rootless user `app` will be used (though it will be referenced by its user id)
222+
* on Windows the rootless user `ContainerUser` will be used
223+
* otherwise no default `ContainerUser` will be used
224+
225+
```xml
226+
<PropertyGroup>
227+
<ContainerUser>my-existing-app-user</ContainerUser>
228+
</PropertyGroup>
229+
```
230+
210231
## Default container labels
211232

212233
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.

0 commit comments

Comments
 (0)