Browse Source

Add support for running local containers (#141)

* Add support for running local containers
- This support builds the application locally then volume mounts the application into the container when the --docker flag is specified. Added support for volume mounts and working directory to the DockerRunner.
- Add simple test for tye run --docker
pull/147/head
David Fowler 6 years ago
committed by GitHub
parent
commit
fe07b638d0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      src/Microsoft.Tye.Hosting/DockerRunner.cs
  2. 6
      src/Microsoft.Tye.Hosting/Model/DockerRunInfo.cs
  3. 2
      src/Microsoft.Tye.Hosting/Model/ServiceDescription.cs
  4. 100
      src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs
  5. 18
      src/Microsoft.Tye.Hosting/TyeHost.cs
  6. 6
      src/tye/Program.RunCommand.cs
  7. 42
      test/E2ETest/TyeRunTests.cs

12
src/Microsoft.Tye.Hosting/DockerRunner.cs

@ -68,6 +68,8 @@ namespace Microsoft.Tye.Hosting
var serviceDescription = service.Description;
var environmentArguments = "";
var volumes = "";
var workingDirectory = docker.WorkingDirectory != null ? $"-w {docker.WorkingDirectory}" : "";
var dockerInfo = new DockerInformation(new Task[service.Description.Replicas]);
@ -95,6 +97,9 @@ namespace Microsoft.Tye.Hosting
{
status.Ports = ports.Select(p => p.Port);
// These ports should also be passed in not assuming ASP.NET Core
environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://*:{p.Port}"));
portString = string.Join(" ", ports.Select(p => $"-p {p.Port}:{p.InternalPort ?? p.Port}"));
foreach (var p in ports)
@ -112,7 +117,12 @@ namespace Microsoft.Tye.Hosting
environmentArguments += $"-e {pair.Key}={pair.Value} ";
}
var command = $"run -d {environmentArguments} {portString} --name {replica} --restart=unless-stopped {docker.Image} {docker.Args ?? ""}";
foreach (var pair in docker.VolumeMappings)
{
volumes += $"-v {pair.Key}:{pair.Value} ";
}
var command = $"run -d {workingDirectory} {volumes} {environmentArguments} {portString} --name {replica} --restart=unless-stopped {docker.Image} {docker.Args ?? ""}";
_logger.LogInformation("Running docker command {Command}", command);
service.Logs.OnNext($"[{replica}]: {command}");

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

@ -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.Hosting.Model
{
public class DockerRunInfo : RunInfo
@ -12,6 +14,10 @@ namespace Microsoft.Tye.Hosting.Model
Args = args;
}
public string? WorkingDirectory { get; set; }
public Dictionary<string, string> VolumeMappings { get; } = new Dictionary<string, string>();
public string? Args { get; }
public string Image { get; }

2
src/Microsoft.Tye.Hosting/Model/ServiceDescription.cs

@ -16,7 +16,7 @@ namespace Microsoft.Tye.Hosting.Model
public string Name { get; }
public RunInfo? RunInfo { get; }
public RunInfo? RunInfo { get; set; }
public int Replicas { get; set; } = 1;
public List<ServiceBinding> Bindings { get; } = new List<ServiceBinding>();

100
src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs

@ -0,0 +1,100 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Tye.Hosting.Model;
namespace Microsoft.Tye.Hosting
{
public class TransformProjectsIntoContainers : IApplicationProcessor
{
private readonly ILogger _logger;
public TransformProjectsIntoContainers(ILogger logger)
{
_logger = logger;
}
public Task StartAsync(Model.Application application)
{
// This transforms a ProjectRunInfo into
var tasks = new List<Task>();
foreach (var s in application.Services.Values)
{
if (s.Description.RunInfo is ProjectRunInfo project)
{
tasks.Add(TransformProjectToContainer(application, s, project));
}
}
return Task.WhenAll(tasks);
}
private async Task TransformProjectToContainer(Model.Application application, Model.Service service, ProjectRunInfo project)
{
var serviceDescription = service.Description;
var serviceName = serviceDescription.Name;
var expandedProject = Environment.ExpandEnvironmentVariables(project.Project);
var fullProjectPath = Path.GetFullPath(Path.Combine(application.ContextDirectory, expandedProject));
service.Status.ProjectFilePath = fullProjectPath;
// Sometimes building can fail because of file locking (like files being open in VS)
_logger.LogInformation("Building project {ProjectFile}", service.Status.ProjectFilePath);
service.Logs.OnNext($"dotnet build \"{service.Status.ProjectFilePath}\" /nologo");
var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" /nologo",
outputDataReceived: data => service.Logs.OnNext(data),
throwOnError: false);
if (buildResult.ExitCode != 0)
{
_logger.LogInformation("Building {ProjectFile} failed with exit code {ExitCode}: " + buildResult.StandardOutput + buildResult.StandardError, service.Status.ProjectFilePath, buildResult.ExitCode);
return;
}
var targetFramework = GetTargetFramework(service.Status.ProjectFilePath);
// We transform the project information into the following docker command:
// docker run -w /app -v {projectDir}:/app -it {image} dotnet /app/bin/Debug/{tfm}/{outputfile}.dll
var containerImage = DetermineContainerImage(targetFramework);
var outputFileName = Path.GetFileNameWithoutExtension(service.Status.ProjectFilePath) + ".dll";
var dockerRunInfo = new DockerRunInfo(containerImage, $"dotnet /app/bin/Debug/{targetFramework}/{outputFileName} {project.Args}")
{
WorkingDirectory = "/app"
};
dockerRunInfo.VolumeMappings[Path.GetDirectoryName(service.Status.ProjectFilePath)!] = "/app";
// Change the project into a container info
serviceDescription.RunInfo = dockerRunInfo;
}
private static string DetermineContainerImage(string targetFramework)
{
// TODO: Determine the base iamge from the tfm
return "mcr.microsoft.com/dotnet/core/sdk:3.1-buster";
}
private static string GetTargetFramework(string? projectFilePath)
{
// TODO: Use msbuild to get the target path
var debugOutputPath = Path.Combine(Path.GetDirectoryName(projectFilePath)!, "bin", "Debug");
var tfms = Directory.Exists(debugOutputPath) ? Directory.GetDirectories(debugOutputPath) : Array.Empty<string>();
return tfms.Select(tfm => new DirectoryInfo(tfm).Name).FirstOrDefault() ?? "netcoreapp3.1";
}
public Task StopAsync(Model.Application application)
{
return Task.CompletedTask;
}
}
}

18
src/Microsoft.Tye.Hosting/TyeHost.cs

@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
@ -14,12 +15,11 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Tye.Hosting.Diagnostics;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Serilog.Filters;
using Microsoft.Tye.Hosting.Diagnostics;
using Microsoft.Tye.Hosting.Model;
namespace Microsoft.Tye.Hosting
{
@ -246,13 +246,21 @@ namespace Microsoft.Tye.Hosting
// Print out what providers were selected and their values
diagnosticOptions.DumpDiagnostics(logger);
var processor = new AggregateApplicationProcessor(new IApplicationProcessor[] {
var processors = new List<IApplicationProcessor>
{
new EventPipeDiagnosticsRunner(logger, diagnosticsCollector),
new ProxyService(logger),
new DockerRunner(logger),
new ProcessRunner(logger, ProcessRunnerOptions.FromArgs(args)),
});
return processor;
};
// If the docker command is specified then transport the ProjectRunInfo into DockerRunInfo
if (args.Contains("--docker"))
{
processors.Insert(0, new TransformProjectsIntoContainers(logger));
}
return new AggregateApplicationProcessor(processors);
}
public void Dispose()

6
src/tye/Program.RunCommand.cs

@ -55,6 +55,12 @@ namespace Microsoft.Tye
Required = false
});
command.AddOption(new Option("--docker")
{
Description = "Run projects as docker containers.",
Required = false
});
command.Handler = CommandHandler.Create<IConsole, FileInfo>(async (console, path) =>
{
// Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654

42
test/E2ETest/TyeRunTests.cs

@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
@ -11,6 +12,7 @@ using System.Threading.Tasks;
using Microsoft.Tye;
using Microsoft.Tye.ConfigModel;
using Microsoft.Tye.Hosting;
using Microsoft.Tye.Hosting.Model;
using Xunit;
using Xunit.Abstractions;
@ -131,6 +133,46 @@ namespace E2ETest
}
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task FrontendBackendRunTestWithDocker()
{
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "frontend-backend"));
using var tempDirectory = TempDirectory.Create();
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), new[] { "--docker" })
{
Sink = sink,
};
await host.StartAsync();
try
{
// Make sure we're runningn containers
Assert.True(host.Application.Services.All(s => s.Value.Description.RunInfo is DockerRunInfo));
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = false
};
var client = new HttpClient(new RetryHandler(handler));
var dashboardUri = new Uri(host.DashboardWebApplication!.Addresses.First());
var dashboardResponse = await client.GetStringAsync(dashboardUri);
await CheckServiceIsUp(host.Application, client, "backend", dashboardUri);
await CheckServiceIsUp(host.Application, client, "frontend", dashboardUri);
}
finally
{
await host.StopAsync();
}
}
private async Task CheckServiceIsUp(Microsoft.Tye.Hosting.Model.Application application, HttpClient client, string serviceName, Uri dashboardUri)
{
// make sure backend is up before frontend

Loading…
Cancel
Save