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

Support podman #1014

Merged
merged 4 commits into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Microsoft.Tye.Core/ApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -23,6 +24,8 @@ public ApplicationBuilder(FileInfo source, string name)

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>();
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.Tye.Core/ApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F
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
};
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.Tye.Core/BuildDockerImageStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder
return;
}

if (!DockerDetector.Instance.IsUsable(out string? unusableReason))
if (!application.ContainerEngine.IsUsable(out string? unusableReason))
{
throw new CommandException($"Cannot generate a docker image for '{service.Name}' because {unusableReason}.");
}
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class ConfigApplication

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>>();
Expand Down
12 changes: 12 additions & 0 deletions src/Microsoft.Tye.Core/ConfigModel/ContainerEngineType.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
185 changes: 185 additions & 0 deletions src/Microsoft.Tye.Core/ContainerEngine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// 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;
}

if (!Version.TryParse(result.StandardOutput, out Version? version))
{
unusableReason = $"cannot parse podman version '{result.StandardOutput}'.";
return true;
}
Version minVersion = new Version(3, 1);
if (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;
}
}
}
6 changes: 2 additions & 4 deletions src/Microsoft.Tye.Core/DockerContainerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ public static async Task BuildContainerImageFromDockerFileAsync(OutputContext ou
output.WriteDebugLine("Running 'docker build'.");
output.WriteCommandLine("docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"");
var capture = output.Capture();
var exitCode = await ProcessUtil.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,
Expand Down Expand Up @@ -148,8 +147,7 @@ public static async Task BuildContainerImageAsync(OutputContext output, Applicat
output.WriteDebugLine("Running 'docker build'.");
output.WriteCommandLine("docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"");
var capture = output.Capture();
var exitCode = await ProcessUtil.ExecuteAsync(
$"docker",
var exitCode = await application.ContainerEngine.ExecuteAsync(
$"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"",
project.ProjectFile.DirectoryName,
stdOut: capture.StdOut,
Expand Down
125 changes: 0 additions & 125 deletions src/Microsoft.Tye.Core/DockerDetector.cs

This file was deleted.

Loading