зеркало из https://github.com/dotnet/tye.git
Support podman (#1014)
This commit is contained in:
Родитель
c09165cb0d
Коммит
c3f3c6cb91
|
@ -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);
|
||||
|
|
Загрузка…
Ссылка в новой задаче