Browse Source

Add support for container networking (#278)

* Add support for container networking
- This adds all containers in the tye.yaml to the same container network.
- Container networking only works with a single replica today since the containers are named after their replicas.
- Use the container host name and port when injecting env variables. This handles the replica case by falling back to the host ip and port.
- This cleans up container to container communication when migrating docker-compose files.
- Don't override assign container port if already set.
- Remove StopAsync from TyeHost and exposed DisposeAsync. This patterns removes common clean up and hanging issues that occur.
- Cleaned up run tests to use similar code to run, clean up and capture logs.

* Move where we handle errors while shutting down the host

* Fixed project -> container networking issue
- Only use service name as the host name if both target and source re containers

* Formatting...

* Add service definition to the logs

* Added env variables to disable console colors for process run

* Hit the backend before the frontend

* Small cleanup
- Update docs to use host name for redis cli
- Show docker network and network alias in the API
pull/286/head
David Fowler 6 years ago
committed by GitHub
parent
commit
69118b7890
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      docs/redis.md
  2. 21
      samples/mongo-sample/tye.yaml
  3. 8
      samples/redis/tye.yaml
  4. 4
      src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor
  5. 81
      src/Microsoft.Tye.Hosting/DockerRunner.cs
  6. 12
      src/Microsoft.Tye.Hosting/HttpProxyService.cs
  7. 24
      src/Microsoft.Tye.Hosting/Model/Application.cs
  8. 4
      src/Microsoft.Tye.Hosting/Model/DockerStatus.cs
  9. 2
      src/Microsoft.Tye.Hosting/Model/V1/V1ReplicaStatus.cs
  10. 5
      src/Microsoft.Tye.Hosting/PortAssigner.cs
  11. 3
      src/Microsoft.Tye.Hosting/ProcessRunner.cs
  12. 2
      src/Microsoft.Tye.Hosting/TyeDashboardApi.cs
  13. 49
      src/Microsoft.Tye.Hosting/TyeHost.cs
  14. 2
      src/tye/Program.RunCommand.cs
  15. 5
      test/E2ETest/TestHelpers.cs
  16. 74
      test/E2ETest/TyePurgeTests.cs
  17. 339
      test/E2ETest/TyeRunTests.cs

2
docs/redis.md

@ -95,7 +95,7 @@ We just showed how `tye` makes it easier to communicate between 2 applications r
- port: 6379
- name: redis-cli
image: redis
args: "redis-cli -h host.docker.internal MONITOR"
args: "redis-cli -h redis MONITOR"
```
We've added 2 services to the `tye.yaml` file. The `redis` service itself and a `redis-cli` service that we will use to watch the data being sent to and retrieved from redis.

21
samples/mongo-sample/tye.yaml

@ -0,0 +1,21 @@
services:
- name: mongo
image: mongo
env:
- name: ME_CONFIG_MONGODB_ADMINUSERNAME
value: root
- name: ME_CONFIG_MONGODB_ADMINPASSWORD
value: example
bindings:
- port: 27017
- name: mongo-express
image: mongo-express
bindings:
- port: 8081
containerPort: 8081
protocol: http
env:
- name: ME_CONFIG_MONGODB_ADMINUSERNAME
value: root
- name: ME_CONFIG_MONGODB_ADMINPASSWORD
value: example

8
samples/redis/tye.yaml

@ -0,0 +1,8 @@
services:
- name: redis
image: redis
bindings:
- port: 6379
- name: redis-cli
image: redis
args: "redis-cli -h redis MONITOR"

4
src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor

@ -42,7 +42,7 @@
{
if (b.Port != null)
{
if (b.Protocol == null || b.Protocol == "http" || b.Protocol == "https")
if (b.Protocol == "http" || b.Protocol == "https")
{
var url = GetUrl(b);
<span><a href="@url" target="_blank">@url</a></span>
@ -79,7 +79,7 @@
string GetUrl(ServiceBinding b)
{
return $"{(b.Protocol ?? "http")}://{b.Host ?? "localhost"}:{b.Port}";
return $"{(b.Protocol ?? "tcp")}://{b.Host ?? "localhost"}:{b.Port}";
}
protected override void OnInitialized()

81
src/Microsoft.Tye.Hosting/DockerRunner.cs

@ -34,17 +34,53 @@ namespace Microsoft.Tye.Hosting
{
await PurgeFromPreviousRun();
var tasks = new Task[application.Services.Count];
var index = 0;
var containers = new List<Service>();
foreach (var s in application.Services)
{
tasks[index++] = s.Value.Description.RunInfo is DockerRunInfo docker ? StartContainerAsync(application, s.Value, docker) : Task.CompletedTask;
if (s.Value.Description.RunInfo is DockerRunInfo)
{
containers.Add(s.Value);
}
}
if (containers.Count == 0)
{
return;
}
string? dockerNetwork = 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)
{
dockerNetwork = "tye_network_" + Guid.NewGuid().ToString().Substring(0, 10);
application.Items["dockerNetwork"] = dockerNetwork;
_logger.LogInformation("Creating docker network {Network}", dockerNetwork);
var command = $"network create --driver bridge {dockerNetwork}";
_logger.LogInformation("Running docker command {Command}", command);
await ProcessUtil.RunAsync("docker", command);
}
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);
}
await Task.WhenAll(tasks);
}
public Task StopAsync(Application application)
public async Task StopAsync(Application application)
{
var services = application.Services;
@ -56,10 +92,22 @@ namespace Microsoft.Tye.Hosting
tasks[index++] = StopContainerAsync(state);
}
return Task.WhenAll(tasks);
await Task.WhenAll(tasks);
if (application.Items.TryGetValue("dockerNetwork", out var dockerNetwork))
{
_logger.LogInformation("Removing docker network {Network}", dockerNetwork);
var command = $"network rm {dockerNetwork}";
_logger.LogInformation("Running docker command {Command}", command);
// Clean up the network we created
await ProcessUtil.RunAsync("docker", command, throwOnError: false);
}
}
private async Task StartContainerAsync(Application application, Service service, DockerRunInfo docker)
private async Task StartContainerAsync(Application application, Service service, DockerRunInfo docker, string? dockerNetwork)
{
var serviceDescription = service.Description;
var environmentArguments = "";
@ -167,9 +215,10 @@ namespace Microsoft.Tye.Hosting
var command = $"run -d {workingDirectory} {volumes} {environmentArguments} {portString} --name {replica} --restart=unless-stopped {docker.Image} {docker.Args ?? ""}";
_logger.LogInformation("Running docker command {Command}", command);
service.Logs.OnNext($"[{replica}]: {command}");
service.Logs.OnNext($"[{replica}]: docker {command}");
status.DockerCommand = command;
status.DockerNetwork = dockerNetwork;
WriteReplicaToStore(replica);
var result = await ProcessUtil.RunAsync(
@ -208,6 +257,24 @@ namespace Microsoft.Tye.Hosting
_logger.LogInformation("Running container {ContainerName} with ID {ContainerId}", replica, shortContainerId);
if (!string.IsNullOrEmpty(dockerNetwork))
{
// If this is the only replica then the network alias is the service name
var alias = serviceDescription.Replicas == 1 ? serviceDescription.Name : replica;
status.DockerNetworkAlias = alias;
var networkCommand = $"network connect {dockerNetwork} {replica} --alias {alias}";
service.Logs.OnNext($"[{replica}]: docker {networkCommand}");
_logger.LogInformation("Running docker command {Command}", networkCommand);
result = await ProcessUtil.RunAsync("docker", networkCommand);
PrintStdOutAndErr(service, replica, result);
}
service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, status));
_logger.LogInformation("Collecting docker logs for {ContainerName}.", replica);

12
src/Microsoft.Tye.Hosting/HttpProxyService.cs

@ -154,12 +154,14 @@ namespace Microsoft.Tye.Hosting
conventions.WithDisplayName(rule.Service);
}
}
}
foreach (var app in _webApplications)
{
await app.StartAsync();
await webApp.StartAsync();
foreach (var replica in service.Replicas)
{
service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, replica.Value));
}
}
}
}

24
src/Microsoft.Tye.Hosting/Model/Application.cs

@ -24,6 +24,8 @@ namespace Microsoft.Tye.Hosting.Model
public Dictionary<string, Service> Services { get; }
public Dictionary<object, object> Items { get; } = new Dictionary<object, object>();
public void PopulateEnvironment(Service service, Action<string, string> set, string defaultHost = "localhost")
{
if (service.Description.Configuration != null)
@ -76,8 +78,9 @@ namespace Microsoft.Tye.Hosting.Model
throw new InvalidOperationException($"Unable to resolve the desired value '{source.Kind}' from binding '{source.Binding}' for service '{source.Service}'.");
}
void SetBinding(string serviceName, ServiceBinding b)
void SetBinding(ServiceDescription targetService, ServiceBinding b)
{
var serviceName = targetService.Name.ToUpper();
var configName = "";
var envName = "";
@ -107,12 +110,21 @@ namespace Microsoft.Tye.Hosting.Model
if (b.Port != null)
{
set($"SERVICE__{configName}__PORT", b.Port.Value.ToString());
set($"{envName}_SERVICE_PORT", b.Port.Value.ToString());
var port = (service.Description.RunInfo is DockerRunInfo &&
targetService.RunInfo is DockerRunInfo &&
targetService.Replicas == 1) ? b.ContainerPort ?? b.Port.Value : b.Port.Value;
set($"SERVICE__{configName}__PORT", port.ToString());
set($"{envName}_SERVICE_PORT", port.ToString());
}
set($"SERVICE__{configName}__HOST", b.Host ?? defaultHost);
set($"{envName}_SERVICE_HOST", b.Host ?? defaultHost);
// Use the container name as the host name if there's a single replica (current limitation)
var host = b.Host ?? (service.Description.RunInfo is DockerRunInfo &&
targetService.RunInfo is DockerRunInfo &&
targetService.Replicas == 1 ? targetService.Name : defaultHost);
set($"SERVICE__{configName}__HOST", host);
set($"{envName}_SERVICE_HOST", host);
}
// Inject dependency information
@ -120,7 +132,7 @@ namespace Microsoft.Tye.Hosting.Model
{
foreach (var b in s.Description.Bindings)
{
SetBinding(s.Description.Name.ToUpper(), b);
SetBinding(s.Description, b);
}
}
}

4
src/Microsoft.Tye.Hosting/Model/DockerStatus.cs

@ -12,6 +12,10 @@ namespace Microsoft.Tye.Hosting.Model
public string? DockerCommand { get; set; }
public string? DockerNetwork { get; set; }
public string? DockerNetworkAlias { get; set; }
public string? ContainerId { get; set; }
}
}

2
src/Microsoft.Tye.Hosting/Model/V1/V1ReplicaStatus.cs

@ -11,6 +11,8 @@ namespace Microsoft.Tye.Hosting.Model.V1
{
public string? DockerCommand { get; set; }
public string? ContainerId { get; set; }
public string? DockerNetwork { get; set; }
public string? DockerNetworkAlias { get; set; }
public string? Name { get; set; }
public IEnumerable<int>? Ports { get; set; }
public int? ExitCode { get; set; }

5
src/Microsoft.Tye.Hosting/PortAssigner.cs

@ -84,14 +84,13 @@ namespace Microsoft.Tye.Hosting
// Default the first http and https port to 80 and 443
if (httpBinding != null)
{
httpBinding.ContainerPort = 80;
httpBinding.ContainerPort ??= 80;
}
if (httpsBinding != null)
{
httpsBinding.ContainerPort = 443;
httpsBinding.ContainerPort ??= 443;
}
}
return Task.CompletedTask;

3
src/Microsoft.Tye.Hosting/ProcessRunner.cs

@ -129,6 +129,9 @@ namespace Microsoft.Tye.Hosting
// Default to development environment
["DOTNET_ENVIRONMENT"] = "Development",
["ASPNETCORE_ENVIRONMENT"] = "Development",
// Remove the color codes from the console output
["DOTNET_LOGGING__CONSOLE__DISABLECOLORS"] = "true",
["ASPNETCORE_LOGGING__CONSOLE__DISABLECOLORS"] = "true"
};
// Set up environment variables to use the version of dotnet we're using to run

2
src/Microsoft.Tye.Hosting/TyeDashboardApi.cs

@ -185,6 +185,8 @@ namespace Microsoft.Tye.Hosting
{
replicaStatus.DockerCommand = dockerStatus.DockerCommand;
replicaStatus.ContainerId = dockerStatus.ContainerId;
replicaStatus.DockerNetwork = dockerStatus.DockerNetwork;
replicaStatus.DockerNetworkAlias = dockerStatus.DockerNetworkAlias;
}
}

49
src/Microsoft.Tye.Hosting/TyeHost.cs

@ -24,7 +24,7 @@ using Serilog.Filters;
namespace Microsoft.Tye.Hosting
{
public class TyeHost : IDisposable
public class TyeHost : IAsyncDisposable
{
private const int DefaultPort = 8000;
private const int AutodetectPort = 0;
@ -101,25 +101,6 @@ namespace Microsoft.Tye.Hosting
return app;
}
public async Task StopAsync()
{
try
{
if (_processor != null)
{
await _processor.StopAsync(_application);
}
}
finally
{
if (DashboardWebApplication != null)
{
// Stop the host after everything else has been shutdown
await DashboardWebApplication.StopAsync();
}
}
}
private static WebApplication BuildWebApplication(
Application application,
string[] args,
@ -292,8 +273,34 @@ namespace Microsoft.Tye.Hosting
return new AggregateApplicationProcessor(processors);
}
public void Dispose()
private async Task StopAsync()
{
try
{
if (_processor != null)
{
await _processor.StopAsync(_application);
}
_processor = null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while shutting down");
}
finally
{
if (DashboardWebApplication != null)
{
// Stop the host after everything else has been shutdown
await DashboardWebApplication.StopAsync();
}
}
}
public async ValueTask DisposeAsync()
{
await StopAsync();
_replicaRegistry?.Dispose();
DashboardWebApplication?.Dispose();
}

2
src/tye/Program.RunCommand.cs

@ -84,7 +84,7 @@ namespace Microsoft.Tye
throw new CommandException($"No services found in \"{application.Source.Name}\"");
}
using var host = new TyeHost(application.ToHostingApplication(), args, debug);
await using var host = new TyeHost(application.ToHostingApplication(), args, debug);
await host.RunAsync();
});

5
test/E2ETest/TestHelpers.cs

@ -109,11 +109,6 @@ namespace E2ETest
await startedTask.Task;
}
}
catch (TaskCanceledException)
{
await host.StopAsync();
throw;
}
finally
{
foreach (var observer in servicesStateObserver)

74
test/E2ETest/TyePurgeTests.cs

@ -49,27 +49,22 @@ namespace E2ETest
try
{
await TestHelpers.StartHostAndWaitForReplicasToStart(host);
try
{
var pids = GetAllAppPids(host.Application);
Assert.True(Directory.Exists(tyeDir.FullName));
Assert.Subset(new HashSet<int>(GetAllPids()), new HashSet<int>(pids));
await TestHelpers.PurgeHostAndWaitForGivenReplicasToStop(host,
GetAllReplicasNames(host.Application));
var runningPids = new HashSet<int>(GetAllPids());
Assert.True(pids.All(pid => !runningPids.Contains(pid)));
}
finally
{
await host.StopAsync();
}
var pids = GetAllAppPids(host.Application);
Assert.True(Directory.Exists(tyeDir.FullName));
Assert.Subset(new HashSet<int>(GetAllPids()), new HashSet<int>(pids));
await TestHelpers.PurgeHostAndWaitForGivenReplicasToStop(host,
GetAllReplicasNames(host.Application));
var runningPids = new HashSet<int>(GetAllPids());
Assert.True(pids.All(pid => !runningPids.Contains(pid)));
}
finally
{
host.Dispose();
await host.DisposeAsync();
Assert.False(Directory.Exists(tyeDir.FullName));
}
}
@ -94,33 +89,28 @@ namespace E2ETest
try
{
await TestHelpers.StartHostAndWaitForReplicasToStart(host);
try
{
var pids = GetAllAppPids(host.Application);
var containers = GetAllContainerIds(host.Application);
Assert.True(Directory.Exists(tyeDir.FullName));
Assert.Subset(new HashSet<int>(GetAllPids()), new HashSet<int>(pids));
Assert.Subset(new HashSet<string>(await DockerAssert.GetRunningContainersIdsAsync(_output)),
new HashSet<string>(containers));
await TestHelpers.PurgeHostAndWaitForGivenReplicasToStop(host,
GetAllReplicasNames(host.Application));
var runningPids = new HashSet<int>(GetAllPids());
Assert.True(pids.All(pid => !runningPids.Contains(pid)));
var runningContainers =
new HashSet<string>(await DockerAssert.GetRunningContainersIdsAsync(_output));
Assert.True(containers.All(c => !runningContainers.Contains(c)));
}
finally
{
await host.StopAsync();
}
var pids = GetAllAppPids(host.Application);
var containers = GetAllContainerIds(host.Application);
Assert.True(Directory.Exists(tyeDir.FullName));
Assert.Subset(new HashSet<int>(GetAllPids()), new HashSet<int>(pids));
Assert.Subset(new HashSet<string>(await DockerAssert.GetRunningContainersIdsAsync(_output)),
new HashSet<string>(containers));
await TestHelpers.PurgeHostAndWaitForGivenReplicasToStop(host,
GetAllReplicasNames(host.Application));
var runningPids = new HashSet<int>(GetAllPids());
Assert.True(pids.All(pid => !runningPids.Contains(pid)));
var runningContainers =
new HashSet<string>(await DockerAssert.GetRunningContainersIdsAsync(_output));
Assert.True(containers.All(c => !runningContainers.Contains(c)));
}
finally
{
host.Dispose();
await host.DisposeAsync();
Assert.False(Directory.Exists(tyeDir.FullName));
}
}

339
test/E2ETest/TyeRunTests.cs

@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.CommandLine.IO;
using System.IO;
using System.Linq;
using System.Net;
@ -11,7 +12,6 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Tye;
using Microsoft.Tye.ConfigModel;
using Microsoft.Tye.Hosting;
using Microsoft.Tye.Hosting.Model;
using Microsoft.Tye.Hosting.Model.V1;
@ -52,54 +52,23 @@ namespace E2ETest
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "test-project.csproj"));
var outputContext = new OutputContext(_sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
using var host = new TyeHost(application.ToHostingApplication(), Array.Empty<string>())
{
Sink = _sink,
};
await host.StartAsync();
try
var handler = new HttpClientHandler
{
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = false
};
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = false
};
var client = new HttpClient(new RetryHandler(handler));
var client = new HttpClient(new RetryHandler(handler));
// Make sure dashboard and applications are up.
// Dashboard should be hosted in same process.
var dashboardUri = new Uri(host.DashboardWebApplication!.Addresses.First());
var dashboardString = await client.GetStringAsync($"{dashboardUri}api/v1/services/test-project");
await RunHostingApplication(application, Array.Empty<string>(), async (app, uri) =>
{
var testUri = await GetServiceUrl(client, uri, "test-project");
var service = JsonSerializer.Deserialize<V1Service>(dashboardString, _options);
var binding = service.Description!.Bindings.Where(b => b.Protocol == "http").Single();
var uriBackendProcess = new Uri($"{binding.Protocol}://localhost:{binding.Port}");
var testResponse = await client.GetAsync(testUri);
// This isn't reliable right now because micronetes only guarantees the process starts, not that
// that kestrel started.
try
{
var appResponse = await client.GetAsync(uriBackendProcess);
Assert.Equal(HttpStatusCode.OK, appResponse.StatusCode);
}
finally
{
// If we failed, there's a good chance the service isn't running. Let's get the logs either way and put
// them in the output.
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(dashboardUri, $"/api/v1/logs/{service.Description.Name}"));
var response = await client.SendAsync(request);
var text = await response.Content.ReadAsStringAsync();
_output.WriteLine($"Logs for service: {service.Description.Name}");
_output.WriteLine(text);
}
}
finally
{
await host.StopAsync();
}
Assert.True(testResponse.IsSuccessStatusCode);
});
}
[Fact]
@ -112,31 +81,108 @@ namespace E2ETest
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var outputContext = new OutputContext(_sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
using var host = new TyeHost(application.ToHostingApplication(), Array.Empty<string>())
var handler = new HttpClientHandler
{
Sink = _sink,
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = false
};
await host.StartAsync();
try
var client = new HttpClient(new RetryHandler(handler));
await RunHostingApplication(application, Array.Empty<string>(), async (app, uri) =>
{
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = false
};
var frontendUri = await GetServiceUrl(client, uri, "frontend");
var backendUri = await GetServiceUrl(client, uri, "backend");
var client = new HttpClient(new RetryHandler(handler));
var backendResponse = await client.GetAsync(backendUri);
var frontendResponse = await client.GetAsync(frontendUri);
var dashboardUri = new Uri(host.DashboardWebApplication!.Addresses.First());
Assert.True(backendResponse.IsSuccessStatusCode);
Assert.True(frontendResponse.IsSuccessStatusCode);
});
}
await CheckServiceIsUp(host.Application, client, "backend", dashboardUri);
await CheckServiceIsUp(host.Application, client, "frontend", dashboardUri);
}
finally
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task FrontendBackendRunTestWithDocker()
{
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "frontend-backend"));
using var tempDirectory = TempDirectory.Create(preferUserDirectoryOnMacOS: 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 handler = new HttpClientHandler
{
await host.StopAsync();
}
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));
var frontendUri = await GetServiceUrl(client, uri, "frontend");
var backendUri = await GetServiceUrl(client, uri, "backend");
var backendResponse = await client.GetAsync(backendUri);
var frontendResponse = await client.GetAsync(frontendUri);
Assert.True(backendResponse.IsSuccessStatusCode);
Assert.True(frontendResponse.IsSuccessStatusCode);
});
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task FrontendProjectBackendDocker()
{
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "frontend-backend"));
using var tempDirectory = TempDirectory.Create(preferUserDirectoryOnMacOS: 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);
// Transform the backend into a docker image for testing
var project = (ProjectServiceBuilder)application.Services.First(s => s.Name == "backend");
application.Services.Remove(project);
var outputFileName = project.AssemblyName + ".dll";
var container = new ContainerServiceBuilder(project.Name, $"mcr.microsoft.com/dotnet/core/sdk:{project.TargetFrameworkVersion}");
container.Volumes.Add(new VolumeBuilder(project.PublishDir, name: null, target: "/app"));
container.Args = $"dotnet /app/{outputFileName} {project.Args}";
container.Bindings.AddRange(project.Bindings);
await ProcessUtil.RunAsync("dotnet", $"publish \"{project.ProjectFile.FullName}\" /nologo", outputDataReceived: _sink.WriteLine, errorDataReceived: _sink.WriteLine);
application.Services.Add(container);
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = false
};
var client = new HttpClient(new RetryHandler(handler));
await RunHostingApplication(application, Array.Empty<string>(), async (app, uri) =>
{
var frontendUri = await GetServiceUrl(client, uri, "frontend");
var backendUri = await GetServiceUrl(client, uri, "backend");
var backendResponse = await client.GetAsync(backendUri);
var frontendResponse = await client.GetAsync(frontendUri);
Assert.True(backendResponse.IsSuccessStatusCode);
Assert.True(frontendResponse.IsSuccessStatusCode);
});
}
[ConditionalFact]
@ -165,7 +211,7 @@ namespace E2ETest
var client = new HttpClient(new RetryHandler(handler));
var args = new[] { "--docker" };
await RunHostingApplication(application, args, _sink, async serviceApi =>
await RunHostingApplication(application, args, async (app, serviceApi) =>
{
var serviceUri = await GetServiceUrl(client, serviceApi, "volume-test");
@ -180,7 +226,7 @@ namespace E2ETest
Assert.Equal("Things saved to the volume!", await client.GetStringAsync(serviceUri));
});
await RunHostingApplication(application, args, _sink, async serviceApi =>
await RunHostingApplication(application, args, async (app, serviceApi) =>
{
var serviceUri = await GetServiceUrl(client, serviceApi, "volume-test");
@ -223,7 +269,7 @@ namespace E2ETest
var client = new HttpClient(new RetryHandler(handler));
var args = new[] { "--docker" };
await RunHostingApplication(application, args, _sink, async serviceApi =>
await RunHostingApplication(application, args, async (app, serviceApi) =>
{
var serviceUri = await GetServiceUrl(client, serviceApi, "volume-test");
@ -244,10 +290,6 @@ namespace E2ETest
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var outputContext = new OutputContext(_sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
using var host = new TyeHost(application.ToHostingApplication(), Array.Empty<string>())
{
Sink = _sink,
};
var handler = new HttpClientHandler
{
@ -255,13 +297,13 @@ namespace E2ETest
AllowAutoRedirect = false
};
using var client = new HttpClient(new RetryHandler(handler));
await host.StartAsync();
var serviceApi = new Uri(host.DashboardWebApplication!.Addresses.First());
var client = new HttpClient(new RetryHandler(handler));
try
await RunHostingApplication(application, Array.Empty<string>(), async (app, uri) =>
{
var ingressUri = await GetServiceUrl(client, serviceApi, "ingress");
using var client = new HttpClient();
var ingressUri = await GetServiceUrl(client, uri, "ingress");
var responseA = await client.GetAsync(ingressUri + "/A");
var responseB = await client.GetAsync(ingressUri + "/B");
@ -279,64 +321,7 @@ namespace E2ETest
Assert.StartsWith("Hello from Application A", await responseA.Content.ReadAsStringAsync());
Assert.StartsWith("Hello from Application B", await responseB.Content.ReadAsStringAsync());
}
finally
{
// If we failed, there's a good chance the service isn't running. Let's get the logs either way and put
// them in the output.
foreach (var s in host.Application.Services.Values)
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(serviceApi, $"/api/v1/logs/{s.Description.Name}"));
var response = await client.SendAsync(request);
var text = await response.Content.ReadAsStringAsync();
_output.WriteLine($"Logs for service: {s.Description.Name}");
_output.WriteLine(text);
}
await host.StopAsync();
}
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task FrontendBackendRunTestWithDocker()
{
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "frontend-backend"));
using var tempDirectory = TempDirectory.Create(preferUserDirectoryOnMacOS: 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);
using var host = new TyeHost(application.ToHostingApplication(), new[] { "--docker" })
{
Sink = _sink,
};
await host.StartAsync();
try
{
// Make sure we're running containers
Assert.True(host.Application.Services.All(s => s.Value.Description.RunInfo is DockerRunInfo));
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = false
};
var client = new HttpClient(new RetryHandler(handler));
var dashboardUri = new Uri(host.DashboardWebApplication!.Addresses.First());
await CheckServiceIsUp(host.Application, client, "backend", dashboardUri, timeout: TimeSpan.FromSeconds(60));
await CheckServiceIsUp(host.Application, client, "frontend", dashboardUri, timeout: TimeSpan.FromSeconds(60));
}
finally
{
await host.StopAsync();
}
});
}
[Fact]
@ -351,110 +336,58 @@ namespace E2ETest
// Debug targets can be null if not specified, so make sure calling host.Start does not throw.
var outputContext = new OutputContext(_sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
using var host = new TyeHost(application.ToHostingApplication(), Array.Empty<string>())
await using var host = new TyeHost(application.ToHostingApplication(), Array.Empty<string>())
{
Sink = _sink,
};
await host.StartAsync();
await host.StopAsync();
}
private async Task<string> GetServiceUrl(HttpClient client, Uri serviceApi, string serviceName)
private async Task<string> GetServiceUrl(HttpClient client, Uri uri, string serviceName)
{
var serviceResult = await client.GetStringAsync($"{serviceApi}api/v1/services/{serviceName}");
var serviceResult = await client.GetStringAsync($"{uri}api/v1/services/{serviceName}");
var service = JsonSerializer.Deserialize<V1Service>(serviceResult, _options);
var binding = service.Description!.Bindings.Where(b => b.Protocol == "http").Single();
return $"{binding.Protocol ?? "http"}://localhost:{binding.Port}";
}
private async Task RunHostingApplication(ApplicationBuilder application, string[] args, TestOutputLogEventSink sink, Func<Uri, Task> execute)
private async Task RunHostingApplication(ApplicationBuilder application, string[] args, Func<Application, Uri, Task> execute)
{
using var host = new TyeHost(application.ToHostingApplication(), args)
await using var host = new TyeHost(application.ToHostingApplication(), args)
{
Sink = sink,
Sink = _sink,
};
await StartHostAndWaitForReplicasToStart(host);
var serviceApi = new Uri(host.DashboardWebApplication!.Addresses.First());
try
{
await execute(serviceApi!);
await StartHostAndWaitForReplicasToStart(host);
var uri = new Uri(host.DashboardWebApplication!.Addresses.First());
await execute(host.Application, uri!);
}
finally
{
using (var client = new HttpClient())
if (host.DashboardWebApplication != null)
{
// If we failed, there's a good chance the service isn't running. Let's get the logs either way and put
// them in the output.
var uri = new Uri(host.DashboardWebApplication!.Addresses.First());
using var client = new HttpClient();
foreach (var s in host.Application.Services.Values)
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(serviceApi, $"/api/v1/logs/{s.Description.Name}"));
var response = await client.SendAsync(request);
var text = await response.Content.ReadAsStringAsync();
var logs = await client.GetStringAsync(new Uri(uri, $"/api/v1/logs/{s.Description.Name}"));
_output.WriteLine($"Logs for service: {s.Description.Name}");
_output.WriteLine(text);
}
}
_output.WriteLine(logs);
await host.StopAsync();
}
}
private async Task CheckServiceIsUp(Application application, HttpClient client, string serviceName, Uri dashboardUri, TimeSpan? timeout = default)
{
// make sure backend is up before frontend
var dashboardString = await client.GetStringAsync($"{dashboardUri}api/v1/services/{serviceName}");
var service = JsonSerializer.Deserialize<V1Service>(dashboardString, _options);
var binding = service.Description!.Bindings.Where(b => b.Protocol == "http").Single();
var uriBackendProcess = new Uri($"{binding.Protocol}://localhost:{binding.Port}");
var description = await client.GetStringAsync(new Uri(uri, $"/api/v1/services/{s.Description.Name}"));
var startTime = DateTime.UtcNow;
try
{
// Wait up until the timeout to see if we can access the service.
// For instance if we have to pull a base-image it can take a while.
while (timeout.HasValue && startTime + timeout.Value > DateTime.UtcNow)
{
try
{
await client.GetAsync(uriBackendProcess);
break;
}
catch (HttpRequestException)
{
await Task.Delay(TimeSpan.FromSeconds(3));
_output.WriteLine($"Service defintion: {s.Description.Name}");
_output.WriteLine(description);
}
}
var appResponse = await client.GetAsync(uriBackendProcess);
var content = await appResponse.Content.ReadAsStringAsync();
_output.WriteLine(content);
Assert.Equal(HttpStatusCode.OK, appResponse.StatusCode);
if (serviceName == "frontend")
{
Assert.Matches("Frontend Listening IP: (.+)\n", content);
Assert.Matches("Backend Listening IP: (.+)\n", content);
}
}
finally
{
// If we failed, there's a good chance the service isn't running. Let's get the logs either way and put
// them in the output.
foreach (var s in application.Services.Values)
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(dashboardUri, $"/api/v1/logs/{s.Description.Name}"));
var response = await client.SendAsync(request);
var text = await response.Content.ReadAsStringAsync();
_output.WriteLine($"Logs for service: {s.Description.Name}");
_output.WriteLine(text);
}
}
}
}

Loading…
Cancel
Save