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 all commits
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
9 changes: 2 additions & 7 deletions src/Microsoft.Tye.Core/BuildDockerImageStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,9 @@ public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder
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)
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
}
}
181 changes: 181 additions & 0 deletions src/Microsoft.Tye.Core/ContainerEngine.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder
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,
Expand Down Expand Up @@ -81,7 +81,7 @@ public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder
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,
Expand Down
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 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,
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 Process.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
55 changes: 0 additions & 55 deletions src/Microsoft.Tye.Core/DockerDetector.cs

This file was deleted.

5 changes: 2 additions & 3 deletions src/Microsoft.Tye.Core/DockerPush.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -30,8 +30,7 @@ public static async Task ExecuteAsync(OutputContext output, string imageName, st
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);
Expand Down
13 changes: 12 additions & 1 deletion src/Microsoft.Tye.Core/ProcessUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ public static class ProcessUtil

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,
Expand Down Expand Up @@ -109,7 +120,7 @@ public static async Task<ProcessResult> RunAsync(

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
{
Expand Down
Loading