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

Commit

Permalink
Fixes for docker scenarios (#444)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidfowl authored May 7, 2020
1 parent af82001 commit 7395203
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 62 deletions.
2 changes: 2 additions & 0 deletions src/Microsoft.Tye.Core/ContainerServiceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public ContainerServiceBuilder(string name, string image)

public string Image { get; set; }

public bool IsAspNet { get; set; }

public string? Args { get; set; }

public string? DockerFile { get; set; }
Expand Down
68 changes: 17 additions & 51 deletions src/Microsoft.Tye.Hosting/DockerRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,6 @@ private async Task StartContainerAsync(Application application, Service service,
hostname = addresses[0].ToString();
}

// This is .NET specific
var userSecretStore = GetUserSecretsPathFromSecrets();

if (!string.IsNullOrEmpty(userSecretStore))
{
// Map the user secrets on this drive to user secrets
docker.VolumeMappings.Add(new DockerVolume(source: userSecretStore, name: null, target: "/root/.microsoft/usersecrets:ro"));
}

var dockerInfo = new DockerInformation(new Task[service.Description.Replicas]);

async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? ContainerPort, string? Protocol)> ports)
Expand All @@ -250,15 +241,7 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont

service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status));

var environment = new Dictionary<string, string>
{
// Default to development environment
["DOTNET_ENVIRONMENT"] = "Development",
["ASPNETCORE_ENVIRONMENT"] = "Development",
// Remove the color codes from the console output
["DOTNET_LOGGING__CONSOLE__DISABLECOLORS"] = "true",
["ASPNETCORE_LOGGING__CONSOLE__DISABLECOLORS"] = "true"
};
var environment = new Dictionary<string, string>();

var portString = "";

Expand All @@ -271,16 +254,19 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont
// 1. Tell the docker container what port to bind to
portString = docker.Private ? "" : string.Join(" ", ports.Select(p => $"-p {p.Port}:{p.ContainerPort ?? p.Port}"));

// 2. Configure ASP.NET Core to bind to those same ports
environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://*:{p.ContainerPort ?? p.Port}"));

// Set the HTTPS port for the redirect middleware
foreach (var p in ports)
if (docker.IsAspNet)
{
if (string.Equals(p.Protocol, "https", StringComparison.OrdinalIgnoreCase))
// 2. Configure ASP.NET Core to bind to those same ports
environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://*:{p.ContainerPort ?? p.Port}"));

// Set the HTTPS port for the redirect middleware
foreach (var p in ports)
{
// We need to set the redirect URL to the exposed port so the redirect works cleanly
environment["HTTPS_PORT"] = p.ExternalPort.ToString();
if (string.Equals(p.Protocol, "https", StringComparison.OrdinalIgnoreCase))
{
// We need to set the redirect URL to the exposed port so the redirect works cleanly
environment["HTTPS_PORT"] = p.ExternalPort.ToString();
}
}
}

Expand Down Expand Up @@ -385,6 +371,8 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont

_logger.LogInformation("Collecting docker logs for {ContainerName}.", replica);

var backOff = TimeSpan.FromSeconds(5);

while (!dockerInfo.StoppingTokenSource.Token.IsCancellationRequested)
{
var logsRes = await ProcessUtil.RunAsync("docker", $"logs -f {containerId}",
Expand All @@ -403,13 +391,15 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont
try
{
// Avoid spamming logs if restarts are happening
await Task.Delay(5000, dockerInfo.StoppingTokenSource.Token);
await Task.Delay(backOff, dockerInfo.StoppingTokenSource.Token);
}
catch (OperationCanceledException)
{
break;
}
}

backOff *= 2;
}

_logger.LogInformation("docker logs collection for {ContainerName} complete with exit code {ExitCode}", replica, result.ExitCode);
Expand Down Expand Up @@ -528,30 +518,6 @@ private Task StopContainerAsync(Service service)
return Task.CompletedTask;
}

private static string? GetUserSecretsPathFromSecrets()
{
// This is the logic used to determine the user secrets path
// See https://github.com/dotnet/extensions/blob/64140f90157fec1bfd8aeafdffe8f30308ccdf41/src/Configuration/Config.UserSecrets/src/PathHelper.cs#L27
const string userSecretsFallbackDir = "DOTNET_USER_SECRETS_FALLBACK_DIR";

// For backwards compat, this checks env vars first before using Env.GetFolderPath
var appData = Environment.GetEnvironmentVariable("APPDATA");
var root = appData // On Windows it goes to %APPDATA%\Microsoft\UserSecrets\
?? Environment.GetEnvironmentVariable("HOME") // On Mac/Linux it goes to ~/.microsoft/usersecrets/
?? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
?? Environment.GetEnvironmentVariable(userSecretsFallbackDir); // this fallback is an escape hatch if everything else fails

if (string.IsNullOrEmpty(root))
{
return null;
}

return !string.IsNullOrEmpty(appData)
? Path.Combine(root, "Microsoft", "UserSecrets")
: Path.Combine(root, ".microsoft", "usersecrets");
}

private class DockerInformation
{
public DockerInformation(Task[] tasks)
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.Tye.Hosting/Model/DockerRunInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// 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;

Expand All @@ -16,6 +17,7 @@ public DockerRunInfo(string image, string? args)
}

public bool Private { get; set; }
public bool IsAspNet { get; set; }

public string? NetworkAlias { get; set; }

Expand Down
6 changes: 6 additions & 0 deletions src/Microsoft.Tye.Hosting/Model/EnvironmentVariable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ public EnvironmentVariable(string name)
Name = name;
}

public EnvironmentVariable(string name, string? value)
{
Name = name;
Value = value;
}

public string Name { get; }
public string? Value { get; set; }

Expand Down
3 changes: 2 additions & 1 deletion src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public ProjectRunInfo(ProjectServiceBuilder project)
RunCommand = project.RunCommand;
RunArguments = project.RunArguments;
PublishOutputPath = project.PublishDir;
IsAspNet = project.IsAspNet;
}

public Dictionary<string, string> BuildProperties { get; } = new Dictionary<string, string>();
Expand All @@ -34,7 +35,7 @@ public ProjectRunInfo(ProjectServiceBuilder project)
public string TargetFrameworkName { get; set; } = default!;
public string TargetFrameworkVersion { get; set; } = default!;
public string TargetFramework { get; }

public bool IsAspNet { get; }
public string Version { get; }

public string AssemblyName { get; }
Expand Down
9 changes: 8 additions & 1 deletion src/Microsoft.Tye.Hosting/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string?
environment["PORT"] = string.Join(";", ports.Select(p => $"{p.Port}"));
}

var backOff = TimeSpan.FromSeconds(5);

while (!processInfo.StoppedTokenSource.IsCancellationRequested)
{
var replica = serviceName + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower();
Expand Down Expand Up @@ -280,6 +282,9 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string?
_logger.LogInformation("{ServiceName} running on process id {PID}", replica, pid);
}

// Reset the backoff
backOff = TimeSpan.FromSeconds(5);

status.Pid = pid;

WriteReplicaToStore(pid.ToString());
Expand All @@ -301,14 +306,16 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string?

try
{
await Task.Delay(5000, processInfo.StoppedTokenSource.Token);
await Task.Delay(backOff, processInfo.StoppedTokenSource.Token);
}
catch (OperationCanceledException)
{
// Swallow cancellation exceptions and continue
}
}

backOff *= 2;

service.Restarts++;

if (status.ExitCode != null)
Expand Down
78 changes: 74 additions & 4 deletions src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@
// 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
{
using System.Linq;

public class TransformProjectsIntoContainers : IApplicationProcessor
{
private readonly ILogger _logger;
private Lazy<TempDirectory> _certificateDirectory;

public TransformProjectsIntoContainers(ILogger logger)
{
_logger = logger;
_certificateDirectory = new Lazy<TempDirectory>(() => TempDirectory.Create());
}

public Task StartAsync(Application application)
Expand Down Expand Up @@ -72,26 +75,93 @@ private async Task TransformProjectToContainer(Service service, ProjectRunInfo p
var outputFileName = project.AssemblyName + ".dll";
var dockerRunInfo = new DockerRunInfo(containerImage, $"dotnet {outputFileName} {project.Args}")
{
WorkingDirectory = "/app"
WorkingDirectory = "/app",
IsAspNet = project.IsAspNet
};

dockerRunInfo.VolumeMappings.Add(new DockerVolume(source: project.PublishOutputPath, name: null, target: "/app"));

// Make volume mapping works when running as a container
dockerRunInfo.VolumeMappings.AddRange(project.VolumeMappings);

// This is .NET specific
var userSecretStore = GetUserSecretsPathFromSecrets();

if (!string.IsNullOrEmpty(userSecretStore))
{
// Map the user secrets on this drive to user secrets
dockerRunInfo.VolumeMappings.Add(new DockerVolume(source: userSecretStore, name: null, target: "/root/.microsoft/usersecrets:ro"));
}

// Default to development environment
serviceDescription.Configuration.Add(new EnvironmentVariable("DOTNET_ENVIRONMENT", "Development"));

// Remove the color codes from the console output
serviceDescription.Configuration.Add(new EnvironmentVariable("DOTNET_LOGGING__CONSOLE__DISABLECOLORS", "true"));

if (project.IsAspNet)
{
serviceDescription.Configuration.Add(new EnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"));
serviceDescription.Configuration.Add(new EnvironmentVariable("ASPNETCORE_LOGGING__CONSOLE__DISABLECOLORS", "true"));
}

// If we have an https binding then export the dev cert and mount the volume into the container
if (serviceDescription.Bindings.Any(b => string.Equals(b.Protocol, "https", StringComparison.OrdinalIgnoreCase)))
{
// We export the developer certificate from this machine
var certPassword = Guid.NewGuid().ToString();
var certificateDirectory = _certificateDirectory.Value;
var certificateFilePath = Path.Combine(certificateDirectory.DirectoryPath, project.AssemblyName + ".pfx");
await ProcessUtil.RunAsync("dotnet", $"dev-certs https -ep {certificateFilePath} -p {certPassword}");
serviceDescription.Configuration.Add(new EnvironmentVariable("Kestrel__Certificates__Development__Password", certPassword));

// Certificate Path: https://github.com/dotnet/aspnetcore/blob/a9d702624a02ad4ebf593d9bf9c1c69f5702a6f5/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs#L419
dockerRunInfo.VolumeMappings.Add(new DockerVolume(source: certificateDirectory.DirectoryPath, name: null, target: "/root/.aspnet/https:ro"));
}

// Change the project into a container info
serviceDescription.RunInfo = dockerRunInfo;
}

private static string DetermineContainerImage(ProjectRunInfo project)
{
return $"mcr.microsoft.com/dotnet/core/sdk:{project.TargetFrameworkVersion}";
var baseImage = project.IsAspNet ? "mcr.microsoft.com/dotnet/core/aspnet" : "mcr.microsoft.com/dotnet/core/runtime";

return $"{baseImage}:{project.TargetFrameworkVersion}";
}

public Task StopAsync(Application application)
{
if (_certificateDirectory.IsValueCreated)
{
_certificateDirectory.Value.Dispose();
}

return Task.CompletedTask;
}

private static string? GetUserSecretsPathFromSecrets()
{
// This is the logic used to determine the user secrets path
// See https://github.com/dotnet/extensions/blob/64140f90157fec1bfd8aeafdffe8f30308ccdf41/src/Configuration/Config.UserSecrets/src/PathHelper.cs#L27
const string userSecretsFallbackDir = "DOTNET_USER_SECRETS_FALLBACK_DIR";

// For backwards compat, this checks env vars first before using Env.GetFolderPath
var appData = Environment.GetEnvironmentVariable("APPDATA");
var root = appData // On Windows it goes to %APPDATA%\Microsoft\UserSecrets\
?? Environment.GetEnvironmentVariable("HOME") // On Mac/Linux it goes to ~/.microsoft/usersecrets/
?? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
?? Environment.GetEnvironmentVariable(userSecretsFallbackDir); // this fallback is an escape hatch if everything else fails

if (string.IsNullOrEmpty(root))
{
return null;
}

return !string.IsNullOrEmpty(appData)
? Path.Combine(root, "Microsoft", "UserSecrets")
: Path.Combine(root, ".microsoft", "usersecrets");
}
}
}
5 changes: 4 additions & 1 deletion src/tye/ApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ public static Application ToHostingApplication(this ApplicationBuilder applicati
}
else if (service is ContainerServiceBuilder container)
{
var dockerRunInfo = new DockerRunInfo(container.Image, container.Args);
var dockerRunInfo = new DockerRunInfo(container.Image, container.Args)
{
IsAspNet = container.IsAspNet
};

if (!string.IsNullOrEmpty(container.DockerFile))
{
Expand Down
15 changes: 11 additions & 4 deletions test/E2ETest/TyeRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,13 @@ public async Task FrontendProjectBackendDocker()
application.Services.Remove(project);

var outputFileName = project.AssemblyName + ".dll";
var container = new ContainerServiceBuilder(project.Name, $"mcr.microsoft.com/dotnet/core/sdk:{project.TargetFrameworkVersion}");
var container = new ContainerServiceBuilder(project.Name, $"mcr.microsoft.com/dotnet/core/aspnet:{project.TargetFrameworkVersion}")
{
IsAspNet = true
};
container.Volumes.Add(new VolumeBuilder(project.PublishDir, name: null, target: "/app"));
container.Args = $"dotnet /app/{outputFileName} {project.Args}";
container.Bindings.AddRange(project.Bindings);
container.Bindings.AddRange(project.Bindings.Where(b => b.Protocol != "https"));

await ProcessUtil.RunAsync("dotnet", $"publish \"{project.ProjectFile.FullName}\" /nologo", outputDataReceived: _sink.WriteLine, errorDataReceived: _sink.WriteLine);
application.Services.Add(container);
Expand Down Expand Up @@ -209,11 +212,15 @@ public async Task FrontendDockerBackendProject()
application.Services.Remove(project);

var outputFileName = project.AssemblyName + ".dll";
var container = new ContainerServiceBuilder(project.Name, $"mcr.microsoft.com/dotnet/core/sdk:{project.TargetFrameworkVersion}");
var container = new ContainerServiceBuilder(project.Name, $"mcr.microsoft.com/dotnet/core/aspnet:{project.TargetFrameworkVersion}")
{
IsAspNet = true
};
container.Dependencies.UnionWith(project.Dependencies);
container.Volumes.Add(new VolumeBuilder(project.PublishDir, name: null, target: "/app"));
container.Args = $"dotnet /app/{outputFileName} {project.Args}";
container.Bindings.AddRange(project.Bindings);
// We're not setting up the dev cert here
container.Bindings.AddRange(project.Bindings.Where(b => b.Protocol != "https"));

await ProcessUtil.RunAsync("dotnet", $"publish \"{project.ProjectFile.FullName}\" /nologo", outputDataReceived: _sink.WriteLine, errorDataReceived: _sink.WriteLine);
application.Services.Add(container);
Expand Down

0 comments on commit 7395203

Please sign in to comment.