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

Add podman support #570

Closed
wants to merge 10 commits into from
17 changes: 17 additions & 0 deletions src/Microsoft.Tye.Core/DockerDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ private DockerDetector()
{
IsDockerInstalled = new Lazy<Task<bool>>(DetectDockerInstalled);
IsDockerConnectedToDaemon = new Lazy<Task<bool>>(DetectDockerConnectedToDaemon);
IsPodman = new Lazy<Task<bool>>(DetectDockerIsPodman);
}

public Lazy<Task<bool>> IsDockerInstalled { get; }

public Lazy<Task<bool>> IsDockerConnectedToDaemon { get; }

public Lazy<Task<bool>> IsPodman { get; }

private async Task<bool> DetectDockerInstalled()
{
try
Expand Down Expand Up @@ -51,5 +54,19 @@ private async Task<bool> DetectDockerConnectedToDaemon()
return false;
}
}

private async Task<bool> DetectDockerIsPodman()
{
try
{
await ProcessUtil.RunAsync("podman", "version", throwOnError: false);
return true;
}
catch (Exception)
{
// Unfortunately, process throws
return false;
}
}
}
}
186 changes: 111 additions & 75 deletions src/Microsoft.Tye.Hosting/DockerRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,95 +50,106 @@ public async Task StartAsync(Application application)
return;
}

string? dockerNetwork = null;
var proxies = new List<Service>();
foreach (var service in application.Services.Values)
List<string>? localhostServices = null;
if (!application.UseHostNetwork)
{
if (service.Description.RunInfo is DockerRunInfo || service.Description.Bindings.Count == 0)
{
continue;
}

// Inject a proxy per non-container service. This allows the container to use normal host names within the
// container network to talk to services on the host
var proxyContanier = new DockerRunInfo($"mcr.microsoft.com/dotnet/core/sdk:3.1", "dotnet Microsoft.Tye.Proxy.dll")
{
WorkingDirectory = "/app",
NetworkAlias = service.Description.Name,
Private = true
};
var proxyLocation = Path.GetDirectoryName(typeof(Microsoft.Tye.Proxy.Program).Assembly.Location);
proxyContanier.VolumeMappings.Add(new DockerVolume(proxyLocation, name: null, target: "/app"));
var proxyDescription = new ServiceDescription($"{service.Description.Name}-proxy", proxyContanier);
foreach (var binding in service.Description.Bindings)
foreach (var service in application.Services.Values)
{
if (binding.Port == null)
if (service.Description.RunInfo is DockerRunInfo || service.Description.Bindings.Count == 0)
{
continue;
}

var b = new ServiceBinding()
// Inject a proxy per non-container service. This allows the container to use normal host names within the
// container network to talk to services on the host
var proxyContanier = new DockerRunInfo($"mcr.microsoft.com/dotnet/core/sdk:3.1", "dotnet Microsoft.Tye.Proxy.dll")
{
ConnectionString = binding.ConnectionString,
Host = binding.Host,
ContainerPort = binding.ContainerPort,
Name = binding.Name,
Port = binding.Port,
Protocol = binding.Protocol
WorkingDirectory = "/app",
NetworkAlias = service.Description.Name,
Private = true
};
b.ReplicaPorts.Add(b.Port.Value);
proxyDescription.Bindings.Add(b);
}
var proxyContanierService = new Service(proxyDescription);
containers.Add(proxyContanierService);
proxies.Add(proxyContanierService);
}
var proxyLocation = Path.GetDirectoryName(typeof(Microsoft.Tye.Proxy.Program).Assembly.Location);
proxyContanier.VolumeMappings.Add(new DockerVolume(proxyLocation, name: null, target: "/app"));
var proxyDescription = new ServiceDescription($"{service.Description.Name}-proxy", proxyContanier);
foreach (var binding in service.Description.Bindings)
{
if (binding.Port == null)
{
continue;
}

string? dockerNetwork = null;
var b = new ServiceBinding()
{
ConnectionString = binding.ConnectionString,
Host = binding.Host,
ContainerPort = binding.ContainerPort,
Name = binding.Name,
Port = binding.Port,
Protocol = binding.Protocol
};
b.ReplicaPorts.Add(b.Port.Value);
proxyDescription.Bindings.Add(b);
}
var proxyContanierService = new Service(proxyDescription);
containers.Add(proxyContanierService);
proxies.Add(proxyContanierService);
}

if (!string.IsNullOrEmpty(application.Network))
{
var dockerNetworkResult = await ProcessUtil.RunAsync("docker", $"network ls --filter \"name={application.Network}\" --format \"{{{{.ID}}}}\"", throwOnError: false);
if (dockerNetworkResult.ExitCode != 0)
if (!string.IsNullOrEmpty(application.Network))
{
_logger.LogError("{Network}: Run docker network ls command failed", application.Network);
var dockerNetworkResult = await ProcessUtil.RunAsync("docker", $"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);

throw new CommandException("Run docker network ls command failed");
}
throw new CommandException("Run docker network ls command failed");
}

if (!string.IsNullOrWhiteSpace(dockerNetworkResult.StandardOutput))
{
_logger.LogInformation("The specified network {Network} exists", application.Network);
if (!string.IsNullOrWhiteSpace(dockerNetworkResult.StandardOutput))
{
_logger.LogInformation("The specified network {Network} exists", application.Network);

dockerNetwork = application.Network;
}
else
{
_logger.LogWarning("The specified network {Network} doesn't exist.", application.Network);
dockerNetwork = application.Network;
}
else
{
_logger.LogWarning("The specified network {Network} doesn't exist.", application.Network);

application.Network = null;
application.Network = null;
}
}
}

// We're going to be making containers, only make a network if we have more than one (we assume they'll need to talk)
if (string.IsNullOrEmpty(dockerNetwork) && containers.Count > 1)
{
dockerNetwork = "tye_network_" + Guid.NewGuid().ToString().Substring(0, 10);
// We're going to be making containers, only make a network if we have more than one (we assume they'll need to talk)
if (string.IsNullOrEmpty(dockerNetwork) && containers.Count > 1)
{
dockerNetwork = "tye_network_" + Guid.NewGuid().ToString().Substring(0, 10);

application.Items["dockerNetwork"] = dockerNetwork;
application.Items["dockerNetwork"] = dockerNetwork;

_logger.LogInformation("Creating docker network {Network}", dockerNetwork);
_logger.LogInformation("Creating docker network {Network}", dockerNetwork);

var command = $"network create --driver bridge {dockerNetwork}";
var command = $"network create --driver bridge {dockerNetwork}";

_logger.LogInformation("Running docker command {Command}", command);
_logger.LogInformation("Running docker command {Command}", command);

var dockerNetworkResult = await ProcessUtil.RunAsync("docker", command, throwOnError: false);
var dockerNetworkResult = await ProcessUtil.RunAsync("docker", command, throwOnError: false);

if (dockerNetworkResult.ExitCode != 0)
{
_logger.LogInformation("Running docker command with exception info {ExceptionStdOut} {ExceptionStdErr}", dockerNetworkResult.StandardOutput, dockerNetworkResult.StandardError);
if (dockerNetworkResult.ExitCode != 0)
{
_logger.LogInformation("Running docker command with exception info {ExceptionStdOut} {ExceptionStdErr}", dockerNetworkResult.StandardOutput, dockerNetworkResult.StandardError);

throw new CommandException("Run docker network create command failed");
throw new CommandException("Run docker network create command failed");
}
}
}
else
{
localhostServices = new List<string>();
foreach (var s in application.Services.Values)
{
localhostServices.Add(s.Description.Name);
}
}

Expand All @@ -152,7 +163,7 @@ public async Task StartAsync(Application application)
{
var docker = (DockerRunInfo)s.Description.RunInfo!;

tasks[index++] = StartContainerAsync(application, s, docker, dockerNetwork);
tasks[index++] = StartContainerAsync(application, s, localhostServices, docker, dockerNetwork);
}

await Task.WhenAll(tasks);
Expand Down Expand Up @@ -196,23 +207,30 @@ public async Task StopAsync(Application application)
}
}

private async Task StartContainerAsync(Application application, Service service, DockerRunInfo docker, string? dockerNetwork)
private async Task StartContainerAsync(Application application, Service service, List<string>? localhostServices, 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))
string dockerHostHostname;
if (application.UseHostNetwork)
{
dockerHostHostname = "localhost";
}
else
{
dockerHostHostname = "host.docker.internal";

// 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();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var addresses = await Dns.GetHostAddressesAsync(Dns.GetHostName());
dockerHostHostname = addresses[0].ToString();
}
}

async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? ContainerPort, string? Protocol)> ports, CancellationToken cancellationToken)
Expand Down Expand Up @@ -266,18 +284,20 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont
//
// The way we do proxying here doesn't really work for multi-container scenarios on linux
// without some more setup.
application.PopulateEnvironment(service, (key, value) => environment[key] = value, hostname!);
application.PopulateEnvironment(service, (key, value) => environment[key] = value, dockerHostHostname!);

environment["APP_INSTANCE"] = replica;
environment["CONTAINER_HOST"] = hostname!;
environment["CONTAINER_HOST"] = dockerHostHostname!;

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)
Expand All @@ -291,7 +311,23 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont
}
}

var command = $"run -d \"{workingDirectory}\" {volumes} {environmentArguments} {portString} --name {replica} --restart=unless-stopped {dockerImage} {docker.Args ?? ""}";
// When running on the host network, make service names resolve to 127.0.0.1.
// When running on a specific network, service names are resolved via aliases passed to the 'network connect' command.
string hostNetworkArgs = "";
if (application.UseHostNetwork)
{
hostNetworkArgs = $"--network host";
foreach (var serviceName in localhostServices!)
{
hostNetworkArgs += $" --add-host {serviceName}:127.0.0.1";
}
}

// Workaround podman issue: https://github.com/containers/libpod/issues/6508
// Fixed in podman v2.
bool isPodman = await DockerDetector.Instance.IsPodman.Value;
string restartArg = isPodman ? "always" : "unless-stopped";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean for shutdown?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This affect what happens when the system restarts.
For "docker", "unless-stopped" means only containers which weren't stopped will be started at boot.
"podman" doesn't start containers at boot. With podman v2, "always" and "unless-stopped" are aliases.

var command = $"run -d \"{workingDirectory}\" {volumes} {environmentArguments} {portString} {hostNetworkArgs} --name {replica} --restart={restartArg} {dockerImage} {docker.Args ?? ""}";

_logger.LogInformation("Running image {Image} for {Replica}", docker.Image, replica);

Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.Tye.Hosting/Model/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public Application(FileInfo source, Dictionary<string, Service> services)

public string? Network { get; set; }

// All services and application run on the container host.
public bool UseHostNetwork { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to parse this in config, right? Do you intend for this to be part of tye.yaml and/or a command line arg to tye run?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah nvrm, you use podman existing as that check.

Little bit confusing, should this variable be called IsPodman for now?


public void PopulateEnvironment(Service service, Action<string, string> set, string defaultHost = "localhost")
{
var bindings = ComputeBindings(service, defaultHost);
Expand Down
39 changes: 30 additions & 9 deletions src/Microsoft.Tye.Hosting/PortAssigner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@ public PortAssigner(ILogger logger)
_logger = logger;
}

public Task StartAsync(Application application)
public async Task StartAsync(Application application)
{
// rootless podman doesn't permit creation of networks.
// Use the host network instead, and perform communication between applications using "localhost".
if (string.IsNullOrEmpty(application.Network))
{
bool isPodman = await DockerDetector.Instance.IsPodman.Value;
application.UseHostNetwork = isPodman;
}

foreach (var service in application.Services.Values)
{
if (service.Description.RunInfo == null)
Expand All @@ -44,10 +52,22 @@ static int GetNextPort()

foreach (var binding in service.Description.Bindings)
{
// Auto assign a port
// We assign a port to each binding.
// When we use the host network, port mapping is not supported.
// The ContainerPort and Port need to match.
if (binding.Port == null)
{
binding.Port = GetNextPort();
// UseHostNetwork: ContainerPort exposes the service on localhost
// Set Port to match ContainerPort.
if (application.UseHostNetwork && binding.ContainerPort.HasValue)
{
binding.Port = binding.ContainerPort.Value;
}
else
{
// Pick a random port.
binding.Port = GetNextPort();
}
}

if (service.Description.Readiness == null && service.Description.Replicas == 1)
Expand All @@ -72,22 +92,23 @@ static int GetNextPort()
binding.Name ?? binding.Protocol);
}

// Set ContainerPort for the first http and https port.
// For ASP.NET we'll match the Port when UseHostNetwork. ASPNETCORE_URLS will configure the application.
// For other applications, we use the default ports 80 and 443.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should we default to 80 and 443 for non-aspnet services?

var httpBinding = service.Description.Bindings.FirstOrDefault(b => b.Protocol == "http");
var httpsBinding = service.Description.Bindings.FirstOrDefault(b => b.Protocol == "https");

// Default the first http and https port to 80 and 443
bool isAspNetWithHostNetwork = application.UseHostNetwork &&
(service.Description.RunInfo as DockerRunInfo)?.IsAspNet == true;
if (httpBinding != null)
{
httpBinding.ContainerPort ??= 80;
httpBinding.ContainerPort ??= isAspNetWithHostNetwork ? httpBinding.Port : 80;
}

if (httpsBinding != null)
{
httpsBinding.ContainerPort ??= 443;
httpsBinding.ContainerPort ??= isAspNetWithHostNetwork ? httpsBinding.Port : 443;
}
}

return Task.CompletedTask;
}

public Task StopAsync(Application application)
Expand Down
Loading