committed by
GitHub
27 changed files with 328 additions and 171 deletions
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue