Browse Source

Add support for named docker volumes (#253)

* Add support for named docker volumes
- Added tests
pull/254/head
David Fowler 6 years ago
committed by GitHub
parent
commit
8da92ba9c8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/Microsoft.Tye.Core/ApplicationFactory.cs
  2. 3
      src/Microsoft.Tye.Core/ConfigModel/ConfigVolume.cs
  3. 16
      src/Microsoft.Tye.Core/ProjectReader.cs
  4. 7
      src/Microsoft.Tye.Core/VolumeBuilder.cs
  5. 15
      src/Microsoft.Tye.Hosting/DockerRunner.cs
  6. 2
      src/Microsoft.Tye.Hosting/Model/DockerRunInfo.cs
  7. 21
      src/Microsoft.Tye.Hosting/Model/DockerVolume.cs
  8. 2
      src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs
  9. 14
      src/Microsoft.Tye.Hosting/Model/V1/V1DockerVolume.cs
  10. 2
      src/Microsoft.Tye.Hosting/Model/V1/V1RunInfo.cs
  11. 7
      src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs
  12. 8
      src/Microsoft.Tye.Hosting/TyeDashboardApi.cs
  13. 4
      src/tye/ApplicationBuilderExtensions.cs
  14. 148
      test/E2ETest/TyeRunTests.cs
  15. 26
      test/E2ETest/testassets/projects/volume-test/Program.cs
  16. 27
      test/E2ETest/testassets/projects/volume-test/Properties/launchSettings.json
  17. 54
      test/E2ETest/testassets/projects/volume-test/Startup.cs
  18. 9
      test/E2ETest/testassets/projects/volume-test/appsettings.Development.json
  19. 10
      test/E2ETest/testassets/projects/volume-test/appsettings.json
  20. 13
      test/E2ETest/testassets/projects/volume-test/tye.yaml
  21. 8
      test/E2ETest/testassets/projects/volume-test/volume-test.csproj

2
src/Microsoft.Tye.Core/ApplicationFactory.cs

@ -167,7 +167,7 @@ namespace Microsoft.Tye
foreach (var configVolume in configService.Volumes)
{
var volume = new VolumeBuilder(configVolume.Source, configVolume.Target);
var volume = new VolumeBuilder(configVolume.Source, configVolume.Name, configVolume.Target);
if (service is ProjectServiceBuilder project)
{
project.Volumes.Add(volume);

3
src/Microsoft.Tye.Core/ConfigModel/ConfigVolume.cs

@ -8,9 +8,10 @@ namespace Microsoft.Tye.ConfigModel
{
public class ConfigVolume
{
[Required]
public string Source { get; set; } = default!;
public string Name { get; set; } = default!;
[Required]
public string Target { get; set; } = default!;
}

16
src/Microsoft.Tye.Core/ProjectReader.cs

@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Threading.Tasks;
using Microsoft.Build.Construction;
@ -211,9 +212,9 @@ namespace Microsoft.Tye
output.WriteDebugLine($"IntermediateOutputPath={project.IntermediateOutputPath}");
// Normalize directories to their absolute paths
project.IntermediateOutputPath = Path.Combine(project.ProjectFile.DirectoryName, project.IntermediateOutputPath);
project.TargetPath = Path.Combine(project.ProjectFile.DirectoryName, project.TargetPath);
project.PublishDir = Path.Combine(project.ProjectFile.DirectoryName, project.PublishDir);
project.IntermediateOutputPath = Path.Combine(project.ProjectFile.DirectoryName, NormalizePath(project.IntermediateOutputPath));
project.TargetPath = Path.Combine(project.ProjectFile.DirectoryName, NormalizePath(project.TargetPath));
project.PublishDir = Path.Combine(project.ProjectFile.DirectoryName, NormalizePath(project.PublishDir));
var targetFramework = projectInstance.GetPropertyValue("TargetFramework");
project.TargetFramework = targetFramework;
@ -258,5 +259,14 @@ namespace Microsoft.Tye
return default;
}
}
private static string NormalizePath(string path)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return path.Replace('/', '\\');
}
return path.Replace('\\', '/');
}
}
}

7
src/Microsoft.Tye.Core/VolumeBuilder.cs

@ -6,13 +6,16 @@ namespace Microsoft.Tye
{
public sealed class VolumeBuilder
{
public VolumeBuilder(string source, string target)
public VolumeBuilder(string? source, string? name, string target)
{
Source = source;
Name = name;
Target = target;
}
public string Source { get; set; }
public string? Source { get; set; }
public string? Name { get; set; }
public string Target { get; set; }
}

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

@ -86,7 +86,7 @@ namespace Microsoft.Tye.Hosting
if (!string.IsNullOrEmpty(userSecretStore))
{
// Map the user secrets on this drive to user secrets
docker.VolumeMappings[userSecretStore] = "/root/.microsoft/usersecrets:ro";
docker.VolumeMappings.Add(new DockerVolume(source: userSecretStore, name: null, target: "/root/.microsoft/usersecrets:ro"));
}
var dockerInfo = new DockerInformation(new Task[service.Description.Replicas]);
@ -151,10 +151,17 @@ namespace Microsoft.Tye.Hosting
environmentArguments += $"-e {pair.Key}={pair.Value} ";
}
foreach (var pair in docker.VolumeMappings)
foreach (var volumeMapping in docker.VolumeMappings)
{
var sourcePath = Path.GetFullPath(Path.Combine(application.ContextDirectory, pair.Key.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)));
volumes += $"-v {sourcePath}:{pair.Value} ";
if (volumeMapping.Source != null)
{
var sourcePath = Path.GetFullPath(Path.Combine(application.ContextDirectory, volumeMapping.Source));
volumes += $"-v {sourcePath}:{volumeMapping.Target} ";
}
else if (volumeMapping.Name != null)
{
volumes += $"-v {volumeMapping.Name}:{volumeMapping.Target} ";
}
}
var command = $"run -d {workingDirectory} {volumes} {environmentArguments} {portString} --name {replica} --restart=unless-stopped {docker.Image} {docker.Args ?? ""}";

2
src/Microsoft.Tye.Hosting/Model/DockerRunInfo.cs

@ -16,7 +16,7 @@ namespace Microsoft.Tye.Hosting.Model
public string? WorkingDirectory { get; set; }
public Dictionary<string, string> VolumeMappings { get; } = new Dictionary<string, string>();
public List<DockerVolume> VolumeMappings { get; } = new List<DockerVolume>();
public string? Args { get; }

21
src/Microsoft.Tye.Hosting/Model/DockerVolume.cs

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.Tye.Hosting.Model
{
public class DockerVolume
{
public DockerVolume(string? source, string? name, string target)
{
Source = source;
Name = name;
Target = target;
}
public string? Name { get; }
public string? Source { get; }
public string Target { get; }
}
}

2
src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs

@ -44,6 +44,6 @@ namespace Microsoft.Tye.Hosting.Model
public string RunArguments { get; }
// This exists for running projects as containers
public Dictionary<string, string> VolumeMappings { get; } = new Dictionary<string, string>();
public List<DockerVolume> VolumeMappings { get; } = new List<DockerVolume>();
}
}

14
src/Microsoft.Tye.Hosting/Model/V1/V1DockerVolume.cs

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.Tye.Hosting.Model.V1
{
public class V1DockerVolume
{
public string? Name { get; set; }
public string? Source { get; set; }
public string? Target { get; set; }
}
}

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

@ -13,7 +13,7 @@ namespace Microsoft.Tye.Hosting.Model.V1
public bool Build { get; set; }
public string? Project { get; set; }
public string? WorkingDirectory { get; set; }
public Dictionary<string, string>? VolumeMappings { get; set; }
public List<V1DockerVolume>? VolumeMappings { get; set; }
public string? Image { get; set; }
public string? Executable { get; set; }
}

7
src/Microsoft.Tye.Hosting/TransformProjectsIntoContainers.cs

@ -71,13 +71,10 @@ namespace Microsoft.Tye.Hosting
WorkingDirectory = "/app"
};
dockerRunInfo.VolumeMappings[project.PublishOutputPath] = "/app";
dockerRunInfo.VolumeMappings.Add(new DockerVolume(source: project.PublishOutputPath, name: null, target: "/app"));
// Make volume mapping works when running as a container
foreach (var mapping in project.VolumeMappings)
{
dockerRunInfo.VolumeMappings[mapping.Key] = mapping.Value;
}
dockerRunInfo.VolumeMappings.AddRange(project.VolumeMappings);
// Change the project into a container info
serviceDescription.RunInfo = dockerRunInfo;

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

@ -133,7 +133,13 @@ namespace Microsoft.Tye.Hosting
{
v1RunInfo.Type = V1RunInfoType.Docker;
v1RunInfo.Image = dockerRunInfo.Image;
v1RunInfo.VolumeMappings = dockerRunInfo.VolumeMappings;
v1RunInfo.VolumeMappings = dockerRunInfo.VolumeMappings.Select(v => new V1DockerVolume
{
Name = v.Name,
Source = v.Source,
Target = v.Target
}).ToList();
v1RunInfo.WorkingDirectory = dockerRunInfo.WorkingDirectory;
v1RunInfo.Args = dockerRunInfo.Args;
}

4
src/tye/ApplicationBuilderExtensions.cs

@ -29,7 +29,7 @@ namespace Microsoft.Tye.ConfigModel
foreach (var mapping in container.Volumes)
{
dockerRunInfo.VolumeMappings[mapping.Source!] = mapping.Target!;
dockerRunInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target));
}
runInfo = dockerRunInfo;
@ -66,7 +66,7 @@ namespace Microsoft.Tye.ConfigModel
foreach (var mapping in project.Volumes)
{
projectInfo.VolumeMappings[mapping.Source!] = mapping.Target!;
projectInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target));
}
runInfo = projectInfo;

148
test/E2ETest/TyeRunTests.cs

@ -17,6 +17,7 @@ using Microsoft.Tye.Hosting.Model;
using Microsoft.Tye.Hosting.Model.V1;
using Xunit;
using Xunit.Abstractions;
using static E2ETest.TestHelpers;
namespace E2ETest
{
@ -138,6 +139,101 @@ namespace E2ETest
}
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task DockerNamedVolumeTest()
{
using var projectDirectory = CopyTestProjectDirectory("volume-test");
var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));
var outputContext = new OutputContext(_sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Add a volume
var project = ((ProjectServiceBuilder)application.Services[0]);
// Remove the existing volume so we can generate a random one for this test to avoid conflicts
var volumeName = "tye_docker_volumes_test" + Guid.NewGuid().ToString().Substring(0, 10);
project.Volumes.Clear();
project.Volumes.Add(new VolumeBuilder(source: null, name: volumeName, "/data"));
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = false
};
var client = new HttpClient(new RetryHandler(handler));
var args = new[] { "--docker" };
await RunHostingApplication(application, args, _sink, async serviceApi =>
{
var serviceUri = await GetServiceUrl(client, serviceApi, "volume-test");
Assert.NotNull(serviceUri);
var response = await client.GetAsync(serviceUri);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
await client.PostAsync(serviceUri, new StringContent("Things saved to the volume!"));
Assert.Equal("Things saved to the volume!", await client.GetStringAsync(serviceUri));
});
await RunHostingApplication(application, args, _sink, async serviceApi =>
{
var serviceUri = await GetServiceUrl(client, serviceApi, "volume-test");
Assert.NotNull(serviceUri);
// The volume has data persisted
Assert.Equal("Things saved to the volume!", await client.GetStringAsync(serviceUri));
});
// Delete the volume
await ProcessUtil.RunAsync("docker", $"volume rm {volumeName}");
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task DockerHostVolumeTest()
{
using var projectDirectory = CopyTestProjectDirectory("volume-test");
var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));
var outputContext = new OutputContext(_sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Add a volume
var project = ((ProjectServiceBuilder)application.Services[0]);
using var tempDir = TempDirectory.Create();
project.Volumes.Clear();
project.Volumes.Add(new VolumeBuilder(source: tempDir.DirectoryPath, name: null, target: "/data"));
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = false
};
File.WriteAllText(Path.Combine(tempDir.DirectoryPath, "file.txt"), "This content came from the host");
var client = new HttpClient(new RetryHandler(handler));
var args = new[] { "--docker" };
await RunHostingApplication(application, args, _sink, async serviceApi =>
{
var serviceUri = await GetServiceUrl(client, serviceApi, "volume-test");
Assert.NotNull(serviceUri);
// The volume has data the host mapped data
Assert.Equal("This content came from the host", await client.GetStringAsync(serviceUri));
});
}
[Fact]
public async Task IngressRunTest()
{
@ -165,11 +261,7 @@ namespace E2ETest
try
{
var ingressService = await client.GetStringAsync($"{serviceApi}api/v1/services/ingress");
var service = JsonSerializer.Deserialize<V1Service>(ingressService, _options);
var binding = service.Description!.Bindings.Single();
var ingressUri = $"http://localhost:{binding.Port}";
var ingressUri = await GetServiceUrl(client, serviceApi, "ingress");
var responseA = await client.GetAsync(ingressUri + "/A");
var responseB = await client.GetAsync(ingressUri + "/B");
@ -270,7 +362,51 @@ namespace E2ETest
await host.StopAsync();
}
private async Task CheckServiceIsUp(Microsoft.Tye.Hosting.Model.Application application, HttpClient client, string serviceName, Uri dashboardUri, TimeSpan? timeout = default)
private async Task<string> GetServiceUrl(HttpClient client, Uri serviceApi, string serviceName)
{
var serviceResult = await client.GetStringAsync($"{serviceApi}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)
{
using var host = new TyeHost(application.ToHostingApplication(), args)
{
Sink = sink,
};
await StartHostAndWaitForReplicasToStart(host);
var serviceApi = new Uri(host.DashboardWebApplication!.Addresses.First());
try
{
await execute(serviceApi!);
}
finally
{
using (var client = new HttpClient())
{
// 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();
}
}
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}");

26
test/E2ETest/testassets/projects/volume-test/Program.cs

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace volume_test
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

27
test/E2ETest/testassets/projects/volume-test/Properties/launchSettings.json

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:27607",
"sslPort": 44301
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"volume_test": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

54
test/E2ETest/testassets/projects/volume-test/Startup.cs

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace volume_test
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
if (!File.Exists("/data/file.txt"))
{
context.Response.StatusCode = 404;
return;
}
var data = await File.ReadAllTextAsync("/data/file.txt");
await context.Response.WriteAsync(data);
});
endpoints.MapPost("/", async context =>
{
await File.WriteAllTextAsync("/data/file.txt", await new StreamReader(context.Request.Body).ReadToEndAsync());
context.Response.StatusCode = 202;
});
});
}
}
}

9
test/E2ETest/testassets/projects/volume-test/appsettings.Development.json

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

10
test/E2ETest/testassets/projects/volume-test/appsettings.json

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

13
test/E2ETest/testassets/projects/volume-test/tye.yaml

@ -0,0 +1,13 @@
# tye application configuration file
# read all about it at https://github.com/dotnet/tye
#
# when you've given us a try, we'd love to know what you think:
# https://aka.ms/AA7q20u
#
name: volume-test
services:
- name: volume-test
project: volume-test.csproj
volumes:
- name: data-vol
target: /data

8
test/E2ETest/testassets/projects/volume-test/volume-test.csproj

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>volume_test</RootNamespace>
</PropertyGroup>
</Project>
Loading…
Cancel
Save