This commit is contained in:
Tom Deseyn 2021-04-29 18:54:53 +02:00 коммит произвёл GitHub
Родитель c09165cb0d
Коммит c3f3c6cb91
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
27 изменённых файлов: 328 добавлений и 171 удалений

Просмотреть файл

@ -9,10 +9,11 @@ namespace Microsoft.Tye
{
public sealed class ApplicationBuilder
{
public ApplicationBuilder(FileInfo source, string name)
public ApplicationBuilder(FileInfo source, string name, ContainerEngine containerEngine)
{
Source = source;
Name = name;
ContainerEngine = containerEngine;
}
public FileInfo Source { get; set; }
@ -23,6 +24,8 @@ namespace Microsoft.Tye
public ContainerRegistry? Registry { get; set; }
public ContainerEngine ContainerEngine { get; set; }
public List<ExtensionConfiguration> Extensions { get; } = new List<ExtensionConfiguration>();
public List<ServiceBuilder> Services { get; } = new List<ServiceBuilder>();

Просмотреть файл

@ -30,7 +30,7 @@ namespace Microsoft.Tye
var rootConfig = ConfigFactory.FromFile(source);
rootConfig.Validate();
var root = new ApplicationBuilder(source, rootConfig.Name!)
var root = new ApplicationBuilder(source, rootConfig.Name!, new ContainerEngine(rootConfig.ContainerEngineType))
{
Namespace = rootConfig.Namespace
};

Просмотреть файл

@ -25,14 +25,9 @@ namespace Microsoft.Tye
return;
}
if (!await DockerDetector.Instance.IsDockerInstalled.Value)
if (!application.ContainerEngine.IsUsable(out string? unusableReason))
{
throw new CommandException($"Cannot generate a docker image for '{service.Name}' because docker is not installed.");
}
if (!await DockerDetector.Instance.IsDockerConnectedToDaemon.Value)
{
throw new CommandException($"Cannot generate a docker image for '{service.Name}' because docker is not running.");
throw new CommandException($"Cannot generate a docker image for '{service.Name}' because {unusableReason}.");
}
if (project is DotnetProjectServiceBuilder dotnetProject)

Просмотреть файл

@ -28,6 +28,8 @@ namespace Microsoft.Tye.ConfigModel
public string? Registry { get; set; }
public ContainerEngineType? ContainerEngineType { get; set; }
public string? Network { get; set; }
public List<Dictionary<string, object>> Extensions { get; set; } = new List<Dictionary<string, object>>();

Просмотреть файл

@ -0,0 +1,12 @@
// 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.
namespace Microsoft.Tye.ConfigModel
{
public enum ContainerEngineType
{
Docker,
Podman
}
}

Просмотреть файл

@ -0,0 +1,181 @@
// 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.Net;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Tye.ConfigModel;
namespace Microsoft.Tye
{
public class ContainerEngine
{
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10);
// Used by tests:
public static ContainerEngine? s_default;
public static ContainerEngine Default
=> (s_default ??= new ContainerEngine(default));
private bool _isUsable { get; }
private string? _unusableReason;
private bool _isPodman;
private string? _containerHost;
private string _aspnetUrlsHost;
public string AspNetUrlsHost => _aspnetUrlsHost;
public string? ContainerHost => _containerHost;
public Task<int> ExecuteAsync(
string args,
string? workingDir = null,
Action<string>? stdOut = null,
Action<string>? stdErr = null,
params (string key, string value)[] environmentVariables)
=> ProcessUtil.ExecuteAsync(CommandName, args, workingDir, stdOut, stdErr, environmentVariables);
public Task<ProcessResult> RunAsync(
string arguments,
string? workingDirectory = null,
bool throwOnError = true,
IDictionary<string, string>? environmentVariables = null,
Action<string>? outputDataReceived = null,
Action<string>? errorDataReceived = null,
Action<int>? onStart = null,
Action<int>? onStop = null,
CancellationToken cancellationToken = default)
=> ProcessUtil.RunAsync(CommandName, arguments, workingDirectory, throwOnError, environmentVariables,
outputDataReceived, errorDataReceived, onStart, onStop, cancellationToken);
private string CommandName
{
get
{
if (!_isUsable)
{
throw new InvalidOperationException($"Container engine is not usable: {_unusableReason}");
}
return _isPodman ? "podman" : "docker";
}
}
public bool IsUsable(out string? unusableReason)
{
unusableReason = _unusableReason;
return _isUsable;
}
public ContainerEngine(ContainerEngineType? containerEngine)
{
_isUsable = true;
_aspnetUrlsHost = "localhost";
if ((!containerEngine.HasValue || containerEngine == ContainerEngineType.Podman) &&
TryUsePodman(ref _unusableReason, ref _containerHost))
{
_isPodman = true;
return;
}
if ((!containerEngine.HasValue || containerEngine == ContainerEngineType.Docker) &&
TryUseDocker(ref _unusableReason, ref _containerHost, ref _aspnetUrlsHost))
{
return;
}
_isUsable = false;
_unusableReason = "container engine is not installed.";
}
private static bool TryUsePodman(ref string? unusableReason, ref string? containerHost)
{
ProcessResult result;
try
{
result = ProcessUtil.RunAsync("podman", "version -f \"{{ .Client.Version }}\"", throwOnError: false, cancellationToken: new CancellationTokenSource(Timeout).Token).Result;
}
catch
{
return false;
}
if (result.ExitCode != 0)
{
unusableReason = $"podman version exited with {result.ExitCode}. Standard error: \"{result.StandardError}\".";
return true;
}
Version minVersion = new Version(3, 1);
if (Version.TryParse(result.StandardOutput, out Version? version) &&
version < minVersion)
{
unusableReason = $"podman version '{result.StandardOutput}' is less than the required '{minVersion}'.";
return true;
}
// Check if podman is configured to allow containers to access host services.
bool hostLoopbackEnabled = false;
string containersConfPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.DoNotVerify),
"containers/containers.conf");
string[] containersConf = File.Exists(containersConfPath) ? File.ReadAllLines(containersConfPath) : Array.Empty<string>();
// Poor man's TOML parsing.
foreach (var line in containersConf)
{
string trimmed = line.Replace(" ", "");
if (trimmed.StartsWith("network_cmd_options=", StringComparison.InvariantCultureIgnoreCase) &&
trimmed.Contains("\"allow_host_loopback=true\""))
{
hostLoopbackEnabled = true;
break;
}
}
if (hostLoopbackEnabled)
{
containerHost = "10.0.2.2";
}
return true;
}
private static bool TryUseDocker(ref string? unusableReason, ref string? containerHost, ref string aspnetUrlsHost)
{
ProcessResult result;
try
{
result = ProcessUtil.RunAsync("docker", "version", throwOnError: false, cancellationToken: new CancellationTokenSource(Timeout).Token).Result;
}
catch
{
return false;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// See: https://github.com/docker/for-linux/issues/264
//
// host.docker.internal is making it's way into linux docker but doesn't work yet
// instead we use the machine IP
var addresses = Dns.GetHostAddresses(Dns.GetHostName());
containerHost = addresses[0].ToString();
// We need to bind to all interfaces on linux since the container -> host communication won't work
// if we use the IP address to reach out of the host. This works fine on osx and windows
// but doesn't work on linux.
aspnetUrlsHost = "*";
}
else
{
containerHost = "host.docker.internal";
}
if (result.ExitCode != 0)
{
unusableReason = "docker is not connected.";
}
return true;
}
}
}

Просмотреть файл

@ -46,7 +46,7 @@ namespace Microsoft.Tye
output.WriteDebugLine($"Running 'kubectl apply' in ${ns}");
output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
var exitCode = await ProcessUtil.ExecuteAsync(
$"kubectl",
$"apply -f \"{tempFile.FilePath}\"",
System.Environment.CurrentDirectory,
@ -81,7 +81,7 @@ namespace Microsoft.Tye
var retries = 0;
while (!done && retries < 60)
{
var ingressExitCode = await Process.ExecuteAsync(
var ingressExitCode = await ProcessUtil.ExecuteAsync(
"kubectl",
$"get ingress {ingress.Name} -o jsonpath='{{..ip}}'",
Environment.CurrentDirectory,

Просмотреть файл

@ -43,8 +43,7 @@ namespace Microsoft.Tye
output.WriteDebugLine("Running 'docker build'.");
output.WriteCommandLine("docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
$"docker",
var exitCode = await application.ContainerEngine.ExecuteAsync(
$"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"",
new FileInfo(containerService.DockerFile).DirectoryName,
stdOut: capture.StdOut,
@ -148,8 +147,7 @@ namespace Microsoft.Tye
output.WriteDebugLine("Running 'docker build'.");
output.WriteCommandLine("docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
$"docker",
var exitCode = await application.ContainerEngine.ExecuteAsync(
$"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"",
project.ProjectFile.DirectoryName,
stdOut: capture.StdOut,

Просмотреть файл

@ -1,55 +0,0 @@
// 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.Threading;
using System.Threading.Tasks;
namespace Microsoft.Tye
{
public class DockerDetector
{
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10);
public static DockerDetector Instance { get; } = new DockerDetector();
private DockerDetector()
{
IsDockerInstalled = new Lazy<Task<bool>>(DetectDockerInstalled);
IsDockerConnectedToDaemon = new Lazy<Task<bool>>(DetectDockerConnectedToDaemon);
}
public Lazy<Task<bool>> IsDockerInstalled { get; }
public Lazy<Task<bool>> IsDockerConnectedToDaemon { get; }
private async Task<bool> DetectDockerInstalled()
{
try
{
await ProcessUtil.RunAsync("docker", "version", throwOnError: false, cancellationToken: new CancellationTokenSource(Timeout).Token);
return true;
}
catch (Exception)
{
// Unfortunately, process throws
return false;
}
}
private async Task<bool> DetectDockerConnectedToDaemon()
{
try
{
var result = await ProcessUtil.RunAsync("docker", "version", throwOnError: false, cancellationToken: new CancellationTokenSource(Timeout).Token);
return result.ExitCode == 0;
}
catch (Exception)
{
// Unfortunately, process throws
return false;
}
}
}
}

Просмотреть файл

@ -10,7 +10,7 @@ namespace Microsoft.Tye
{
internal static class DockerPush
{
public static async Task ExecuteAsync(OutputContext output, string imageName, string imageTag)
public static async Task ExecuteAsync(OutputContext output, ContainerEngine containerEngine, string imageName, string imageTag)
{
if (output is null)
{
@ -30,8 +30,7 @@ namespace Microsoft.Tye
output.WriteDebugLine("Running 'docker push'.");
output.WriteCommandLine("docker", $"push {imageName}:{imageTag}");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
$"docker",
var exitCode = await containerEngine.ExecuteAsync(
$"push {imageName}:{imageTag}",
stdOut: capture.StdOut,
stdErr: capture.StdErr);

Просмотреть файл

@ -23,6 +23,17 @@ namespace Microsoft.Tye
private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public static Task<int> ExecuteAsync(
string command,
string args,
string? workingDir = null,
Action<string>? stdOut = null,
Action<string>? stdErr = null,
params (string key, string value)[] environmentVariables)
{
return System.CommandLine.Invocation.Process.ExecuteAsync(command, args, workingDir, stdOut, stdErr, environmentVariables);
}
public static async Task<ProcessResult> RunAsync(
string filename,
string arguments,
@ -109,7 +120,7 @@ namespace Microsoft.Tye
if (throwOnError && process.ExitCode != 0)
{
processLifetimeTask.TrySetException(new InvalidOperationException($"Command {filename} {arguments} returned exit code {process.ExitCode}"));
processLifetimeTask.TrySetException(new InvalidOperationException($"Command {filename} {arguments} returned exit code {process.ExitCode}. Standard error: \"{errorBuilder.ToString()}\""));
}
else
{

Просмотреть файл

@ -27,7 +27,7 @@ namespace Microsoft.Tye
foreach (var image in service.Outputs.OfType<DockerImageOutput>())
{
await DockerPush.ExecuteAsync(output, image.ImageName, image.ImageTag);
await DockerPush.ExecuteAsync(output, application.ContainerEngine, image.ImageName, image.ImageTag);
output.WriteInfoLine($"Pushed docker image: '{image.ImageName}:{image.ImageTag}'");
}
}

Просмотреть файл

@ -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 Microsoft.Tye.ConfigModel;
using YamlDotNet.RepresentationModel;
@ -29,6 +30,21 @@ namespace Tye.Serialization
case "registry":
app.Registry = YamlParser.GetScalarValue(key, child.Value);
break;
case "containerEngine":
string engine = YamlParser.GetScalarValue(key, child.Value);
if (engine.Equals("docker", StringComparison.InvariantCultureIgnoreCase))
{
app.ContainerEngineType = ContainerEngineType.Docker;
}
else if (engine.Equals("podman", StringComparison.InvariantCultureIgnoreCase))
{
app.ContainerEngineType = ContainerEngineType.Podman;
}
else
{
throw new TyeYamlException($"Unknown container engine: \"{engine}\"");
}
break;
case "ingress":
YamlParser.ThrowIfNotYamlSequence(key, child.Value);
ConfigIngressParser.HandleIngress((child.Value as YamlSequenceNode)!, app.Ingress);

Просмотреть файл

@ -122,7 +122,7 @@ namespace Microsoft.Tye
output.WriteDebugLine($"Running 'minikube addons enable ingress'");
output.WriteCommandLine("minikube", "addon enable ingress");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
var exitCode = await ProcessUtil.ExecuteAsync(
$"minikube",
$"addons enable ingress",
System.Environment.CurrentDirectory,
@ -149,7 +149,7 @@ namespace Microsoft.Tye
output.WriteDebugLine($"Running 'kubectl apply'");
output.WriteCommandLine("kubectl", $"apply -f \"https://aka.ms/tye/ingress/deploy\"");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
var exitCode = await ProcessUtil.ExecuteAsync(
$"kubectl",
$"apply -f \"https://aka.ms/tye/ingress/deploy\"",
System.Environment.CurrentDirectory);

Просмотреть файл

@ -3,6 +3,8 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Net;
using System.Net.Sockets;
namespace Microsoft.Extensions.Configuration
{
@ -21,6 +23,11 @@ namespace Microsoft.Extensions.Configuration
return null;
}
if (IPAddress.TryParse(host, out IPAddress address) && address.AddressFamily == AddressFamily.InterNetworkV6)
{
host = "[" + host + "]";
}
return new Uri(protocol + "://" + host + ":" + port + "/");
}

Просмотреть файл

@ -36,36 +36,28 @@ namespace Microsoft.Tye.Hosting
return;
}
if (!await DockerDetector.Instance.IsDockerInstalled.Value)
if (!application.ContainerEngine.IsUsable(out string? unusableReason))
{
_logger.LogError("Unable to detect docker installation. Docker is not installed.");
_logger.LogError($"Unable to pull image: {unusableReason}.");
throw new CommandException("Docker is not installed.");
}
if (!await DockerDetector.Instance.IsDockerConnectedToDaemon.Value)
{
_logger.LogError("Unable to connect to docker daemon. Docker is not running.");
throw new CommandException("Docker is not running.");
throw new CommandException($"Unable to pull image: {unusableReason}.");
}
var tasks = new Task[images.Count];
var index = 0;
foreach (var image in images)
{
tasks[index++] = PullContainerAsync(image);
tasks[index++] = PullContainerAsync(application, image);
}
await Task.WhenAll(tasks);
}
private async Task PullContainerAsync(string image)
private async Task PullContainerAsync(Application application, string image)
{
await Task.Yield();
var result = await ProcessUtil.RunAsync(
"docker",
var result = await application.ContainerEngine.RunAsync(
$"images --filter \"reference={image}\" --format \"{{{{.ID}}}}\"",
throwOnError: false);
@ -85,8 +77,7 @@ namespace Microsoft.Tye.Hosting
_logger.LogInformation("Running docker command {command}", command);
result = await ProcessUtil.RunAsync(
"docker",
result = await application.ContainerEngine.RunAsync(
command,
outputDataReceived: data => _logger.LogInformation("{Image}: " + data, image),
errorDataReceived: data => _logger.LogInformation("{Image}: " + data, image),

Просмотреть файл

@ -33,7 +33,7 @@ namespace Microsoft.Tye.Hosting
public async Task StartAsync(Application application)
{
await PurgeFromPreviousRun();
await PurgeFromPreviousRun(application);
var containers = new List<Service>();
@ -100,7 +100,7 @@ namespace Microsoft.Tye.Hosting
if (!string.IsNullOrEmpty(application.Network))
{
var dockerNetworkResult = await ProcessUtil.RunAsync("docker", $"network ls --filter \"name={application.Network}\" --format \"{{{{.ID}}}}\"", throwOnError: false);
var dockerNetworkResult = await application.ContainerEngine.RunAsync($"network ls --filter \"name={application.Network}\" --format \"{{{{.ID}}}}\"", throwOnError: false);
if (dockerNetworkResult.ExitCode != 0)
{
_logger.LogError("{Network}: Run docker network ls command failed", application.Network);
@ -135,7 +135,7 @@ namespace Microsoft.Tye.Hosting
_logger.LogInformation("Running docker command {Command}", command);
var dockerNetworkResult = await ProcessUtil.RunAsync("docker", command, throwOnError: false);
var dockerNetworkResult = await application.ContainerEngine.RunAsync(command, throwOnError: false);
if (dockerNetworkResult.ExitCode != 0)
{
@ -148,17 +148,12 @@ namespace Microsoft.Tye.Hosting
// Stash information outside of the application services
application.Items[typeof(DockerApplicationInformation)] = new DockerApplicationInformation(dockerNetwork, proxies);
var tasks = new Task[containers.Count];
var index = 0;
foreach (var s in containers)
{
var docker = (DockerRunInfo)s.Description.RunInfo!;
tasks[index++] = StartContainerAsync(application, s, docker, dockerNetwork);
StartContainerAsync(application, s, docker, dockerNetwork);
}
await Task.WhenAll(tasks);
}
public async Task StopAsync(Application application)
@ -195,29 +190,25 @@ namespace Microsoft.Tye.Hosting
_logger.LogInformation("Running docker command {Command}", command);
// Clean up the network we created
await ProcessUtil.RunAsync("docker", command, throwOnError: false);
await application.ContainerEngine.RunAsync(command, throwOnError: false);
}
}
private async Task StartContainerAsync(Application application, Service service, DockerRunInfo docker, string? dockerNetwork)
private void StartContainerAsync(Application application, Service service, DockerRunInfo docker, string? dockerNetwork)
{
var serviceDescription = service.Description;
var environmentArguments = "";
var volumes = "";
var workingDirectory = docker.WorkingDirectory != null ? $"-w \"{docker.WorkingDirectory}\"" : "";
var hostname = "host.docker.internal";
var dockerImage = docker.Image ?? service.Description.Name;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
var hostname = application.ContainerEngine.ContainerHost;
if (hostname == null)
{
// See: https://github.com/docker/for-linux/issues/264
//
// host.docker.internal is making it's way into linux docker but doesn't work yet
// instead we use the machine IP
var addresses = await Dns.GetHostAddressesAsync(Dns.GetHostName());
hostname = addresses[0].ToString();
_logger.LogError("Configuration doesn't allow containers to access services on the host.");
throw new CommandException("Configuration doesn't allow containers to access services on the host.");
}
var dockerImage = docker.Image ?? service.Description.Name;
async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? ContainerPort, string? Protocol)> ports, CancellationToken cancellationToken)
{
var hasPorts = ports.Any();
@ -276,17 +267,19 @@ namespace Microsoft.Tye.Hosting
status.Environment = environment;
var environmentArguments = "";
foreach (var pair in environment)
{
environmentArguments += $"-e \"{pair.Key}={pair.Value}\" ";
}
var volumes = "";
foreach (var volumeMapping in docker.VolumeMappings)
{
if (volumeMapping.Source != null)
{
var sourcePath = Path.GetFullPath(Path.Combine(application.ContextDirectory, volumeMapping.Source));
volumes += $"-v \"{sourcePath}:{volumeMapping.Target}\" ";
volumes += $"-v \"{sourcePath}:{volumeMapping.Target}:{(volumeMapping.ReadOnly ? "ro," : "")}z\" ";
}
else if (volumeMapping.Name != null)
{
@ -294,7 +287,13 @@ namespace Microsoft.Tye.Hosting
}
}
var command = $"run -d {workingDirectory} {volumes} {environmentArguments} {portString} --name {replica} --restart=unless-stopped {dockerImage} {docker.Args ?? ""}";
var command = $"run -d {workingDirectory} {volumes} {environmentArguments} {portString} --name {replica} --restart=unless-stopped";
if (!string.IsNullOrEmpty(dockerNetwork))
{
status.DockerNetworkAlias = docker.NetworkAlias ?? serviceDescription!.Name;
command += $" --network {dockerNetwork} --network-alias {status.DockerNetworkAlias}";
}
command += $" {dockerImage} {docker.Args ?? ""}";
if (!docker.IsProxy)
{
@ -311,8 +310,7 @@ namespace Microsoft.Tye.Hosting
status.DockerNetwork = dockerNetwork;
WriteReplicaToStore(replica);
var result = await ProcessUtil.RunAsync(
"docker",
var result = await application.ContainerEngine.RunAsync(
command,
throwOnError: false,
cancellationToken: cancellationToken,
@ -336,7 +334,7 @@ namespace Microsoft.Tye.Hosting
while (string.IsNullOrEmpty(containerId))
{
// Try to get the ID of the container
result = await ProcessUtil.RunAsync("docker", $"ps --no-trunc -f name={replica} --format " + "{{.ID}}");
result = await application.ContainerEngine.RunAsync($"ps --no-trunc -f name={replica} --format " + "{{.ID}}");
containerId = result.ExitCode == 0 ? result.StandardOutput.Trim() : null;
}
@ -347,21 +345,6 @@ namespace Microsoft.Tye.Hosting
_logger.LogInformation("Running container {ContainerName} with ID {ContainerId}", replica, shortContainerId);
if (!string.IsNullOrEmpty(dockerNetwork))
{
status.DockerNetworkAlias = docker.NetworkAlias ?? serviceDescription!.Name;
var networkCommand = $"network connect {dockerNetwork} {replica} --alias {status.DockerNetworkAlias}";
service.Logs.OnNext($"[{replica}]: docker {networkCommand}");
_logger.LogInformation("Running docker command {Command}", networkCommand);
result = await ProcessUtil.RunAsync("docker", networkCommand);
PrintStdOutAndErr(service, replica, result);
}
var sentStartedEvent = false;
while (!cancellationToken.IsCancellationRequested)
@ -369,7 +352,7 @@ namespace Microsoft.Tye.Hosting
if (sentStartedEvent)
{
using var restartCts = new CancellationTokenSource(DockerStopTimeout);
result = await ProcessUtil.RunAsync("docker", $"restart {containerId}", throwOnError: false, cancellationToken: restartCts.Token);
result = await application.ContainerEngine.RunAsync($"restart {containerId}", throwOnError: false, cancellationToken: restartCts.Token);
if (restartCts.IsCancellationRequested)
{
@ -398,7 +381,7 @@ namespace Microsoft.Tye.Hosting
while (!status.StoppingTokenSource.Token.IsCancellationRequested)
{
var logsRes = await ProcessUtil.RunAsync("docker", $"logs -f {containerId}",
var logsRes = await application.ContainerEngine.RunAsync($"logs -f {containerId}",
outputDataReceived: data => service.Logs.OnNext($"[{replica}]: {data}"),
errorDataReceived: data => service.Logs.OnNext($"[{replica}]: {data}"),
throwOnError: false,
@ -435,7 +418,7 @@ namespace Microsoft.Tye.Hosting
_logger.LogInformation("Stopping container {ContainerName} with ID {ContainerId}", replica, shortContainerId);
result = await ProcessUtil.RunAsync("docker", $"stop {containerId}", throwOnError: false, cancellationToken: timeoutCts.Token);
result = await application.ContainerEngine.RunAsync($"stop {containerId}", throwOnError: false, cancellationToken: timeoutCts.Token);
if (timeoutCts.IsCancellationRequested)
{
@ -451,7 +434,7 @@ namespace Microsoft.Tye.Hosting
_logger.LogInformation("Stopped container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode);
result = await ProcessUtil.RunAsync("docker", $"rm {containerId}", throwOnError: false, cancellationToken: timeoutCts.Token);
result = await application.ContainerEngine.RunAsync($"rm {containerId}", throwOnError: false, cancellationToken: timeoutCts.Token);
if (timeoutCts.IsCancellationRequested)
{
@ -486,8 +469,7 @@ namespace Microsoft.Tye.Hosting
arguments.Append($" --build-arg {buildArg.Key}={buildArg.Value}");
}
var dockerBuildResult = await ProcessUtil.RunAsync(
$"docker",
var dockerBuildResult = await application.ContainerEngine.RunAsync(
arguments.ToString(),
outputDataReceived: Log,
errorDataReceived: Log,
@ -550,13 +532,13 @@ namespace Microsoft.Tye.Hosting
service.Items[typeof(DockerInformation)] = dockerInfo;
}
private async Task PurgeFromPreviousRun()
private async Task PurgeFromPreviousRun(Application application)
{
var dockerReplicas = await _replicaRegistry.GetEvents(DockerReplicaStore);
foreach (var replica in dockerReplicas)
{
var container = replica["container"];
await ProcessUtil.RunAsync("docker", $"rm -f {container}", throwOnError: false);
await application.ContainerEngine.RunAsync($"rm -f {container}", throwOnError: false);
_logger.LogInformation("removed container {container} from previous run", container);
}

Просмотреть файл

@ -12,17 +12,20 @@ namespace Microsoft.Tye.Hosting.Model
{
public class Application
{
public Application(FileInfo source, Dictionary<string, Service> services)
public Application(FileInfo source, Dictionary<string, Service> services, ContainerEngine containerEngine)
{
Source = source.FullName;
ContextDirectory = source.DirectoryName!;
Services = services;
ContainerEngine = containerEngine;
}
public string Source { get; }
public string ContextDirectory { get; }
public ContainerEngine ContainerEngine { get; set; }
public Dictionary<string, Service> Services { get; }
public Dictionary<object, object> Items { get; } = new Dictionary<object, object>();

Просмотреть файл

@ -7,15 +7,17 @@ namespace Microsoft.Tye.Hosting.Model
{
public class DockerVolume
{
public DockerVolume(string? source, string? name, string target)
public DockerVolume(string? source, string? name, string target, bool readOnly = false)
{
Source = source;
Name = name;
Target = target;
ReadOnly = readOnly;
}
public string? Name { get; }
public string? Source { get; }
public string Target { get; }
public bool ReadOnly { get; }
}
}

Просмотреть файл

@ -217,15 +217,10 @@ namespace Microsoft.Tye.Hosting
if (hasPorts)
{
// We need to bind to all interfaces on linux since the container -> host communication won't work
// if we use the IP address to reach out of the host. This works fine on osx and windows
// but doesn't work on linux.
var host = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "*" : "localhost";
// These are the ports that the application should use for binding
// 1. Configure ASP.NET Core to bind to those same ports
environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://{host}:{p.Port}"));
environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://{application.ContainerEngine.AspNetUrlsHost}:{p.Port}"));
// Set the HTTPS port for the redirect middleware
foreach (var p in ports)

Просмотреть файл

@ -87,10 +87,12 @@ namespace Microsoft.Tye.Hosting
// This is .NET specific
var userSecretStore = GetUserSecretsPathFromSecrets();
Directory.CreateDirectory(userSecretStore);
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"));
dockerRunInfo.VolumeMappings.Add(new DockerVolume(source: userSecretStore, name: null, target: "/root/.microsoft/usersecrets", readOnly: true));
}
// Default to development environment
@ -116,7 +118,7 @@ namespace Microsoft.Tye.Hosting
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"));
dockerRunInfo.VolumeMappings.Add(new DockerVolume(source: certificateDirectory.DirectoryPath, name: null, target: "/root/.aspnet/https", readOnly: true));
}
// Change the project into a container info

Просмотреть файл

@ -13,6 +13,11 @@
"description": "Dockerhub username or hostname of remote registry. Used for tagging images.",
"type": "string"
},
"containerEngine": {
"description": "Container engine.",
"type": "string",
"enum": ["docker", "podman"]
},
"namespace": {
"description": "The Kubernetes namespace to use.",
"type": "string"

Просмотреть файл

@ -213,7 +213,7 @@ namespace Microsoft.Tye
services.Add(ingress.Name, new Service(description));
}
return new Application(application.Source, services) { Network = application.Network };
return new Application(application.Source, services, application.ContainerEngine) { Network = application.Network };
}
public static Tye.Hosting.Model.EnvironmentVariable ToHostingEnvironmentVariable(this EnvironmentVariableBuilder builder)

Просмотреть файл

@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
@ -504,7 +505,7 @@ services:
});
// Delete the volume
await ProcessUtil.RunAsync("docker", $"volume rm {volumeName}");
await ContainerEngine.Default.RunAsync($"volume rm {volumeName}");
}
[ConditionalFact]
@ -521,7 +522,7 @@ services:
application.Network = dockerNetwork;
// Create the existing network
await ProcessUtil.RunAsync("docker", $"network create {dockerNetwork}");
await ContainerEngine.Default.RunAsync($"network create {dockerNetwork}");
var handler = new HttpClientHandler
{
@ -560,7 +561,7 @@ services:
finally
{
// Delete the network
await ProcessUtil.RunAsync("docker", $"network rm {dockerNetwork}");
await ContainerEngine.Default.RunAsync($"network rm {dockerNetwork}");
}
}
@ -771,6 +772,14 @@ services:
[SkipIfDockerNotRunning]
public async Task NginxIngressTest()
{
// https://github.com/dotnet/tye/issues/428
// nginx container fails to start succesfully on non-Windows because it
// can't resolve the upstream hosts.
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
using var projectDirectory = CopyTestProjectDirectory("nginx-ingress");
var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));

Просмотреть файл

@ -10,6 +10,7 @@ using System.Text;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
using Microsoft.Tye;
namespace Test.Infrastructure
{
@ -22,8 +23,7 @@ namespace Test.Infrastructure
var builder = new StringBuilder();
output.WriteLine($"> docker images \"{repository}\" --format \"{{{{.Repository}}}}\"");
var exitCode = await Process.ExecuteAsync(
"docker",
var exitCode = await ContainerEngine.Default.ExecuteAsync(
$"images \"{repository}\" --format \"{{{{.Repository}}}}\"",
stdOut: OnOutput,
stdErr: OnOutput);
@ -33,12 +33,13 @@ namespace Test.Infrastructure
}
var lines = builder.ToString().Split(new[] { '\r', '\n', }, StringSplitOptions.RemoveEmptyEntries);
if (lines.Any(line => line == repository))
if (lines.Any(line => line == repository ||
line == $"localhost/{repository}")) // podman format.
{
return;
}
throw new XunitException($"Image '{repository}' was not found.");
throw new XunitException($"Image '{repository}' was not found in {builder.ToString()}.");
void OnOutput(string text)
{
@ -57,8 +58,7 @@ namespace Test.Infrastructure
{
output.WriteLine($"> docker rmi \"{id}\" --force");
var exitCode = await Process.ExecuteAsync(
"docker",
var exitCode = await ContainerEngine.Default.ExecuteAsync(
$"rmi \"{id}\" --force",
stdOut: OnOutput,
stdErr: OnOutput);
@ -82,8 +82,7 @@ namespace Test.Infrastructure
var builder = new StringBuilder();
output.WriteLine($"> docker ps --format \"{{{{.ID}}}}\"");
var exitCode = await Process.ExecuteAsync(
"docker",
var exitCode = await ContainerEngine.Default.ExecuteAsync(
$"ps --format \"{{{{.ID}}}}\"",
stdOut: OnOutput,
stdErr: OnOutput);
@ -110,8 +109,7 @@ namespace Test.Infrastructure
var builder = new StringBuilder();
output.WriteLine($"> docker images -q \"{repository}\"");
var exitCode = await Process.ExecuteAsync(
"docker",
var exitCode = await ContainerEngine.Default.ExecuteAsync(
$"images -q \"{repository}\"",
stdOut: OnOutput,
stdErr: OnOutput);

Просмотреть файл

@ -5,6 +5,7 @@
using System;
using System.Runtime.InteropServices;
using Microsoft.Tye;
using Microsoft.Tye.ConfigModel;
namespace Test.Infrastructure
{
@ -14,8 +15,8 @@ namespace Test.Infrastructure
public SkipIfDockerNotRunningAttribute()
{
// TODO Check performance of this.
IsMet = DockerDetector.Instance.IsDockerConnectedToDaemon.Value.GetAwaiter().GetResult() && !(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AGENT_OS")));
SkipReason = "Docker is not installed or running.";
IsMet = ContainerEngine.Default.IsUsable(out string unusableReason) && !(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AGENT_OS")));
SkipReason = $"Container engine not usable: {unusableReason}";
}
public bool IsMet { get; }

Просмотреть файл

@ -251,8 +251,8 @@ namespace Test.Infrastructure
var processRunner = new ProcessRunner(logger, replicaRegistry, new ProcessRunnerOptions());
var dockerRunner = new DockerRunner(logger, replicaRegistry);
await processRunner.StartAsync(new Application(new FileInfo(host.Application.Source), new Dictionary<string, Service>()));
await dockerRunner.StartAsync(new Application(new FileInfo(host.Application.Source), new Dictionary<string, Service>()));
await processRunner.StartAsync(new Application(new FileInfo(host.Application.Source), new Dictionary<string, Service>(), ContainerEngine.Default));
await dockerRunner.StartAsync(new Application(new FileInfo(host.Application.Source), new Dictionary<string, Service>(), ContainerEngine.Default));
}
await DoOperationAndWaitForReplicasToChangeState(host, ReplicaState.Stopped, replicas.Length, replicas.ToHashSet(), new HashSet<string>(), TimeSpan.Zero, Purge);