diff --git a/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj b/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj
index 16e4ad6e..53ba752d 100644
--- a/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj
+++ b/src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj
@@ -16,7 +16,7 @@
The Microsoft.Build.Locator package takes care of dynamically loading these assemblies
at runtime. We don't need/want to ship them, just to have them as references.
-->
-
+
diff --git a/src/Microsoft.Tye.Core/ProjectReader.cs b/src/Microsoft.Tye.Core/ProjectReader.cs
index 427b8d0b..326af38b 100644
--- a/src/Microsoft.Tye.Core/ProjectReader.cs
+++ b/src/Microsoft.Tye.Core/ProjectReader.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
@@ -10,7 +11,9 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Threading.Tasks;
+using Microsoft.Build.Construction;
using Microsoft.Build.Definition;
+using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Locator;
@@ -21,17 +24,46 @@ namespace Microsoft.Tye
{
public static class ProjectReader
{
+ private static object @lock = new object();
private static bool registered;
- public static Task ReadProjectDetailsAsync(OutputContext output, FileInfo projectFile, Project project)
+
+ public static IEnumerable EnumerateProjects(FileInfo solutionFile)
{
- if (output is null)
+ EnsureMSBuildRegistered(null, solutionFile);
+ return EnumerateProjectsCore(solutionFile);
+ }
+
+ // Do not load MSBuild types before using EnsureMSBuildRegistered.
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static IEnumerable EnumerateProjectsCore(FileInfo solutionFile)
+ {
+ var solution = SolutionFile.Parse(solutionFile.FullName);
+ foreach (var project in solution.ProjectsInOrder)
{
- throw new ArgumentNullException(nameof(output));
+ if (project.ProjectType != SolutionProjectType.KnownToBeMSBuildFormat)
+ {
+ continue;
+ }
+
+ var extension = Path.GetExtension(project.AbsolutePath).ToLower();
+ switch (extension)
+ {
+ case ".csproj":
+ case ".fsproj":
+ break;
+ default:
+ continue;
+ }
+
+ yield return new FileInfo(project.AbsolutePath.Replace('\\', '/'));
}
+ }
- if (projectFile is null)
+ public static Task ReadProjectDetailsAsync(OutputContext output, FileInfo projectFile, Project project)
+ {
+ if (output is null)
{
- throw new ArgumentNullException(nameof(projectFile));
+ throw new ArgumentNullException(nameof(output));
}
if (project is null)
@@ -39,54 +71,63 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(project));
}
- EnsureMSBuildRegistered(projectFile);
+ EnsureMSBuildRegistered(output, projectFile);
- using (var step = output.BeginStep("Reading Project Details..."))
- {
- EvaluateProject(output, projectFile, project);
+ EvaluateProject(output, projectFile, project);
- if (!SemVersion.TryParse(project.Version, out var version))
- {
- output.WriteInfoLine($"No version or invalid version '{project.Version}' found, using default.");
- version = new SemVersion(0, 1, 0);
- project.Version = version.ToString();
- }
-
- step.MarkComplete();
+ if (!SemVersion.TryParse(project.Version, out var version))
+ {
+ output.WriteInfoLine($"No version or invalid version '{project.Version}' found, using default.");
+ version = new SemVersion(0, 1, 0);
+ project.Version = version.ToString();
}
return Task.CompletedTask;
}
- private static void EnsureMSBuildRegistered(FileInfo projectFile)
+ private static void EnsureMSBuildRegistered(OutputContext? output, FileInfo projectFile)
{
if (!registered)
{
- // It says VisualStudio - but we'll just use .NET SDK
- var instances = MSBuildLocator.QueryVisualStudioInstances(new VisualStudioInstanceQueryOptions()
+ lock (@lock)
{
- DiscoveryTypes = DiscoveryType.DotNetSdk,
-
- // Using the project as the working directory. We're making the assumption that
- // all of the projects want to use the same SDK version. This library is going
- // load a single version of the SDK's assemblies into our process, so we can't
- // use supprt SDKs at once without getting really tricky.
- //
- // The .NET SDK-based discovery uses `dotnet --info` and returns the SDK
- // in use for the directory.
- //
- // https://github.com/microsoft/MSBuildLocator/blob/master/src/MSBuildLocator/MSBuildLocator.cs#L320
- WorkingDirectory = projectFile.DirectoryName,
- });
+ output?.WriteDebugLine("Locating .NET SDK...");
- var instance = instances.SingleOrDefault();
- if (instance == null)
- {
- throw new CommandException("Failed to find dotnet. Make sure the .NET SDK is installed and on the PATH.");
- }
+ // It says VisualStudio - but we'll just use .NET SDK
+ var instances = MSBuildLocator.QueryVisualStudioInstances(new VisualStudioInstanceQueryOptions()
+ {
+ DiscoveryTypes = DiscoveryType.DotNetSdk,
+
+ // Using the project as the working directory. We're making the assumption that
+ // all of the projects want to use the same SDK version. This library is going
+ // load a single version of the SDK's assemblies into our process, so we can't
+ // use supprt SDKs at once without getting really tricky.
+ //
+ // The .NET SDK-based discovery uses `dotnet --info` and returns the SDK
+ // in use for the directory.
+ //
+ // https://github.com/microsoft/MSBuildLocator/blob/master/src/MSBuildLocator/MSBuildLocator.cs#L320
+ WorkingDirectory = projectFile.DirectoryName,
+ });
+
+ var instance = instances.SingleOrDefault();
+ if (instance == null)
+ {
+ throw new CommandException("Failed to find dotnet. Make sure the .NET SDK is installed and on the PATH.");
+ }
- MSBuildLocator.RegisterInstance(instance);
- registered = true;
+ output?.WriteDebugLine("Found .NET SDK at: " + instance.MSBuildPath);
+
+ try
+ {
+ MSBuildLocator.RegisterInstance(instance);
+ output?.WriteDebug("Registered .NET SDK.");
+ }
+ finally
+ {
+ registered = true;
+ }
+ }
}
}
@@ -96,16 +137,25 @@ namespace Microsoft.Tye
{
var sw = Stopwatch.StartNew();
+ // We need to isolate projects from each other for testing. MSBuild does not support
+ // loading the same project twice in the same collection.
+ var projectCollection = new ProjectCollection();
+
ProjectInstance projectInstance;
+
try
{
output.WriteDebugLine($"Loading project '{projectFile.FullName}'.");
- projectInstance = ProjectInstance.FromFile(projectFile.FullName, new ProjectOptions());
+ var msbuildProject = Microsoft.Build.Evaluation.Project.FromFile(projectFile.FullName, new ProjectOptions()
+ {
+ ProjectCollection = projectCollection,
+ });
+ projectInstance = msbuildProject.CreateProjectInstance();
output.WriteDebugLine($"Loaded project '{projectFile.FullName}'.");
}
- catch
+ catch (Exception ex)
{
- throw new CommandException($"Failed to load project: '{projectFile.FullName}'.");
+ throw new CommandException($"Failed to load project: '{projectFile.FullName}'.", ex);
}
// Currently we only log at debug level.
diff --git a/src/Microsoft.Tye.Hosting/DockerRunner.cs b/src/Microsoft.Tye.Hosting/DockerRunner.cs
index 7c83bfed..86ca02e1 100644
--- a/src/Microsoft.Tye.Hosting/DockerRunner.cs
+++ b/src/Microsoft.Tye.Hosting/DockerRunner.cs
@@ -129,6 +129,10 @@ namespace Microsoft.Tye.Hosting
environment["PORT"] = string.Join(";", ports.Select(p => $"{p.InternalPort ?? p.Port}"));
}
+ // See: https://github.com/docker/for-linux/issues/264
+ //
+ // The way we do proxying here doesn't really work for multi-container scenarios on linux
+ // without some more setup.
application.PopulateEnvironment(service, (key, value) => environment[key] = value, "host.docker.internal");
environment["APP_INSTANCE"] = replica;
diff --git a/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj b/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj
index 2eb8638b..10d54720 100644
--- a/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj
+++ b/src/Microsoft.Tye.Hosting/Microsoft.Tye.Hosting.csproj
@@ -19,7 +19,6 @@
-
diff --git a/src/shared/TempDirectory.cs b/src/shared/TempDirectory.cs
index 0f9f0bb0..ad87d47f 100644
--- a/src/shared/TempDirectory.cs
+++ b/src/shared/TempDirectory.cs
@@ -4,16 +4,30 @@
using System;
using System.IO;
+using System.Runtime.InteropServices;
namespace Microsoft.Tye
{
internal class TempDirectory : IDisposable
{
- public static TempDirectory Create()
+ public static TempDirectory Create(bool preferUserDirectoryOnMacOS = false)
{
- var directoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- var directoryInfo = Directory.CreateDirectory(directoryPath);
- return new TempDirectory(directoryPath, directoryInfo);
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && preferUserDirectoryOnMacOS)
+ {
+ var baseDirectory = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ baseDirectory = Path.Combine(baseDirectory, ".tye");
+ Directory.CreateDirectory(baseDirectory);
+
+ var directoryPath = Path.Combine(baseDirectory, Path.GetRandomFileName());
+ var directoryInfo = Directory.CreateDirectory(directoryPath);
+ return new TempDirectory(directoryPath, directoryInfo);
+ }
+ else
+ {
+ var directoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ var directoryInfo = Directory.CreateDirectory(directoryPath);
+ return new TempDirectory(directoryPath, directoryInfo);
+ }
}
private TempDirectory(string directoryPath, DirectoryInfo directoryInfo)
diff --git a/src/tye/ConfigModel/ConfigFactory.cs b/src/tye/ConfigModel/ConfigFactory.cs
index 4568798e..3d24405a 100644
--- a/src/tye/ConfigModel/ConfigFactory.cs
+++ b/src/tye/ConfigModel/ConfigFactory.cs
@@ -57,25 +57,11 @@ namespace Microsoft.Tye.ConfigModel
Source = file,
};
- var solution = SolutionFile.Parse(file.FullName);
- foreach (var project in solution.ProjectsInOrder)
+ // BE CAREFUL modifying this code. Avoid proliferating MSBuild types
+ // throughout the code, because we load them dynamically.
+ foreach (var projectFile in ProjectReader.EnumerateProjects(file))
{
- if (project.ProjectType != SolutionProjectType.KnownToBeMSBuildFormat)
- {
- continue;
- }
-
- var extension = Path.GetExtension(project.AbsolutePath).ToLower();
- switch (extension)
- {
- case ".csproj":
- case ".fsproj":
- break;
- default:
- continue;
- }
-
- var description = CreateService(new FileInfo(project.AbsolutePath.Replace('\\', '/')));
+ var description = CreateService(projectFile);
if (description != null)
{
application.Services.Add(description);
diff --git a/test/E2ETest/E2ETest.csproj b/test/E2ETest/E2ETest.csproj
index f18097d9..0df1fa54 100644
--- a/test/E2ETest/E2ETest.csproj
+++ b/test/E2ETest/E2ETest.csproj
@@ -13,6 +13,16 @@
+
+
+
+
+
+
+
diff --git a/test/E2ETest/Infrastructure/ConditionalFactAttribute.cs b/test/E2ETest/Infrastructure/ConditionalFactAttribute.cs
index 4559f87d..6047c821 100644
--- a/test/E2ETest/Infrastructure/ConditionalFactAttribute.cs
+++ b/test/E2ETest/Infrastructure/ConditionalFactAttribute.cs
@@ -5,7 +5,7 @@ using Xunit.Sdk;
namespace E2ETest
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
- [XunitTestCaseDiscoverer("E2ETest." + nameof(ConditionalFactDiscoverer), "E2ETest")]
+ [XunitTestCaseDiscoverer("E2ETest." + nameof(ConditionalFactDiscoverer), "Microsoft.Tye.E2ETest")]
public class ConditionalFactAttribute : FactAttribute
{
}
diff --git a/test/E2ETest/Infrastructure/SkipOnLinuxAttribute.cs b/test/E2ETest/Infrastructure/SkipOnLinuxAttribute.cs
new file mode 100644
index 00000000..37d040c2
--- /dev/null
+++ b/test/E2ETest/Infrastructure/SkipOnLinuxAttribute.cs
@@ -0,0 +1,23 @@
+// 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.Runtime.InteropServices;
+
+namespace E2ETest
+{
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
+ internal class SkipOnLinuxAttribute : Attribute, ITestCondition
+ {
+ public SkipOnLinuxAttribute()
+ {
+ IsMet = !RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+ SkipReason = "This is linux. Here's a free bus-ticket to skipsville.";
+ }
+
+ public bool IsMet { get; }
+
+ public string SkipReason { get; set; }
+ }
+}
diff --git a/test/E2ETest/TestOutputLogEventSink.cs b/test/E2ETest/TestOutputLogEventSink.cs
index b7b2e70a..d42f2c67 100644
--- a/test/E2ETest/TestOutputLogEventSink.cs
+++ b/test/E2ETest/TestOutputLogEventSink.cs
@@ -32,7 +32,8 @@ namespace E2ETest
public void Write(string value)
{
- output.WriteLine(value);
+ // our usage of IConsole includes newlines, so strip them out.
+ output.WriteLine(value.TrimEnd('\r', '\n'));
}
}
}
diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs
index f5463103..4196f3e6 100644
--- a/test/E2ETest/TyeRunTests.cs
+++ b/test/E2ETest/TyeRunTests.cs
@@ -137,10 +137,11 @@ namespace E2ETest
[ConditionalFact]
[SkipIfDockerNotRunning]
+ [SkipOnLinux]
public async Task FrontendBackendRunTestWithDocker()
{
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "frontend-backend"));
- using var tempDirectory = TempDirectory.Create();
+ using var tempDirectory = TempDirectory.Create(preferUserDirectoryOnMacOS: true);
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
@@ -165,8 +166,8 @@ namespace E2ETest
var dashboardUri = new Uri(host.DashboardWebApplication!.Addresses.First());
- await CheckServiceIsUp(host.Application, client, "backend", dashboardUri);
- await CheckServiceIsUp(host.Application, client, "frontend", dashboardUri);
+ await CheckServiceIsUp(host.Application, client, "backend", dashboardUri, timeout: TimeSpan.FromSeconds(60));
+ await CheckServiceIsUp(host.Application, client, "frontend", dashboardUri, timeout: TimeSpan.FromSeconds(60));
}
finally
{
@@ -174,7 +175,7 @@ namespace E2ETest
}
}
- private async Task CheckServiceIsUp(Microsoft.Tye.Hosting.Model.Application application, HttpClient client, string serviceName, Uri dashboardUri)
+ private async Task CheckServiceIsUp(Microsoft.Tye.Hosting.Model.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}");
@@ -183,11 +184,28 @@ namespace E2ETest
var binding = service.Description!.Bindings.Where(b => b.Protocol == "http").Single();
var uriBackendProcess = new Uri($"{binding.Protocol}://localhost:{binding.Port}");
+ 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));
+ }
+ }
+
var appResponse = await client.GetAsync(uriBackendProcess);
- Assert.Equal(HttpStatusCode.OK, appResponse.StatusCode);
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);
diff --git a/test/E2ETest/testassets/generate/frontend-backend.yaml b/test/E2ETest/testassets/generate/frontend-backend.yaml
index 5b2ef157..a4e061e9 100644
--- a/test/E2ETest/testassets/generate/frontend-backend.yaml
+++ b/test/E2ETest/testassets/generate/frontend-backend.yaml
@@ -23,6 +23,8 @@ spec:
env:
- name: ASPNETCORE_URLS
value: 'http://*'
+ - name: PORT
+ value: '80'
- name: SERVICE__FRONTEND__PROTOCOL
value: 'http'
- name: SERVICE__FRONTEND__PORT
@@ -76,6 +78,8 @@ spec:
env:
- name: ASPNETCORE_URLS
value: 'http://*'
+ - name: PORT
+ value: '80'
- name: SERVICE__BACKEND__PROTOCOL
value: 'http'
- name: SERVICE__BACKEND__PORT
diff --git a/test/E2ETest/testassets/generate/multi-project.yaml b/test/E2ETest/testassets/generate/multi-project.yaml
index 630dc259..7734f44e 100644
--- a/test/E2ETest/testassets/generate/multi-project.yaml
+++ b/test/E2ETest/testassets/generate/multi-project.yaml
@@ -23,6 +23,8 @@ spec:
env:
- name: ASPNETCORE_URLS
value: 'http://*:7000'
+ - name: PORT
+ value: '7000'
- name: SERVICE__FRONTEND__PROTOCOL
value: 'http'
- name: SERVICE__FRONTEND__PORT
@@ -87,6 +89,8 @@ spec:
env:
- name: ASPNETCORE_URLS
value: 'http://*:8000'
+ - name: PORT
+ value: '8000'
- name: SERVICE__BACKEND__PROTOCOL
value: 'http'
- name: SERVICE__BACKEND__PORT
diff --git a/test/E2ETest/testassets/generate/single-project-noregistry.yaml b/test/E2ETest/testassets/generate/single-project-noregistry.yaml
index f5d5d0ec..9e33747f 100644
--- a/test/E2ETest/testassets/generate/single-project-noregistry.yaml
+++ b/test/E2ETest/testassets/generate/single-project-noregistry.yaml
@@ -23,6 +23,8 @@ spec:
env:
- name: ASPNETCORE_URLS
value: 'http://*'
+ - name: PORT
+ value: '80'
ports:
- containerPort: 80
...
diff --git a/test/E2ETest/testassets/generate/single-project.yaml b/test/E2ETest/testassets/generate/single-project.yaml
index 0717d032..70c70507 100644
--- a/test/E2ETest/testassets/generate/single-project.yaml
+++ b/test/E2ETest/testassets/generate/single-project.yaml
@@ -23,6 +23,8 @@ spec:
env:
- name: ASPNETCORE_URLS
value: 'http://*'
+ - name: PORT
+ value: '80'
ports:
- containerPort: 80
...