Skip to content
This repository was archived by the owner on Nov 20, 2023. It is now read-only.

Commit 9cbf1fd

Browse files
ossentooOscar Yahoojkotalik
authored
Added support for docker build arguments (#521)
* Added support for docker build arguments * fixed whitespace issue? #511 * Additional tests * Update src/Microsoft.Tye.Core/ApplicationFactory.cs Co-authored-by: Justin Kotalik <[email protected]> * Update src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs Co-authored-by: Justin Kotalik <[email protected]> * Update test/E2ETest/TyeBuildTests.Dockerfile.cs Co-authored-by: Justin Kotalik <[email protected]> * Moved below DockerFile to keep ordering consistent. * refactored HandleServiceDockerArgsNameMapping * Use a stringbuilder for concatenation * Removed appsettings development file * Removed launchSettings file * duplicate parameters test * Multiple build args test * small nits Co-authored-by: Oscar Yahoo <[email protected]> Co-authored-by: Justin Kotalik <[email protected]>
1 parent c9561a4 commit 9cbf1fd

31 files changed

+574
-4
lines changed

src/Microsoft.Tye.Core/ApplicationFactory.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F
133133
Replicas = configService.Replicas ?? 1,
134134
DockerFile = Path.Combine(source.DirectoryName, configService.DockerFile),
135135
// Supplying an absolute path with trailing slashes fails for DockerFileContext when calling docker build, so trim trailing slash.
136-
DockerFileContext = GetDockerFileContext(source, configService)
136+
DockerFileContext = GetDockerFileContext(source, configService),
137+
BuildArgs = configService.DockerFileArgs
137138
};
138139
service = dockerFile;
139140

src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class ConfigService
2222
public bool External { get; set; }
2323
public string? Image { get; set; }
2424
public string? DockerFile { get; set; }
25+
public Dictionary<string, string> DockerFileArgs { get; set; } = new Dictionary<string, string>();
2526
public string? DockerFileContext { get; set; }
2627
public string? Project { get; set; }
2728
public string? Include { get; set; }

src/Microsoft.Tye.Core/DockerFileServiceBuilder.cs

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Collections.Generic;
6+
57
namespace Microsoft.Tye
68
{
79
public class DockerFileServiceBuilder : ProjectServiceBuilder
@@ -14,6 +16,7 @@ public DockerFileServiceBuilder(string name, string image)
1416
public string Image { get; set; }
1517

1618
public string? DockerFile { get; set; }
19+
public Dictionary<string, string> BuildArgs { get; set; } = new Dictionary<string, string>();
1720

1821
public string? DockerFileContext { get; set; }
1922
}

src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs

+32
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Collections.Generic;
6+
using System.Linq;
67
using Microsoft.Tye.ConfigModel;
78
using YamlDotNet.RepresentationModel;
89

@@ -46,6 +47,14 @@ private static void HandleServiceNameMapping(YamlMappingNode yamlMappingNode, Co
4647
case "dockerFile":
4748
service.DockerFile = YamlParser.GetScalarValue(key, child.Value);
4849
break;
50+
case "dockerFileArgs":
51+
if (child.Value.NodeType != YamlNodeType.Sequence)
52+
{
53+
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key));
54+
}
55+
56+
HandleDockerFileArgs((child.Value as YamlSequenceNode)!, service.DockerFileArgs);
57+
break;
4958
case "dockerFileContext":
5059
service.DockerFileContext = YamlParser.GetScalarValue(key, child.Value);
5160
break;
@@ -399,6 +408,14 @@ private static void HandleBuildProperties(YamlSequenceNode yamlSequenceNode, Lis
399408
buildProperties.Add(buildProperty);
400409
}
401410
}
411+
private static void HandleDockerFileArgs(YamlSequenceNode yamlSequenceNode, Dictionary<string, string> dockerArguments)
412+
{
413+
foreach (var child in yamlSequenceNode.Children)
414+
{
415+
YamlParser.ThrowIfNotYamlMapping(child);
416+
HandleServiceDockerArgsNameMapping((YamlMappingNode)child, dockerArguments);
417+
}
418+
}
402419

403420
private static void HandleServiceConfiguration(YamlSequenceNode yamlSequenceNode, List<ConfigConfigurationSource> configuration)
404421
{
@@ -459,5 +476,20 @@ private static void HandleServiceTags(YamlSequenceNode yamlSequenceNode, List<st
459476
tags.Add(tag);
460477
}
461478
}
479+
private static void HandleServiceDockerArgsNameMapping(YamlMappingNode yamlMappingNode, IDictionary<string, string> dockerArguments)
480+
{
481+
foreach (var child in yamlMappingNode!.Children)
482+
{
483+
var key = YamlParser.GetScalarValue(child.Key);
484+
var value = YamlParser.GetScalarValue(key, child.Value);
485+
486+
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value))
487+
{
488+
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
489+
}
490+
491+
dockerArguments.Add(key, value);
492+
}
493+
}
462494
}
463495
}

src/Microsoft.Tye.Hosting/DockerRunner.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Linq;
99
using System.Net;
1010
using System.Runtime.InteropServices;
11+
using System.Text;
1112
using System.Threading;
1213
using System.Threading.Tasks;
1314
using Microsoft.Extensions.Logging;
@@ -468,9 +469,16 @@ void Log(string data)
468469
service.Logs.OnNext(data);
469470
}
470471

472+
var arguments = new StringBuilder($"build \"{docker.DockerFileContext?.FullName}\" -t {dockerImage} -f \"{docker.DockerFile}\"");
473+
474+
foreach (var buildArg in docker.BuildArgs)
475+
{
476+
arguments.Append($" --build-arg {buildArg.Key}={buildArg.Value}");
477+
}
478+
471479
var dockerBuildResult = await ProcessUtil.RunAsync(
472480
$"docker",
473-
$"build \"{docker.DockerFileContext?.FullName}\" -t {dockerImage} -f \"{docker.DockerFile}\"",
481+
arguments.ToString(),
474482
outputDataReceived: Log,
475483
errorDataReceived: Log,
476484
workingDirectory: docker.WorkingDirectory,

src/Microsoft.Tye.Hosting/Model/DockerRunInfo.cs

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public DockerRunInfo(string image, string? args)
2626
public List<DockerVolume> VolumeMappings { get; } = new List<DockerVolume>();
2727

2828
public string? Args { get; }
29+
public Dictionary<string, string> BuildArgs { get; set; } = new Dictionary<string, string>();
2930

3031
public string Image { get; }
3132

src/tye/ApplicationBuilderExtensions.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ public static Application ToHostingApplication(this ApplicationBuilder applicati
5151
{
5252
var dockerRunInfo = new DockerRunInfo(dockerFile.Image, dockerFile.Args)
5353
{
54-
IsAspNet = dockerFile.IsAspNet
54+
IsAspNet = dockerFile.IsAspNet,
55+
BuildArgs = dockerFile.BuildArgs
5556
};
5657

5758
if (!string.IsNullOrEmpty(dockerFile.DockerFile))

test/E2ETest/Microsoft.Tye.E2ETests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
</ItemGroup>
3333

3434
<ItemGroup>
35-
<None Remove="testassets\generate\dockerfile.yaml" />
35+
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.6.1" />
3636
</ItemGroup>
3737

3838
</Project>

test/E2ETest/TyeBuildTests.Dockerfile.cs

+124
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,129 @@ public async Task TyeBuild_SinglePhase_ExistingDockerfile()
8686
await DockerAssert.DeleteDockerImagesAsync(output, imageName);
8787
}
8888
}
89+
90+
[ConditionalFact]
91+
[SkipIfDockerNotRunning]
92+
public async Task TyeBuild_SinglePhase_ExistingDockerfileWithBuildArgs()
93+
{
94+
var projectName = "single-phase-dockerfile-args";
95+
var environment = "production";
96+
var imageName = "test/web";
97+
98+
await DockerAssert.DeleteDockerImagesAsync(output, imageName);
99+
100+
using var projectDirectory = CopyTestProjectDirectory(projectName);
101+
Assert.True(File.Exists(Path.Combine(projectDirectory.DirectoryPath, "Dockerfile")), "Dockerfile should exist.");
102+
103+
var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));
104+
105+
var outputContext = new OutputContext(sink, Verbosity.Debug);
106+
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
107+
108+
application.Registry = new ContainerRegistry("test");
109+
110+
try
111+
{
112+
await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false);
113+
114+
Assert.Single(application.Services.Single().Outputs.OfType<DockerImageOutput>());
115+
var builder = (DockerFileServiceBuilder)application.Services.First();
116+
var valuePair = builder.BuildArgs.First();
117+
Assert.Equal("pat", valuePair.Key);
118+
Assert.Equal("thisisapat", valuePair.Value);
119+
120+
await DockerAssert.AssertImageExistsAsync(output, imageName);
121+
}
122+
finally
123+
{
124+
await DockerAssert.DeleteDockerImagesAsync(output, imageName);
125+
}
126+
}
127+
128+
[ConditionalFact]
129+
[SkipIfDockerNotRunning]
130+
public async Task TyeBuild_SinglePhase_ExistingDockerfileWithBuildArgsDuplicateArgs()
131+
{
132+
var projectName = "single-phase-dockerfile-args";
133+
var environment = "production";
134+
var imageName = "test/web";
135+
136+
await DockerAssert.DeleteDockerImagesAsync(output, imageName);
137+
138+
using var projectDirectory = CopyTestProjectDirectory(projectName);
139+
Assert.True(File.Exists(Path.Combine(projectDirectory.DirectoryPath, "Dockerfile")), "Dockerfile should exist.");
140+
141+
var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));
142+
143+
var outputContext = new OutputContext(sink, Verbosity.Debug);
144+
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
145+
146+
application.Registry = new ContainerRegistry("test");
147+
148+
try
149+
{
150+
await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false);
151+
152+
Assert.Single(application.Services.Single().Outputs.OfType<DockerImageOutput>());
153+
var builder = (DockerFileServiceBuilder)application.Services.First();
154+
var valuePair = builder.BuildArgs.First();
155+
Assert.Equal("pat", valuePair.Key);
156+
Assert.Equal("thisisapat", valuePair.Value);
157+
158+
await DockerAssert.AssertImageExistsAsync(output, imageName);
159+
}
160+
finally
161+
{
162+
await DockerAssert.DeleteDockerImagesAsync(output, imageName);
163+
}
164+
}
165+
166+
[ConditionalFact]
167+
[SkipIfDockerNotRunning]
168+
public async Task TyeBuild_SinglePhase_ExistingDockerfileWithBuildArgsMultiArgs()
169+
{
170+
var projectName = "single-phase-dockerfile-multi-args";
171+
var environment = "production";
172+
var imageName = "test/web";
173+
174+
await DockerAssert.DeleteDockerImagesAsync(output, imageName);
175+
176+
using var projectDirectory = CopyTestProjectDirectory(projectName);
177+
Assert.True(File.Exists(Path.Combine(projectDirectory.DirectoryPath, "Dockerfile")), "Dockerfile should exist.");
178+
179+
var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));
180+
181+
var outputContext = new OutputContext(sink, Verbosity.Debug);
182+
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
183+
184+
application.Registry = new ContainerRegistry("test");
185+
186+
try
187+
{
188+
await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false);
189+
190+
Assert.Single(application.Services.Single().Outputs.OfType<DockerImageOutput>());
191+
var builder = (DockerFileServiceBuilder)application.Services.First();
192+
var valuePair = builder.BuildArgs.ElementAt(0);
193+
194+
Assert.Equal(3, builder.BuildArgs.Count);
195+
Assert.Equal("pat", valuePair.Key);
196+
Assert.Equal("thisisapat", valuePair.Value);
197+
198+
valuePair = builder.BuildArgs.ElementAt(1);
199+
Assert.Equal("number_of_replicas", valuePair.Key);
200+
Assert.Equal("2", valuePair.Value);
201+
202+
valuePair = builder.BuildArgs.ElementAt(2);
203+
Assert.Equal("number_of_shards", valuePair.Key);
204+
Assert.Equal("5", valuePair.Value);
205+
206+
await DockerAssert.AssertImageExistsAsync(output, imageName);
207+
}
208+
finally
209+
{
210+
await DockerAssert.DeleteDockerImagesAsync(output, imageName);
211+
}
212+
}
89213
}
90214
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
2+
WORKDIR /app
3+
COPY . /app
4+
ENTRYPOINT ["dotnet", "single-phase-dockerfile-args.dll"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Hosting;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.Hosting;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace single_phase_dockerfile
11+
{
12+
public class Program
13+
{
14+
public static void Main(string[] args)
15+
{
16+
CreateHostBuilder(args).Build().Run();
17+
}
18+
19+
public static IHostBuilder CreateHostBuilder(string[] args) =>
20+
Host.CreateDefaultBuilder(args)
21+
.ConfigureWebHostDefaults(webBuilder =>
22+
{
23+
webBuilder.UseStartup<Startup>();
24+
});
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "http://localhost:15313",
7+
"sslPort": 44306
8+
}
9+
},
10+
"profiles": {
11+
"IIS Express": {
12+
"commandName": "IISExpress",
13+
"launchBrowser": true,
14+
"environmentVariables": {
15+
"ASPNETCORE_ENVIRONMENT": "Development"
16+
}
17+
},
18+
"single_phase_dockerfile": {
19+
"commandName": "Project",
20+
"launchBrowser": true,
21+
"applicationUrl": "https://localhost:5001;http://localhost:5000",
22+
"environmentVariables": {
23+
"ASPNETCORE_ENVIRONMENT": "Development"
24+
}
25+
}
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Hosting;
10+
11+
namespace single_phase_dockerfile
12+
{
13+
public class Startup
14+
{
15+
// This method gets called by the runtime. Use this method to add services to the container.
16+
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
17+
public void ConfigureServices(IServiceCollection services)
18+
{
19+
}
20+
21+
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
22+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
23+
{
24+
if (env.IsDevelopment())
25+
{
26+
app.UseDeveloperExceptionPage();
27+
}
28+
29+
app.UseRouting();
30+
31+
app.UseEndpoints(endpoints =>
32+
{
33+
endpoints.MapGet("/", context =>
34+
{
35+
return context.Response.WriteAsync("Hello World!");
36+
});
37+
});
38+
}
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft": "Warning",
6+
"Microsoft.Hosting.Lifetime": "Information"
7+
}
8+
},
9+
"AllowedHosts": "*"
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
<RootNamespace>single_phase_dockerfile_args</RootNamespace>
6+
</PropertyGroup>
7+
8+
</Project>

0 commit comments

Comments
 (0)