From 63d176dd3e191d5dd186c8dd8a45792e025947a7 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Mon, 19 Apr 2021 19:42:59 +0200 Subject: [PATCH 1/4] Support podman --- .../BuildDockerImageStep.cs | 9 +- ...DeployApplicationKubernetesManifestStep.cs | 4 +- .../DockerContainerBuilder.cs | 4 +- src/Microsoft.Tye.Core/DockerDetector.cs | 118 ++++++++++++++---- src/Microsoft.Tye.Core/DockerPush.cs | 2 +- src/Microsoft.Tye.Core/ProcessUtil.cs | 24 +++- src/Microsoft.Tye.Core/ValidateIngressStep.cs | 4 +- .../TyeConfigurationExtensions.cs | 7 ++ .../DockerImagePuller.cs | 13 +- src/Microsoft.Tye.Hosting/DockerRunner.cs | 54 +++----- .../Model/DockerVolume.cs | 4 +- src/Microsoft.Tye.Hosting/ProcessRunner.cs | 7 +- .../TransformProjectsIntoContainers.cs | 6 +- test/E2ETest/TyeRunTests.cs | 9 ++ test/Test.Infrastructure/DockerAssert.cs | 14 ++- .../SkipIfDockerNotRunningAttribute.cs | 4 +- 16 files changed, 182 insertions(+), 101 deletions(-) diff --git a/src/Microsoft.Tye.Core/BuildDockerImageStep.cs b/src/Microsoft.Tye.Core/BuildDockerImageStep.cs index ceb0431c0..04b66ef16 100644 --- a/src/Microsoft.Tye.Core/BuildDockerImageStep.cs +++ b/src/Microsoft.Tye.Core/BuildDockerImageStep.cs @@ -25,14 +25,9 @@ public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder return; } - if (!await DockerDetector.Instance.IsDockerInstalled.Value) + if (!DockerDetector.Instance.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) diff --git a/src/Microsoft.Tye.Core/DeployApplicationKubernetesManifestStep.cs b/src/Microsoft.Tye.Core/DeployApplicationKubernetesManifestStep.cs index 89a2a5e49..237940362 100644 --- a/src/Microsoft.Tye.Core/DeployApplicationKubernetesManifestStep.cs +++ b/src/Microsoft.Tye.Core/DeployApplicationKubernetesManifestStep.cs @@ -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, @@ -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, diff --git a/src/Microsoft.Tye.Core/DockerContainerBuilder.cs b/src/Microsoft.Tye.Core/DockerContainerBuilder.cs index 043ef8147..80fca6257 100644 --- a/src/Microsoft.Tye.Core/DockerContainerBuilder.cs +++ b/src/Microsoft.Tye.Core/DockerContainerBuilder.cs @@ -43,7 +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( + var exitCode = await ProcessUtil.ExecuteAsync( $"docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"", new FileInfo(containerService.DockerFile).DirectoryName, @@ -148,7 +148,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( + var exitCode = await ProcessUtil.ExecuteAsync( $"docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"", project.ProjectFile.DirectoryName, diff --git a/src/Microsoft.Tye.Core/DockerDetector.cs b/src/Microsoft.Tye.Core/DockerDetector.cs index 642421ba0..954cbe631 100644 --- a/src/Microsoft.Tye.Core/DockerDetector.cs +++ b/src/Microsoft.Tye.Core/DockerDetector.cs @@ -3,6 +3,10 @@ // See the LICENSE file in the project root for more information. using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -14,41 +18,107 @@ public class DockerDetector public static DockerDetector Instance { get; } = new DockerDetector(); - private DockerDetector() - { - IsDockerInstalled = new Lazy>(DetectDockerInstalled); - IsDockerConnectedToDaemon = new Lazy>(DetectDockerConnectedToDaemon); - } + private bool _isUsable { get; } + private string? _unusableReason { get; } - public Lazy> IsDockerInstalled { get; } + public bool IsPodman { get; } + public string AspNetUrlsHost { get; } + public string? ContainerHost { get; } - public Lazy> IsDockerConnectedToDaemon { get; } - - private async Task DetectDockerInstalled() + public bool IsUsable(out string? unusableReason) { - try - { - await ProcessUtil.RunAsync("docker", "version", throwOnError: false, cancellationToken: new CancellationTokenSource(Timeout).Token); - return true; - } - catch (Exception) - { - // Unfortunately, process throws - return false; - } + unusableReason = _unusableReason; + return _isUsable; } - private async Task DetectDockerConnectedToDaemon() + private DockerDetector() { + AspNetUrlsHost = "localhost"; try { - var result = await ProcessUtil.RunAsync("docker", "version", throwOnError: false, cancellationToken: new CancellationTokenSource(Timeout).Token); - return result.ExitCode == 0; + ProcessResult result; + try + { + // try to use podman. + result = ProcessUtil.RunAsync("podman", "version -f \"{{ .Client.Version }}\"", throwOnError: false, cancellationToken: new CancellationTokenSource(Timeout).Token).Result; + IsPodman = true; + + if (result.ExitCode != 0) + { + _unusableReason = $"podman version exited with {result.ExitCode}. Standard error: \"{result.StandardError}\"."; + return; + } + + if (!Version.TryParse(result.StandardOutput, out Version? version)) + { + _unusableReason = $"cannot parse podman version '{result.StandardOutput}'."; + return; + } + Version minVersion = new Version(3, 1); + if (version < minVersion) + { + _unusableReason = $"podman version '{result.StandardOutput}' is less than the required '{minVersion}'."; + return; + } + + // 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(); + // 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"; + } + } + catch (Exception) + { + // try to use docker. + + 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"; + } + + result = ProcessUtil.RunAsync("docker", "version", throwOnError: false, cancellationToken: new CancellationTokenSource(Timeout).Token).Result; + + if (result.ExitCode != 0) + { + _unusableReason = "docker is not connected."; + return; + } + } + + _isUsable = true; } catch (Exception) { - // Unfortunately, process throws - return false; + _unusableReason = "docker is not installed."; } } } diff --git a/src/Microsoft.Tye.Core/DockerPush.cs b/src/Microsoft.Tye.Core/DockerPush.cs index c1c83b249..1c4dcb8f4 100644 --- a/src/Microsoft.Tye.Core/DockerPush.cs +++ b/src/Microsoft.Tye.Core/DockerPush.cs @@ -30,7 +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( + var exitCode = await ProcessUtil.ExecuteAsync( $"docker", $"push {imageName}:{imageTag}", stdOut: capture.StdOut, diff --git a/src/Microsoft.Tye.Core/ProcessUtil.cs b/src/Microsoft.Tye.Core/ProcessUtil.cs index ef77ec448..4cceb831f 100644 --- a/src/Microsoft.Tye.Core/ProcessUtil.cs +++ b/src/Microsoft.Tye.Core/ProcessUtil.cs @@ -23,6 +23,18 @@ public static class ProcessUtil private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + public static Task ExecuteAsync( + string command, + string args, + string? workingDir = null, + Action? stdOut = null, + Action? stdErr = null, + params (string key, string value)[] environmentVariables) + { + command = CheckForPodman(command); + return System.CommandLine.Invocation.Process.ExecuteAsync(command, args, workingDir, stdOut, stdErr, environmentVariables); + } + public static async Task RunAsync( string filename, string arguments, @@ -35,6 +47,7 @@ public static async Task RunAsync( Action? onStop = null, CancellationToken cancellationToken = default) { + filename = CheckForPodman(filename); using var process = new Process() { StartInfo = @@ -109,7 +122,7 @@ public static async Task 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 { @@ -175,5 +188,14 @@ public static void KillProcess(int pid) catch (ArgumentException) { } catch (InvalidOperationException) { } } + + private static string CheckForPodman(string command) + { + if (command == "docker" && DockerDetector.Instance.IsPodman) + { + return "podman"; + } + return command; + } } } diff --git a/src/Microsoft.Tye.Core/ValidateIngressStep.cs b/src/Microsoft.Tye.Core/ValidateIngressStep.cs index f4709fb61..78c4b5568 100644 --- a/src/Microsoft.Tye.Core/ValidateIngressStep.cs +++ b/src/Microsoft.Tye.Core/ValidateIngressStep.cs @@ -122,7 +122,7 @@ public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder 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 @@ public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder 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); diff --git a/src/Microsoft.Tye.Extensions.Configuration/TyeConfigurationExtensions.cs b/src/Microsoft.Tye.Extensions.Configuration/TyeConfigurationExtensions.cs index 284d8a337..2b6d68af1 100644 --- a/src/Microsoft.Tye.Extensions.Configuration/TyeConfigurationExtensions.cs +++ b/src/Microsoft.Tye.Extensions.Configuration/TyeConfigurationExtensions.cs @@ -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 @@ public static class TyeConfigurationExtensions return null; } + if (IPAddress.TryParse(host, out IPAddress address) && address.AddressFamily == AddressFamily.InterNetworkV6) + { + host = "[" + host + "]"; + } + return new Uri(protocol + "://" + host + ":" + port + "/"); } diff --git a/src/Microsoft.Tye.Hosting/DockerImagePuller.cs b/src/Microsoft.Tye.Hosting/DockerImagePuller.cs index 369d7ef2f..b6eea8265 100644 --- a/src/Microsoft.Tye.Hosting/DockerImagePuller.cs +++ b/src/Microsoft.Tye.Hosting/DockerImagePuller.cs @@ -36,18 +36,11 @@ public async Task StartAsync(Application application) return; } - if (!await DockerDetector.Instance.IsDockerInstalled.Value) + if (!DockerDetector.Instance.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]; diff --git a/src/Microsoft.Tye.Hosting/DockerRunner.cs b/src/Microsoft.Tye.Hosting/DockerRunner.cs index d6e2ecbd7..cf28efa8d 100644 --- a/src/Microsoft.Tye.Hosting/DockerRunner.cs +++ b/src/Microsoft.Tye.Hosting/DockerRunner.cs @@ -148,17 +148,12 @@ service.Description.RunInfo is IngressRunInfo || // 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) @@ -199,25 +194,21 @@ public async Task StopAsync(Application application) } } - 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 = DockerDetector.Instance.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 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont 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 @@ 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 ?? ""}"; + 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) { @@ -347,21 +346,6 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont _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) diff --git a/src/Microsoft.Tye.Hosting/Model/DockerVolume.cs b/src/Microsoft.Tye.Hosting/Model/DockerVolume.cs index 532678ca0..fce0131a2 100644 --- a/src/Microsoft.Tye.Hosting/Model/DockerVolume.cs +++ b/src/Microsoft.Tye.Hosting/Model/DockerVolume.cs @@ -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; } } } diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index 706cfadcc..8dc953446 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -217,15 +217,10 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? 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"}://{DockerDetector.Instance.AspNetUrlsHost}:{p.Port}")); // Set the HTTPS port for the redirect middleware foreach (var p in ports) diff --git a/src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs b/src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs index 528614812..99225ba30 100644 --- a/src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs +++ b/src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs @@ -87,10 +87,12 @@ private async Task TransformProjectToContainer(Service service, ProjectRunInfo p // 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 @@ private async Task TransformProjectToContainer(Service service, ProjectRunInfo p 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 diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index 873b8b9e5..46a92da55 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -9,6 +9,7 @@ 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; @@ -771,6 +772,14 @@ public async Task IngressStaticFilesTest() [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")); diff --git a/test/Test.Infrastructure/DockerAssert.cs b/test/Test.Infrastructure/DockerAssert.cs index eb80d332e..c53bc57cb 100644 --- a/test/Test.Infrastructure/DockerAssert.cs +++ b/test/Test.Infrastructure/DockerAssert.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Xunit.Abstractions; using Xunit.Sdk; +using Microsoft.Tye; namespace Test.Infrastructure { @@ -22,7 +23,7 @@ public static async Task AssertImageExistsAsync(ITestOutputHelper output, string var builder = new StringBuilder(); output.WriteLine($"> docker images \"{repository}\" --format \"{{{{.Repository}}}}\""); - var exitCode = await Process.ExecuteAsync( + var exitCode = await ProcessUtil.ExecuteAsync( "docker", $"images \"{repository}\" --format \"{{{{.Repository}}}}\"", stdOut: OnOutput, @@ -33,12 +34,13 @@ public static async Task AssertImageExistsAsync(ITestOutputHelper output, string } 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,7 +59,7 @@ public static async Task DeleteDockerImagesAsync(ITestOutputHelper output, strin { output.WriteLine($"> docker rmi \"{id}\" --force"); - var exitCode = await Process.ExecuteAsync( + var exitCode = await ProcessUtil.ExecuteAsync( "docker", $"rmi \"{id}\" --force", stdOut: OnOutput, @@ -82,7 +84,7 @@ public static async Task GetRunningContainersIdsAsync(ITestOutputHelpe var builder = new StringBuilder(); output.WriteLine($"> docker ps --format \"{{{{.ID}}}}\""); - var exitCode = await Process.ExecuteAsync( + var exitCode = await ProcessUtil.ExecuteAsync( "docker", $"ps --format \"{{{{.ID}}}}\"", stdOut: OnOutput, @@ -110,7 +112,7 @@ private static async Task ListDockerImagesIdsAsync(ITestOutputHelper o var builder = new StringBuilder(); output.WriteLine($"> docker images -q \"{repository}\""); - var exitCode = await Process.ExecuteAsync( + var exitCode = await ProcessUtil.ExecuteAsync( "docker", $"images -q \"{repository}\"", stdOut: OnOutput, diff --git a/test/Test.Infrastructure/SkipIfDockerNotRunningAttribute.cs b/test/Test.Infrastructure/SkipIfDockerNotRunningAttribute.cs index 27c0639bb..9e088e063 100644 --- a/test/Test.Infrastructure/SkipIfDockerNotRunningAttribute.cs +++ b/test/Test.Infrastructure/SkipIfDockerNotRunningAttribute.cs @@ -14,8 +14,8 @@ public class SkipIfDockerNotRunningAttribute : Attribute, ITestCondition 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 = DockerDetector.Instance.IsUsable(out string unusableReason) && !(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AGENT_OS"))); + SkipReason = $"Container engine not usable: {unusableReason}"; } public bool IsMet { get; } From 0db297b74ccfc73a8aac8806293dc36dc825d657 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Wed, 28 Apr 2021 15:15:55 +0200 Subject: [PATCH 2/4] Allow to choose podman/docker using tye.yaml 'engine' property --- src/Microsoft.Tye.Core/ApplicationBuilder.cs | 5 +- src/Microsoft.Tye.Core/ApplicationFactory.cs | 2 +- .../BuildDockerImageStep.cs | 2 +- .../ConfigModel/ConfigApplication.cs | 2 + .../ConfigModel/ContainerEngineType.cs | 12 ++ src/Microsoft.Tye.Core/ContainerEngine.cs | 185 ++++++++++++++++++ .../DockerContainerBuilder.cs | 6 +- src/Microsoft.Tye.Core/DockerDetector.cs | 125 ------------ src/Microsoft.Tye.Core/DockerPush.cs | 5 +- src/Microsoft.Tye.Core/ProcessUtil.cs | 11 -- src/Microsoft.Tye.Core/PushDockerImageStep.cs | 2 +- .../Serialization/ConfigApplicationParser.cs | 16 ++ .../DockerImagePuller.cs | 12 +- src/Microsoft.Tye.Hosting/DockerRunner.cs | 30 ++- .../Model/Application.cs | 5 +- src/Microsoft.Tye.Hosting/ProcessRunner.cs | 2 +- src/schema/tye-schema.json | 5 + src/tye/ApplicationBuilderExtensions.cs | 2 +- test/E2ETest/TyeRunTests.cs | 6 +- test/Test.Infrastructure/DockerAssert.cs | 12 +- .../SkipIfDockerNotRunningAttribute.cs | 3 +- test/Test.Infrastructure/TestHelpers.cs | 4 +- 22 files changed, 267 insertions(+), 187 deletions(-) create mode 100644 src/Microsoft.Tye.Core/ConfigModel/ContainerEngineType.cs create mode 100644 src/Microsoft.Tye.Core/ContainerEngine.cs delete mode 100644 src/Microsoft.Tye.Core/DockerDetector.cs diff --git a/src/Microsoft.Tye.Core/ApplicationBuilder.cs b/src/Microsoft.Tye.Core/ApplicationBuilder.cs index c14004925..1a1e2f5c0 100644 --- a/src/Microsoft.Tye.Core/ApplicationBuilder.cs +++ b/src/Microsoft.Tye.Core/ApplicationBuilder.cs @@ -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 @@ public ApplicationBuilder(FileInfo source, string name) public ContainerRegistry? Registry { get; set; } + public ContainerEngine ContainerEngine { get; set; } + public List Extensions { get; } = new List(); public List Services { get; } = new List(); diff --git a/src/Microsoft.Tye.Core/ApplicationFactory.cs b/src/Microsoft.Tye.Core/ApplicationFactory.cs index 476bdcb14..ac083f941 100644 --- a/src/Microsoft.Tye.Core/ApplicationFactory.cs +++ b/src/Microsoft.Tye.Core/ApplicationFactory.cs @@ -30,7 +30,7 @@ public static async Task 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 }; diff --git a/src/Microsoft.Tye.Core/BuildDockerImageStep.cs b/src/Microsoft.Tye.Core/BuildDockerImageStep.cs index 04b66ef16..42511e551 100644 --- a/src/Microsoft.Tye.Core/BuildDockerImageStep.cs +++ b/src/Microsoft.Tye.Core/BuildDockerImageStep.cs @@ -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}."); } diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs index 8c80683b8..c95854008 100644 --- a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs @@ -28,6 +28,8 @@ public class ConfigApplication public string? Registry { get; set; } + public ContainerEngineType? ContainerEngineType { get; set; } + public string? Network { get; set; } public List> Extensions { get; set; } = new List>(); diff --git a/src/Microsoft.Tye.Core/ConfigModel/ContainerEngineType.cs b/src/Microsoft.Tye.Core/ConfigModel/ContainerEngineType.cs new file mode 100644 index 000000000..ec7fce2f3 --- /dev/null +++ b/src/Microsoft.Tye.Core/ConfigModel/ContainerEngineType.cs @@ -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 + } +} diff --git a/src/Microsoft.Tye.Core/ContainerEngine.cs b/src/Microsoft.Tye.Core/ContainerEngine.cs new file mode 100644 index 000000000..35c87b49a --- /dev/null +++ b/src/Microsoft.Tye.Core/ContainerEngine.cs @@ -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 ExecuteAsync( + string args, + string? workingDir = null, + Action? stdOut = null, + Action? stdErr = null, + params (string key, string value)[] environmentVariables) + => ProcessUtil.ExecuteAsync(CommandName, args, workingDir, stdOut, stdErr, environmentVariables); + + public Task RunAsync( + string arguments, + string? workingDirectory = null, + bool throwOnError = true, + IDictionary? environmentVariables = null, + Action? outputDataReceived = null, + Action? errorDataReceived = null, + Action? onStart = null, + Action? 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(); + // 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; + } + } +} diff --git a/src/Microsoft.Tye.Core/DockerContainerBuilder.cs b/src/Microsoft.Tye.Core/DockerContainerBuilder.cs index 80fca6257..4c5351975 100644 --- a/src/Microsoft.Tye.Core/DockerContainerBuilder.cs +++ b/src/Microsoft.Tye.Core/DockerContainerBuilder.cs @@ -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, @@ -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, diff --git a/src/Microsoft.Tye.Core/DockerDetector.cs b/src/Microsoft.Tye.Core/DockerDetector.cs deleted file mode 100644 index 954cbe631..000000000 --- a/src/Microsoft.Tye.Core/DockerDetector.cs +++ /dev/null @@ -1,125 +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.IO; -using System.Linq; -using System.Net; -using System.Runtime.InteropServices; -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 bool _isUsable { get; } - private string? _unusableReason { get; } - - public bool IsPodman { get; } - public string AspNetUrlsHost { get; } - public string? ContainerHost { get; } - - public bool IsUsable(out string? unusableReason) - { - unusableReason = _unusableReason; - return _isUsable; - } - - private DockerDetector() - { - AspNetUrlsHost = "localhost"; - try - { - ProcessResult result; - try - { - // try to use podman. - result = ProcessUtil.RunAsync("podman", "version -f \"{{ .Client.Version }}\"", throwOnError: false, cancellationToken: new CancellationTokenSource(Timeout).Token).Result; - IsPodman = true; - - if (result.ExitCode != 0) - { - _unusableReason = $"podman version exited with {result.ExitCode}. Standard error: \"{result.StandardError}\"."; - return; - } - - if (!Version.TryParse(result.StandardOutput, out Version? version)) - { - _unusableReason = $"cannot parse podman version '{result.StandardOutput}'."; - return; - } - Version minVersion = new Version(3, 1); - if (version < minVersion) - { - _unusableReason = $"podman version '{result.StandardOutput}' is less than the required '{minVersion}'."; - return; - } - - // 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(); - // 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"; - } - } - catch (Exception) - { - // try to use docker. - - 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"; - } - - result = ProcessUtil.RunAsync("docker", "version", throwOnError: false, cancellationToken: new CancellationTokenSource(Timeout).Token).Result; - - if (result.ExitCode != 0) - { - _unusableReason = "docker is not connected."; - return; - } - } - - _isUsable = true; - } - catch (Exception) - { - _unusableReason = "docker is not installed."; - } - } - } -} diff --git a/src/Microsoft.Tye.Core/DockerPush.cs b/src/Microsoft.Tye.Core/DockerPush.cs index 1c4dcb8f4..e1897de37 100644 --- a/src/Microsoft.Tye.Core/DockerPush.cs +++ b/src/Microsoft.Tye.Core/DockerPush.cs @@ -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 @@ 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 ProcessUtil.ExecuteAsync( - $"docker", + var exitCode = await containerEngine.ExecuteAsync( $"push {imageName}:{imageTag}", stdOut: capture.StdOut, stdErr: capture.StdErr); diff --git a/src/Microsoft.Tye.Core/ProcessUtil.cs b/src/Microsoft.Tye.Core/ProcessUtil.cs index 4cceb831f..df1c83550 100644 --- a/src/Microsoft.Tye.Core/ProcessUtil.cs +++ b/src/Microsoft.Tye.Core/ProcessUtil.cs @@ -31,7 +31,6 @@ public static Task ExecuteAsync( Action? stdErr = null, params (string key, string value)[] environmentVariables) { - command = CheckForPodman(command); return System.CommandLine.Invocation.Process.ExecuteAsync(command, args, workingDir, stdOut, stdErr, environmentVariables); } @@ -47,7 +46,6 @@ public static async Task RunAsync( Action? onStop = null, CancellationToken cancellationToken = default) { - filename = CheckForPodman(filename); using var process = new Process() { StartInfo = @@ -188,14 +186,5 @@ public static void KillProcess(int pid) catch (ArgumentException) { } catch (InvalidOperationException) { } } - - private static string CheckForPodman(string command) - { - if (command == "docker" && DockerDetector.Instance.IsPodman) - { - return "podman"; - } - return command; - } } } diff --git a/src/Microsoft.Tye.Core/PushDockerImageStep.cs b/src/Microsoft.Tye.Core/PushDockerImageStep.cs index a093a9fdf..558ccd654 100644 --- a/src/Microsoft.Tye.Core/PushDockerImageStep.cs +++ b/src/Microsoft.Tye.Core/PushDockerImageStep.cs @@ -27,7 +27,7 @@ public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder foreach (var image in service.Outputs.OfType()) { - 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}'"); } } diff --git a/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs b/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs index c9a1038c3..09cb8e32d 100644 --- a/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs +++ b/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs @@ -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 @@ public static void HandleConfigApplication(YamlMappingNode yamlMappingNode, Conf case "registry": app.Registry = YamlParser.GetScalarValue(key, child.Value); break; + case "engine": + 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); diff --git a/src/Microsoft.Tye.Hosting/DockerImagePuller.cs b/src/Microsoft.Tye.Hosting/DockerImagePuller.cs index b6eea8265..5305c016d 100644 --- a/src/Microsoft.Tye.Hosting/DockerImagePuller.cs +++ b/src/Microsoft.Tye.Hosting/DockerImagePuller.cs @@ -36,7 +36,7 @@ public async Task StartAsync(Application application) return; } - if (!DockerDetector.Instance.IsUsable(out string? unusableReason)) + if (!application.ContainerEngine.IsUsable(out string? unusableReason)) { _logger.LogError($"Unable to pull image: {unusableReason}."); @@ -47,18 +47,17 @@ public async Task StartAsync(Application application) 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); @@ -78,8 +77,7 @@ private async Task PullContainerAsync(string image) _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), diff --git a/src/Microsoft.Tye.Hosting/DockerRunner.cs b/src/Microsoft.Tye.Hosting/DockerRunner.cs index cf28efa8d..0c5969c65 100644 --- a/src/Microsoft.Tye.Hosting/DockerRunner.cs +++ b/src/Microsoft.Tye.Hosting/DockerRunner.cs @@ -33,7 +33,7 @@ public DockerRunner(ILogger logger, ReplicaRegistry replicaRegistry) public async Task StartAsync(Application application) { - await PurgeFromPreviousRun(); + await PurgeFromPreviousRun(application); var containers = new List(); @@ -100,7 +100,7 @@ service.Description.RunInfo is IngressRunInfo || 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 @@ service.Description.RunInfo is IngressRunInfo || _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) { @@ -190,7 +190,7 @@ public async Task StopAsync(Application application) _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); } } @@ -199,7 +199,7 @@ private void StartContainerAsync(Application application, Service service, Docke var serviceDescription = service.Description; var workingDirectory = docker.WorkingDirectory != null ? $"-w \"{docker.WorkingDirectory}\"" : ""; - var hostname = DockerDetector.Instance.ContainerHost; + var hostname = application.ContainerEngine.ContainerHost; if (hostname == null) { _logger.LogError("Configuration doesn't allow containers to access services on the host."); @@ -310,8 +310,7 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont status.DockerNetwork = dockerNetwork; WriteReplicaToStore(replica); - var result = await ProcessUtil.RunAsync( - "docker", + var result = await application.ContainerEngine.RunAsync( command, throwOnError: false, cancellationToken: cancellationToken, @@ -335,7 +334,7 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont 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; } @@ -353,7 +352,7 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont 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) { @@ -382,7 +381,7 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont 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, @@ -419,7 +418,7 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont _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) { @@ -435,7 +434,7 @@ async Task RunDockerContainer(IEnumerable<(int ExternalPort, int Port, int? Cont _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) { @@ -470,8 +469,7 @@ void Log(string data) 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, @@ -534,13 +532,13 @@ async Task BuildAndRunAsync(CancellationToken cancellationToken) 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); } diff --git a/src/Microsoft.Tye.Hosting/Model/Application.cs b/src/Microsoft.Tye.Hosting/Model/Application.cs index 7430a0c3f..c91ade50e 100644 --- a/src/Microsoft.Tye.Hosting/Model/Application.cs +++ b/src/Microsoft.Tye.Hosting/Model/Application.cs @@ -12,17 +12,20 @@ namespace Microsoft.Tye.Hosting.Model { public class Application { - public Application(FileInfo source, Dictionary services) + public Application(FileInfo source, Dictionary 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 Services { get; } public Dictionary Items { get; } = new Dictionary(); diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index 8dc953446..2bec6e9ff 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -220,7 +220,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? // 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"}://{DockerDetector.Instance.AspNetUrlsHost}:{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) diff --git a/src/schema/tye-schema.json b/src/schema/tye-schema.json index 688bb2a7e..2222d5f8c 100644 --- a/src/schema/tye-schema.json +++ b/src/schema/tye-schema.json @@ -13,6 +13,11 @@ "description": "Dockerhub username or hostname of remote registry. Used for tagging images.", "type": "string" }, + "engine": { + "description": "Container engine.", + "type": "string", + "enum": ["docker", "podman"] + }, "namespace": { "description": "The Kubernetes namespace to use.", "type": "string" diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index 815accec0..3015564ec 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -213,7 +213,7 @@ public static Application ToHostingApplication(this ApplicationBuilder applicati 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) diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index 46a92da55..19c13e635 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -505,7 +505,7 @@ public async Task DockerNamedVolumeTest() }); // Delete the volume - await ProcessUtil.RunAsync("docker", $"volume rm {volumeName}"); + await ContainerEngine.Default.RunAsync($"volume rm {volumeName}"); } [ConditionalFact] @@ -522,7 +522,7 @@ public async Task DockerNetworkAssignmentTest() 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 { @@ -561,7 +561,7 @@ await RunHostingApplication( finally { // Delete the network - await ProcessUtil.RunAsync("docker", $"network rm {dockerNetwork}"); + await ContainerEngine.Default.RunAsync($"network rm {dockerNetwork}"); } } diff --git a/test/Test.Infrastructure/DockerAssert.cs b/test/Test.Infrastructure/DockerAssert.cs index c53bc57cb..5e75e48a5 100644 --- a/test/Test.Infrastructure/DockerAssert.cs +++ b/test/Test.Infrastructure/DockerAssert.cs @@ -23,8 +23,7 @@ public static async Task AssertImageExistsAsync(ITestOutputHelper output, string var builder = new StringBuilder(); output.WriteLine($"> docker images \"{repository}\" --format \"{{{{.Repository}}}}\""); - var exitCode = await ProcessUtil.ExecuteAsync( - "docker", + var exitCode = await ContainerEngine.Default.ExecuteAsync( $"images \"{repository}\" --format \"{{{{.Repository}}}}\"", stdOut: OnOutput, stdErr: OnOutput); @@ -59,8 +58,7 @@ public static async Task DeleteDockerImagesAsync(ITestOutputHelper output, strin { output.WriteLine($"> docker rmi \"{id}\" --force"); - var exitCode = await ProcessUtil.ExecuteAsync( - "docker", + var exitCode = await ContainerEngine.Default.ExecuteAsync( $"rmi \"{id}\" --force", stdOut: OnOutput, stdErr: OnOutput); @@ -84,8 +82,7 @@ public static async Task GetRunningContainersIdsAsync(ITestOutputHelpe var builder = new StringBuilder(); output.WriteLine($"> docker ps --format \"{{{{.ID}}}}\""); - var exitCode = await ProcessUtil.ExecuteAsync( - "docker", + var exitCode = await ContainerEngine.Default.ExecuteAsync( $"ps --format \"{{{{.ID}}}}\"", stdOut: OnOutput, stdErr: OnOutput); @@ -112,8 +109,7 @@ private static async Task ListDockerImagesIdsAsync(ITestOutputHelper o var builder = new StringBuilder(); output.WriteLine($"> docker images -q \"{repository}\""); - var exitCode = await ProcessUtil.ExecuteAsync( - "docker", + var exitCode = await ContainerEngine.Default.ExecuteAsync( $"images -q \"{repository}\"", stdOut: OnOutput, stdErr: OnOutput); diff --git a/test/Test.Infrastructure/SkipIfDockerNotRunningAttribute.cs b/test/Test.Infrastructure/SkipIfDockerNotRunningAttribute.cs index 9e088e063..80b6a13a4 100644 --- a/test/Test.Infrastructure/SkipIfDockerNotRunningAttribute.cs +++ b/test/Test.Infrastructure/SkipIfDockerNotRunningAttribute.cs @@ -5,6 +5,7 @@ using System; using System.Runtime.InteropServices; using Microsoft.Tye; +using Microsoft.Tye.ConfigModel; namespace Test.Infrastructure { @@ -14,7 +15,7 @@ public class SkipIfDockerNotRunningAttribute : Attribute, ITestCondition public SkipIfDockerNotRunningAttribute() { // TODO Check performance of this. - IsMet = DockerDetector.Instance.IsUsable(out string unusableReason) && !(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AGENT_OS"))); + IsMet = ContainerEngine.Default.IsUsable(out string unusableReason) && !(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AGENT_OS"))); SkipReason = $"Container engine not usable: {unusableReason}"; } diff --git a/test/Test.Infrastructure/TestHelpers.cs b/test/Test.Infrastructure/TestHelpers.cs index 0ae89e2c7..df520f1bb 100644 --- a/test/Test.Infrastructure/TestHelpers.cs +++ b/test/Test.Infrastructure/TestHelpers.cs @@ -251,8 +251,8 @@ static async Task Purge(TyeHost host) 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())); - await dockerRunner.StartAsync(new Application(new FileInfo(host.Application.Source), new Dictionary())); + await processRunner.StartAsync(new Application(new FileInfo(host.Application.Source), new Dictionary(), ContainerEngine.Default)); + await dockerRunner.StartAsync(new Application(new FileInfo(host.Application.Source), new Dictionary(), ContainerEngine.Default)); } await DoOperationAndWaitForReplicasToChangeState(host, ReplicaState.Stopped, replicas.Length, replicas.ToHashSet(), new HashSet(), TimeSpan.Zero, Purge); From 3f6e0b87afc140592453b419ef3b442ab3e70c36 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Thu, 29 Apr 2021 08:39:21 +0200 Subject: [PATCH 3/4] Rename tye.yaml engine to containerEngine --- src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs | 2 +- src/schema/tye-schema.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs b/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs index 09cb8e32d..7b1f1d5c6 100644 --- a/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs +++ b/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs @@ -30,7 +30,7 @@ public static void HandleConfigApplication(YamlMappingNode yamlMappingNode, Conf case "registry": app.Registry = YamlParser.GetScalarValue(key, child.Value); break; - case "engine": + case "containerEngine": string engine = YamlParser.GetScalarValue(key, child.Value); if (engine.Equals("docker", StringComparison.InvariantCultureIgnoreCase)) { diff --git a/src/schema/tye-schema.json b/src/schema/tye-schema.json index 2222d5f8c..65060d74f 100644 --- a/src/schema/tye-schema.json +++ b/src/schema/tye-schema.json @@ -13,7 +13,7 @@ "description": "Dockerhub username or hostname of remote registry. Used for tagging images.", "type": "string" }, - "engine": { + "containerEngine": { "description": "Container engine.", "type": "string", "enum": ["docker", "podman"] From 7da91cd0e912f80bae2ea232115a0480a0c54d05 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Thu, 29 Apr 2021 08:42:25 +0200 Subject: [PATCH 4/4] ContainerEngine: be permissive when we're not able to parse the podman version. --- src/Microsoft.Tye.Core/ContainerEngine.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Tye.Core/ContainerEngine.cs b/src/Microsoft.Tye.Core/ContainerEngine.cs index 35c87b49a..2351a3805 100644 --- a/src/Microsoft.Tye.Core/ContainerEngine.cs +++ b/src/Microsoft.Tye.Core/ContainerEngine.cs @@ -108,13 +108,9 @@ private static bool TryUsePodman(ref string? unusableReason, ref string? contain 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) + if (Version.TryParse(result.StandardOutput, out Version? version) && + version < minVersion) { unusableReason = $"podman version '{result.StandardOutput}' is less than the required '{minVersion}'."; return true;