From 69070743e878194da4778e944be3c485245a83ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igr=20Alex=C3=A1nder=20Fern=C3=A1ndez=20Sa=C3=BAco?= Date: Thu, 2 Apr 2020 12:03:54 -0400 Subject: [PATCH] Network configuration for Docker services and usage of existing networks (#287) * Add configuration option to enable the containers usage of an existing network * Use IsNullOrEmpty instead IsNullOrWhiteSpace to check the value of Application.Network * Add tests for network assignment * Remove network-test assent and use frontend-backend for network tests * Improve logs in DockerRunner * Fix whitespaces * Fix whitespace formatting * Fix format with dotnet format * Add assertions to make sure the app working on network tests --- src/Microsoft.Tye.Core/ApplicationBuilder.cs | 2 + src/Microsoft.Tye.Core/ApplicationFactory.cs | 2 + .../ConfigModel/ConfigApplication.cs | 2 + src/Microsoft.Tye.Hosting/DockerRunner.cs | 28 ++++- .../Model/Application.cs | 2 + src/tye/ApplicationBuilderExtensions.cs | 2 +- test/E2ETest/TyeRunTests.cs | 112 +++++++++++++++++- 7 files changed, 146 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Tye.Core/ApplicationBuilder.cs b/src/Microsoft.Tye.Core/ApplicationBuilder.cs index b636c5f3..d3d9ffc6 100644 --- a/src/Microsoft.Tye.Core/ApplicationBuilder.cs +++ b/src/Microsoft.Tye.Core/ApplicationBuilder.cs @@ -26,5 +26,7 @@ namespace Microsoft.Tye public List Services { get; } = new List(); public List Ingress { get; } = new List(); + + public string? Network { get; set; } } } diff --git a/src/Microsoft.Tye.Core/ApplicationFactory.cs b/src/Microsoft.Tye.Core/ApplicationFactory.cs index 053ed4f3..6cb67e67 100644 --- a/src/Microsoft.Tye.Core/ApplicationFactory.cs +++ b/src/Microsoft.Tye.Core/ApplicationFactory.cs @@ -30,6 +30,8 @@ namespace Microsoft.Tye builder.Registry = new ContainerRegistry(config.Registry); } + builder.Network = config.Network; + foreach (var configExtension in config.Extensions) { var extension = new ExtensionConfiguration((string)configExtension["name"]); diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs index b40b0571..0e5a77f4 100644 --- a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs @@ -21,6 +21,8 @@ namespace Microsoft.Tye.ConfigModel public string? Registry { get; set; } + public string? Network { get; set; } + public List> Extensions { get; set; } = new List>(); public List Services { get; set; } = new List(); diff --git a/src/Microsoft.Tye.Hosting/DockerRunner.cs b/src/Microsoft.Tye.Hosting/DockerRunner.cs index acd5b234..021676f4 100644 --- a/src/Microsoft.Tye.Hosting/DockerRunner.cs +++ b/src/Microsoft.Tye.Hosting/DockerRunner.cs @@ -51,8 +51,32 @@ namespace Microsoft.Tye.Hosting string? dockerNetwork = null; + if (!string.IsNullOrEmpty(application.Network)) + { + var dockerNetworkResult = await ProcessUtil.RunAsync("docker", $"network ls --filter \"name={application.Network}\" --format \"{{{{.ID}}}}\""); + if (dockerNetworkResult.ExitCode != 0) + { + _logger.LogError("{Network}: Run docker network ls command failed", application.Network); + + throw new CommandException("Run docker network ls command failed"); + } + + 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); + + 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 (containers.Count > 1) + if (string.IsNullOrEmpty(dockerNetwork) && containers.Count > 1) { dockerNetwork = "tye_network_" + Guid.NewGuid().ToString().Substring(0, 10); @@ -94,7 +118,7 @@ namespace Microsoft.Tye.Hosting await Task.WhenAll(tasks); - if (application.Items.TryGetValue("dockerNetwork", out var dockerNetwork)) + if (string.IsNullOrEmpty(application.Network) && application.Items.TryGetValue("dockerNetwork", out var dockerNetwork)) { _logger.LogInformation("Removing docker network {Network}", dockerNetwork); diff --git a/src/Microsoft.Tye.Hosting/Model/Application.cs b/src/Microsoft.Tye.Hosting/Model/Application.cs index 42bb9f00..7469233d 100644 --- a/src/Microsoft.Tye.Hosting/Model/Application.cs +++ b/src/Microsoft.Tye.Hosting/Model/Application.cs @@ -26,6 +26,8 @@ namespace Microsoft.Tye.Hosting.Model public Dictionary Items { get; } = new Dictionary(); + public string? Network { get; set; } + public void PopulateEnvironment(Service service, Action set, string defaultHost = "localhost") { if (service.Description.Configuration != null) diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index 8201d276..5c5c6748 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -152,7 +152,7 @@ namespace Microsoft.Tye services.Add(ingress.Name, new Service(description)); } - return new Application(application.Source, services); + return new Application(application.Source, services) { 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 ba9b531e..f632f8d0 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -17,10 +17,12 @@ using Microsoft.Tye.Hosting.Model; using Microsoft.Tye.Hosting.Model.V1; using Xunit; using Xunit.Abstractions; + using static E2ETest.TestHelpers; namespace E2ETest { + public class TyeRunTests { private readonly ITestOutputHelper _output; @@ -276,6 +278,114 @@ namespace E2ETest await ProcessUtil.RunAsync("docker", $"volume rm {volumeName}"); } + [ConditionalFact] + [SkipIfDockerNotRunning] + public async Task DockerNetworkAssignmentTest() + { + var projectDirectory = new DirectoryInfo( + Path.Combine(GetSolutionRootDirectory("tye"), "samples", "frontend-backend")); + using var tempDirectory = TempDirectory.Create(true); + DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); + + var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + + var dockerNetwork = "tye_docker_network_test" + Guid.NewGuid().ToString().Substring(0, 10); + application.Network = dockerNetwork; + + // Create the existing network + await ProcessUtil.RunAsync("docker", $"network create {dockerNetwork}"); + + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true, + AllowAutoRedirect = false + }; + + var client = new HttpClient(new RetryHandler(handler)); + + try + { + await RunHostingApplication( + application, + new[] { "--docker" }, + async (app, uri) => + { + // Make sure we're running containers + Assert.True(app.Services.All(s => s.Value.Description.RunInfo is DockerRunInfo)); + + foreach (var serviceBuilder in application.Services) + { + var serviceUri = await GetServiceUrl(client, uri, serviceBuilder.Name); + var serviceResponse = await client.GetAsync(serviceUri); + Assert.True(serviceResponse.IsSuccessStatusCode); + + var serviceResult = + await client.GetStringAsync($"{uri}api/v1/services/{serviceBuilder.Name}"); + var service = JsonSerializer.Deserialize(serviceResult, _options); + + Assert.NotNull(service); + + Assert.Equal(dockerNetwork, service.Replicas.FirstOrDefault().Value.DockerNetwork); + } + }); + } + finally + { + // Delete the network + await ProcessUtil.RunAsync("docker", $"network rm {dockerNetwork}"); + } + } + + [ConditionalFact] + [SkipIfDockerNotRunning] + public async Task DockerNetworkAssignmentForNonExistingNetworkTest() + { + var projectDirectory = new DirectoryInfo( + Path.Combine(GetSolutionRootDirectory("tye"), "samples", "frontend-backend")); + using var tempDirectory = TempDirectory.Create(true); + DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); + + var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + + var dockerNetwork = "tye_docker_network_test" + Guid.NewGuid().ToString().Substring(0, 10); + application.Network = dockerNetwork; + + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true, + AllowAutoRedirect = false + }; + + var client = new HttpClient(new RetryHandler(handler)); + + await RunHostingApplication( + application, + new[] { "--docker" }, + async (app, uri) => + { + // Make sure we're running containers + Assert.True(app.Services.All(s => s.Value.Description.RunInfo is DockerRunInfo)); + + foreach (var serviceBuilder in application.Services) + { + var serviceUri = await GetServiceUrl(client, uri, serviceBuilder.Name); + var serviceResponse = await client.GetAsync(serviceUri); + Assert.True(serviceResponse.IsSuccessStatusCode); + + var serviceResult = await client.GetStringAsync($"{uri}api/v1/services/{serviceBuilder.Name}"); + var service = JsonSerializer.Deserialize(serviceResult, _options); + + Assert.NotNull(service); + + Assert.NotEqual(dockerNetwork, service.Replicas.FirstOrDefault().Value.DockerNetwork); + } + }); + } + [ConditionalFact] [SkipIfDockerNotRunning] public async Task DockerHostVolumeTest() @@ -287,7 +397,7 @@ namespace E2ETest var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); // Add a volume - var project = ((ProjectServiceBuilder)application.Services[0]); + var project = (ProjectServiceBuilder)application.Services[0]; using var tempDir = TempDirectory.Create(preferUserDirectoryOnMacOS: true);