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

Commit

Permalink
Added support for docker build arguments (#521)
Browse files Browse the repository at this point in the history
* Added support for docker build arguments

* fixed whitespace issue? #511

* Additional tests

* Update src/Microsoft.Tye.Core/ApplicationFactory.cs

Co-authored-by: Justin Kotalik <jukotali@microsoft.com>

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

Co-authored-by: Justin Kotalik <jukotali@microsoft.com>

* Update test/E2ETest/TyeBuildTests.Dockerfile.cs

Co-authored-by: Justin Kotalik <jukotali@microsoft.com>

* 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 <ossent@yahoo.co.uk>
Co-authored-by: Justin Kotalik <jukotali@microsoft.com>
  • Loading branch information
3 people authored Jun 18, 2020
1 parent c9561a4 commit 9cbf1fd
Show file tree
Hide file tree
Showing 31 changed files with 574 additions and 4 deletions.
3 changes: 2 additions & 1 deletion src/Microsoft.Tye.Core/ApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F
Replicas = configService.Replicas ?? 1,
DockerFile = Path.Combine(source.DirectoryName, configService.DockerFile),
// Supplying an absolute path with trailing slashes fails for DockerFileContext when calling docker build, so trim trailing slash.
DockerFileContext = GetDockerFileContext(source, configService)
DockerFileContext = GetDockerFileContext(source, configService),
BuildArgs = configService.DockerFileArgs
};
service = dockerFile;

Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class ConfigService
public bool External { get; set; }
public string? Image { get; set; }
public string? DockerFile { get; set; }
public Dictionary<string, string> DockerFileArgs { get; set; } = new Dictionary<string, string>();
public string? DockerFileContext { get; set; }
public string? Project { get; set; }
public string? Include { get; set; }
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.Tye.Core/DockerFileServiceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;

namespace Microsoft.Tye
{
public class DockerFileServiceBuilder : ProjectServiceBuilder
Expand All @@ -14,6 +16,7 @@ public DockerFileServiceBuilder(string name, string image)
public string Image { get; set; }

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

public string? DockerFileContext { get; set; }
}
Expand Down
32 changes: 32 additions & 0 deletions src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Linq;
using Microsoft.Tye.ConfigModel;
using YamlDotNet.RepresentationModel;

Expand Down Expand Up @@ -46,6 +47,14 @@ private static void HandleServiceNameMapping(YamlMappingNode yamlMappingNode, Co
case "dockerFile":
service.DockerFile = YamlParser.GetScalarValue(key, child.Value);
break;
case "dockerFileArgs":
if (child.Value.NodeType != YamlNodeType.Sequence)
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key));
}

HandleDockerFileArgs((child.Value as YamlSequenceNode)!, service.DockerFileArgs);
break;
case "dockerFileContext":
service.DockerFileContext = YamlParser.GetScalarValue(key, child.Value);
break;
Expand Down Expand Up @@ -399,6 +408,14 @@ private static void HandleBuildProperties(YamlSequenceNode yamlSequenceNode, Lis
buildProperties.Add(buildProperty);
}
}
private static void HandleDockerFileArgs(YamlSequenceNode yamlSequenceNode, Dictionary<string, string> dockerArguments)
{
foreach (var child in yamlSequenceNode.Children)
{
YamlParser.ThrowIfNotYamlMapping(child);
HandleServiceDockerArgsNameMapping((YamlMappingNode)child, dockerArguments);
}
}

private static void HandleServiceConfiguration(YamlSequenceNode yamlSequenceNode, List<ConfigConfigurationSource> configuration)
{
Expand Down Expand Up @@ -459,5 +476,20 @@ private static void HandleServiceTags(YamlSequenceNode yamlSequenceNode, List<st
tags.Add(tag);
}
}
private static void HandleServiceDockerArgsNameMapping(YamlMappingNode yamlMappingNode, IDictionary<string, string> dockerArguments)
{
foreach (var child in yamlMappingNode!.Children)
{
var key = YamlParser.GetScalarValue(child.Key);
var value = YamlParser.GetScalarValue(key, child.Value);

if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value))
{
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key));
}

dockerArguments.Add(key, value);
}
}
}
}
10 changes: 9 additions & 1 deletion src/Microsoft.Tye.Hosting/DockerRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -468,9 +469,16 @@ void Log(string data)
service.Logs.OnNext(data);
}

var arguments = new StringBuilder($"build \"{docker.DockerFileContext?.FullName}\" -t {dockerImage} -f \"{docker.DockerFile}\"");

foreach (var buildArg in docker.BuildArgs)
{
arguments.Append($" --build-arg {buildArg.Key}={buildArg.Value}");
}

var dockerBuildResult = await ProcessUtil.RunAsync(
$"docker",
$"build \"{docker.DockerFileContext?.FullName}\" -t {dockerImage} -f \"{docker.DockerFile}\"",
arguments.ToString(),
outputDataReceived: Log,
errorDataReceived: Log,
workingDirectory: docker.WorkingDirectory,
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.Tye.Hosting/Model/DockerRunInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public DockerRunInfo(string image, string? args)
public List<DockerVolume> VolumeMappings { get; } = new List<DockerVolume>();

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

public string Image { get; }

Expand Down
3 changes: 2 additions & 1 deletion src/tye/ApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public static Application ToHostingApplication(this ApplicationBuilder applicati
{
var dockerRunInfo = new DockerRunInfo(dockerFile.Image, dockerFile.Args)
{
IsAspNet = dockerFile.IsAspNet
IsAspNet = dockerFile.IsAspNet,
BuildArgs = dockerFile.BuildArgs
};

if (!string.IsNullOrEmpty(dockerFile.DockerFile))
Expand Down
2 changes: 1 addition & 1 deletion test/E2ETest/Microsoft.Tye.E2ETests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
</ItemGroup>

<ItemGroup>
<None Remove="testassets\generate\dockerfile.yaml" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.6.1" />
</ItemGroup>

</Project>
124 changes: 124 additions & 0 deletions test/E2ETest/TyeBuildTests.Dockerfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,129 @@ public async Task TyeBuild_SinglePhase_ExistingDockerfile()
await DockerAssert.DeleteDockerImagesAsync(output, imageName);
}
}

[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task TyeBuild_SinglePhase_ExistingDockerfileWithBuildArgs()
{
var projectName = "single-phase-dockerfile-args";
var environment = "production";
var imageName = "test/web";

await DockerAssert.DeleteDockerImagesAsync(output, imageName);

using var projectDirectory = CopyTestProjectDirectory(projectName);
Assert.True(File.Exists(Path.Combine(projectDirectory.DirectoryPath, "Dockerfile")), "Dockerfile should exist.");

var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));

var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);

application.Registry = new ContainerRegistry("test");

try
{
await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false);

Assert.Single(application.Services.Single().Outputs.OfType<DockerImageOutput>());
var builder = (DockerFileServiceBuilder)application.Services.First();
var valuePair = builder.BuildArgs.First();
Assert.Equal("pat", valuePair.Key);
Assert.Equal("thisisapat", valuePair.Value);

await DockerAssert.AssertImageExistsAsync(output, imageName);
}
finally
{
await DockerAssert.DeleteDockerImagesAsync(output, imageName);
}
}

[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task TyeBuild_SinglePhase_ExistingDockerfileWithBuildArgsDuplicateArgs()
{
var projectName = "single-phase-dockerfile-args";
var environment = "production";
var imageName = "test/web";

await DockerAssert.DeleteDockerImagesAsync(output, imageName);

using var projectDirectory = CopyTestProjectDirectory(projectName);
Assert.True(File.Exists(Path.Combine(projectDirectory.DirectoryPath, "Dockerfile")), "Dockerfile should exist.");

var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));

var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);

application.Registry = new ContainerRegistry("test");

try
{
await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false);

Assert.Single(application.Services.Single().Outputs.OfType<DockerImageOutput>());
var builder = (DockerFileServiceBuilder)application.Services.First();
var valuePair = builder.BuildArgs.First();
Assert.Equal("pat", valuePair.Key);
Assert.Equal("thisisapat", valuePair.Value);

await DockerAssert.AssertImageExistsAsync(output, imageName);
}
finally
{
await DockerAssert.DeleteDockerImagesAsync(output, imageName);
}
}

[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task TyeBuild_SinglePhase_ExistingDockerfileWithBuildArgsMultiArgs()
{
var projectName = "single-phase-dockerfile-multi-args";
var environment = "production";
var imageName = "test/web";

await DockerAssert.DeleteDockerImagesAsync(output, imageName);

using var projectDirectory = CopyTestProjectDirectory(projectName);
Assert.True(File.Exists(Path.Combine(projectDirectory.DirectoryPath, "Dockerfile")), "Dockerfile should exist.");

var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));

var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);

application.Registry = new ContainerRegistry("test");

try
{
await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false);

Assert.Single(application.Services.Single().Outputs.OfType<DockerImageOutput>());
var builder = (DockerFileServiceBuilder)application.Services.First();
var valuePair = builder.BuildArgs.ElementAt(0);

Assert.Equal(3, builder.BuildArgs.Count);
Assert.Equal("pat", valuePair.Key);
Assert.Equal("thisisapat", valuePair.Value);

valuePair = builder.BuildArgs.ElementAt(1);
Assert.Equal("number_of_replicas", valuePair.Key);
Assert.Equal("2", valuePair.Value);

valuePair = builder.BuildArgs.ElementAt(2);
Assert.Equal("number_of_shards", valuePair.Key);
Assert.Equal("5", valuePair.Value);

await DockerAssert.AssertImageExistsAsync(output, imageName);
}
finally
{
await DockerAssert.DeleteDockerImagesAsync(output, imageName);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY . /app
ENTRYPOINT ["dotnet", "single-phase-dockerfile-args.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace single_phase_dockerfile
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:15313",
"sslPort": 44306
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"single_phase_dockerfile": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace single_phase_dockerfile
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", context =>
{
return context.Response.WriteAsync("Hello World!");
});
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>single_phase_dockerfile_args</RootNamespace>
</PropertyGroup>

</Project>
Loading

0 comments on commit 9cbf1fd

Please sign in to comment.