Browse Source

Merge branch 'main' into philliphoff-arm64

philliphoff-arm64
Phillip Hoff 5 years ago
parent
commit
e440979cca
  1. 2
      .github/CODEOWNERS
  2. 4
      Directory.Build.props
  3. 3
      NuGet.config
  4. 8
      azure-pipelines.yml
  5. 8
      docs/getting_started.md
  6. 13
      docs/reference/schema.md
  7. 4
      eng/Versions.props
  8. 24225
      samples/apps-with-core-angular/MoviesApp/package-lock.json
  9. 4
      samples/apps-with-core-angular/MoviesApp/package.json
  10. 19
      samples/dapr/pub-sub/tye.yaml
  11. 6
      samples/dapr/service-invocation/SentenceApp/SentenceApp.csproj
  12. 7
      samples/dapr/service-invocation/SentenceApp/Services/LowercaseServiceClient.cs
  13. 7
      samples/dapr/service-invocation/SentenceApp/Services/TitlecaseServiceClient.cs
  14. 11
      samples/dapr/service-invocation/SentenceApp/Services/UppercaseServiceClient.cs
  15. 11
      samples/dapr/service-invocation/tye.yaml
  16. 5
      src/Microsoft.Tye.Core/ApplicationBuilder.cs
  17. 7
      src/Microsoft.Tye.Core/ApplicationFactory.cs
  18. 3
      src/Microsoft.Tye.Core/AzureFunctionServiceBuilder.cs
  19. 2
      src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs
  20. 1
      src/Microsoft.Tye.Core/ConfigModel/ConfigIngressBinding.cs
  21. 3
      src/Microsoft.Tye.Core/CoreStrings.resx
  22. 1
      src/Microsoft.Tye.Core/IngressBindingBuilder.cs
  23. 4
      src/Microsoft.Tye.Core/ProcessExtensions.cs
  24. 10
      src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs
  25. 8
      src/Microsoft.Tye.Core/Serialization/ConfigExtensionsParser.cs
  26. 13
      src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs
  27. 26
      src/Microsoft.Tye.Core/Serialization/YamlParser.cs
  28. 149
      src/Microsoft.Tye.Extensions/Dapr/DaprExtension.cs
  29. 24
      src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfiguration.cs
  30. 58
      src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfigurationReader.cs
  31. 3
      src/Microsoft.Tye.Hosting/DockerRunner.cs
  32. 3
      src/Microsoft.Tye.Hosting/HttpProxyService.cs
  33. 5
      src/Microsoft.Tye.Hosting/Model/Application.cs
  34. 2
      src/Microsoft.Tye.Hosting/Model/ServiceBinding.cs
  35. 13
      src/Microsoft.Tye.Hosting/Model/V1/V1Metric.cs
  36. 11
      src/Microsoft.Tye.Hosting/Model/V1/V1MetricMetadata.cs
  37. 14
      src/Microsoft.Tye.Hosting/Model/V1/V1ServiceMetrics.cs
  38. 3
      src/Microsoft.Tye.Hosting/ProcessRunner.cs
  39. 101
      src/Microsoft.Tye.Hosting/TyeDashboardApi.cs
  40. 2
      src/Microsoft.Tye.Hosting/TyeHost.cs
  41. 8
      src/schema/tye-schema.json
  42. 8
      src/tye/ApplicationBuilderExtensions.cs
  43. 9
      test/E2ETest/Microsoft.Tye.E2ETests.csproj
  44. 184
      test/E2ETest/TyeRunTests.cs
  45. 20
      test/E2ETest/testassets/projects/apps-with-ingress/tye-ip_test.yaml
  46. 34
      test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/single-project.sln
  47. 30
      test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/test-project/Program.cs
  48. 44
      test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/test-project/Startup.cs
  49. 9
      test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/test-project/appsettings.Development.json
  50. 10
      test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/test-project/appsettings.json
  51. 8
      test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/test-project/test-project.csproj
  52. 7
      test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/tye.yaml
  53. 34
      test/E2ETest/testassets/projects/non-standard-dashboard-port/single-project.sln
  54. 30
      test/E2ETest/testassets/projects/non-standard-dashboard-port/test-project/Program.cs
  55. 27
      test/E2ETest/testassets/projects/non-standard-dashboard-port/test-project/Properties/launchSettings.json
  56. 44
      test/E2ETest/testassets/projects/non-standard-dashboard-port/test-project/Startup.cs
  57. 9
      test/E2ETest/testassets/projects/non-standard-dashboard-port/test-project/appsettings.Development.json
  58. 10
      test/E2ETest/testassets/projects/non-standard-dashboard-port/test-project/appsettings.json
  59. 8
      test/E2ETest/testassets/projects/non-standard-dashboard-port/test-project/test-project.csproj
  60. 7
      test/E2ETest/testassets/projects/non-standard-dashboard-port/tye.yaml
  61. 4
      test/Test.Infrastructure/TestHelpers.cs

2
.github/CODEOWNERS

@ -1,4 +1,4 @@
# Users referenced in this file will automatically be requested as reviewers for PRs that modify the given paths.
# See https://help.github.com/articles/about-code-owners/
* @philliphoff @pratiksanglikar
* @philliphoff @ravipal

4
Directory.Build.props

@ -12,6 +12,10 @@
<StrongNameKeyId>Microsoft</StrongNameKeyId>
</PropertyGroup>
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)LICENSE.txt" Pack="true" PackagePath="" />
</ItemGroup>
<PropertyGroup>
<!-- Lots of our dependencies are not strong-named -->
<NoWarn>$(NoWarn);8002</NoWarn>

3
NuGet.config

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" />
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<disabledPackageSources />
</configuration>

8
azure-pipelines.yml

@ -46,11 +46,11 @@ stages:
- job: Windows
pool:
${{ if eq(variables['System.TeamProject'], 'public') }}:
name: NetCorePublic-Pool
queue: BuildPool.Server.Amd64.VS2019.Open
name: NetCore1ESPool-Public
demands: ImageOverride -equals Build.Server.Amd64.VS2019.Open
${{ if ne(variables['System.TeamProject'], 'public') }}:
name: NetCoreInternal-Pool
queue: BuildPool.Server.Amd64.VS2019
name: NetCore1ESPool-Internal
demands: ImageOverride -equals Build.Server.Amd64.VS2019
variables:

8
docs/getting_started.md

@ -8,13 +8,13 @@ Tye is a tool that makes developing, testing, and deploying microservices and di
1. Install tye via the following command:
```text
dotnet tool install -g Microsoft.Tye --version "0.9.0-alpha.21380.1"
dotnet tool install -g Microsoft.Tye --version "0.10.0-alpha.21420.1"
```
OR if you already have Tye installed and want to update:
```text
dotnet tool update -g Microsoft.Tye --version "0.9.0-alpha.21380.1"
dotnet tool update -g Microsoft.Tye --version "0.10.0-alpha.21420.1"
```
> If using Mac and, if getting "command not found" errors when running `tye`, you may need to ensure that the `$HOME/.dotnet/tools` directory has been added to `PATH`.
@ -37,13 +37,13 @@ Tye is a tool that makes developing, testing, and deploying microservices and di
This will install the newest available build from our CI.
```txt
dotnet tool install -g Microsoft.Tye --version "0.10.0-*" --add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json
dotnet tool install -g Microsoft.Tye --version "0.11.0-*" --add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json
```
If you already have a build installed and you want to update, replace `install` with `update`:
```txt
dotnet tool update -g Microsoft.Tye --version "0.10.0-*" --add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json
dotnet tool update -g Microsoft.Tye --version "0.11.0-*" --add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json
```
> :bulb: Note that the version numbers for our CI builds and released packages will usually be different.

13
docs/reference/schema.md

@ -85,6 +85,14 @@ Allows configuring the Docker network used for `tye run`.
If a network is configured, then all services running in containers will connect to the specified network. Otherwise a Docker network will be created with a generated name, and used to connect all containers.
#### `dashboardPort` (int)
Allows configuring the dashboard port used for `tye run`.
If a `--port` is provided via the CLI, it will be used instead.
If no `--port` argument or `dashboardPort` value is specified, Tye will use the default port (8000), or a random port if the default port is in use.
#### `ingress` (`Ingress[]`)
Specifies the list of ingresses.
@ -518,6 +526,11 @@ The port of the binding.
The protocol (`http` or `https`).
#### `ip` (`string`)
The optional IP adress to bind to. Can be '*' for all addresses.
Default is localhost.
## IngressRule
`IngressRule` elements appear in an array within the `rules` property of the `Ingress` element. Rules configure the routing behavior of the ingress proxy.

4
eng/Versions.props

@ -2,11 +2,11 @@
<Project>
<PropertyGroup>
<!-- This repo version -->
<VersionPrefix>0.10.0</VersionPrefix>
<VersionPrefix>0.11.0</VersionPrefix>
<PreReleaseVersionLabel>alpha</PreReleaseVersionLabel>
<!-- Opt-out repo features -->
<UsingToolXliff>false</UsingToolXliff>
<UsingToolNetFrameworkReferenceAssemblies Condition="'$(DotNetBuildFromSource)' != 'true'">true</UsingToolNetFrameworkReferenceAssemblies>
<!-- Libs -->
</PropertyGroup>
</Project>
</Project>

24225
samples/apps-with-core-angular/MoviesApp/package-lock.json

File diff suppressed because it is too large

4
samples/apps-with-core-angular/MoviesApp/package.json

@ -32,13 +32,13 @@
"@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3",
"codelyzer": "^5.1.2",
"jasmine-core": "~3.5.0",
"jasmine-core": "~3.8.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.0",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.2",
"karma-jasmine-html-reporter": "^1.6.0",
"protractor": "~5.4.3",
"ts-node": "~8.3.0",
"tslint": "~5.18.0",

19
samples/dapr/pub-sub/tye.yaml

@ -8,10 +8,6 @@ name: dapr
extensions:
- name: dapr
# If you want to use a different tag or container port
# placement-image: daprio/dapr
# placement-container-port: 50005
# log-level configures the log level of the dapr sidecar
log-level: debug
@ -24,11 +20,8 @@ extensions:
# components-path configures the components path of the dapr sidecar
components-path: "./components/"
# You can instruct Tye to not create the Dapr placement container on your behalf. This is required if you have Dapr running and want to use that container.
# Doing a `docker ps` can show if its already running. If it's running then you can set 'exclude-placement-container: true' with `placement-port: xxxx` set to the host port of that container.
# (i.e. In Windows + WSL2, Dapr uses 6050 as the host port)
# exclude-placement-container: true
# If not using the default Dapr placement service or otherwise using a placement service on a nonstandard port,
# you can configure the Dapr sidecar to use an explicit port.
# placement-port: 6050
services:
- name: orders
@ -42,7 +35,7 @@ services:
#
# Doing a `docker ps` can show if its already running. If that's the case
# then comment out out when running locally.
- name: redis
image: redis
bindings:
- port: 6379
# - name: redis
# image: redis
# bindings:
# - port: 6379

6
samples/dapr/service-invocation/SentenceApp/SentenceApp.csproj

@ -5,9 +5,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapr.AspNetCore" Version="1.0.0" />
<PackageReference Include="Dapr.Client" Version="1.0.0" />
<PackageReference Include="Microsoft.Tye.Extensions.Configuration" Version="0.5.0-alpha.20555.1" />
<PackageReference Include="Dapr.AspNetCore" Version="1.3.0" />
<PackageReference Include="Dapr.Client" Version="1.3.0" />
<PackageReference Include="Microsoft.Tye.Extensions.Configuration" Version="0.9.0-alpha.21380.1" />
</ItemGroup>
<ItemGroup>

7
samples/dapr/service-invocation/SentenceApp/Services/LowercaseServiceClient.cs

@ -1,5 +1,6 @@
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Dapr.Client;
using Shared;
@ -26,9 +27,9 @@ namespace SentenceApp.Services
public async Task<ConvertedResult> Convert(string sentence)
{
// Using Dapr sidecar and service invocation building block
return await _daprClient.InvokeMethodAsync<object, ConvertedResult>("lowercaseservice", "lowercase", new object(),
HttpInvocationOptions.UsingGet()
.WithQueryParam("sentence", sentence));
var req = _daprClient.CreateInvokeMethodRequest("lowercaseservice", $"lowercase?sentence={sentence}");
req.Method = HttpMethod.Get;
return await _daprClient.InvokeMethodAsync<ConvertedResult>(req);
// If you're using Tye alone (without dapr)
//var responseMessage = await _client.GetAsync($"/lowercase?sentence={sentence}");

7
samples/dapr/service-invocation/SentenceApp/Services/TitlecaseServiceClient.cs

@ -1,5 +1,6 @@
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Dapr.Client;
using Shared;
@ -26,9 +27,9 @@ namespace SentenceApp.Services
public async Task<ConvertedResult> Convert(string sentence)
{
// Using Dapr sidecar and service invocation building block
return await _daprClient.InvokeMethodAsync<object, ConvertedResult>("titlecaseservice", "titlecase", new object(),
HttpInvocationOptions.UsingGet()
.WithQueryParam("sentence", sentence));
var req = _daprClient.CreateInvokeMethodRequest("titlecaseservice", $"titlecase?sentence={sentence}");
req.Method = HttpMethod.Get;
return await _daprClient.InvokeMethodAsync<ConvertedResult>(req);
// If you're using Tye alone (without dapr)
//var responseMessage = await _client.GetAsync($"/titlecase?sentence={sentence}");

11
samples/dapr/service-invocation/SentenceApp/Services/UppercaseServiceClient.cs

@ -1,6 +1,9 @@
using System.Net.Http;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Dapr.Client;
using Shared;
@ -26,9 +29,9 @@ namespace SentenceApp.Services
public async Task<ConvertedResult> Convert(string sentence)
{
// Using Dapr sidecar and service invocation building block
return await _daprClient.InvokeMethodAsync<object, ConvertedResult>("uppercaseservice", "uppercase", new object(),
HttpInvocationOptions.UsingGet()
.WithQueryParam("sentence", sentence));
var req = _daprClient.CreateInvokeMethodRequest("uppercaseservice", $"uppercase?sentence={sentence}");
req.Method = HttpMethod.Get;
return await _daprClient.InvokeMethodAsync<ConvertedResult>(req);
// If you're using Tye alone (without dapr)
//var responseMessage = await _client.GetAsync($"/uppercase?sentence={sentence}");

11
samples/dapr/service-invocation/tye.yaml

@ -8,10 +8,6 @@ name: distributedtyedemo
extensions:
- name: dapr
# If you want to use a different tag or container port
# placement-image: daprio/dapr
# placement-container-port: 50005
# log-level configures the log level of the dapr sidecar
log-level: debug
@ -24,11 +20,8 @@ extensions:
# components-path configures the components path of the dapr sidecard
# components-path: "./components/"
# You can instruct Tye to not create the Dapr placement container on your behalf. This is required if you have Dapr running and want to use that container.
# Doing a `docker ps` can show if its already running. If it's running then you can set 'exclude-placement-container: true' with `placement-port: xxxx` set to the host port of that container.
# (i.e. In Windows + WSL2, Dapr uses 6050 as the host port)
# exclude-placement-container: true
# If not using the default Dapr placement service or otherwise using a placement service on a nonstandard port,
# you can configure the Dapr sidecar to use an explicit port.
# placement-port: 6050
services:
# uppercase service is a node app and is run via a dockerfile

5
src/Microsoft.Tye.Core/ApplicationBuilder.cs

@ -9,17 +9,20 @@ namespace Microsoft.Tye
{
public sealed class ApplicationBuilder
{
public ApplicationBuilder(FileInfo source, string name, ContainerEngine containerEngine)
public ApplicationBuilder(FileInfo source, string name, ContainerEngine containerEngine, int? dashboardPort)
{
Source = source;
Name = name;
ContainerEngine = containerEngine;
DashboardPort = dashboardPort;
}
public FileInfo Source { get; set; }
public string Name { get; set; }
public int? DashboardPort { get; set; }
public string? Namespace { get; set; }
public ContainerRegistry? Registry { get; set; }

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

@ -30,7 +30,7 @@ namespace Microsoft.Tye
var rootConfig = ConfigFactory.FromFile(source);
rootConfig.Validate();
var root = new ApplicationBuilder(source, rootConfig.Name!, new ContainerEngine(rootConfig.ContainerEngineType))
var root = new ApplicationBuilder(source, rootConfig.Name!, new ContainerEngine(rootConfig.ContainerEngineType), rootConfig.DashboardPort)
{
Namespace = rootConfig.Namespace
};
@ -403,6 +403,10 @@ namespace Microsoft.Tye
{
executable.EnvironmentVariables.Add(envVar);
}
else if (service is AzureFunctionServiceBuilder azureFunction)
{
azureFunction.EnvironmentVariables.Add(envVar);
}
else if (service is ExternalServiceBuilder)
{
throw new CommandException("External services do not support environment variables.");
@ -457,6 +461,7 @@ namespace Microsoft.Tye
Name = configBinding.Name,
Port = configBinding.Port,
Protocol = configBinding.Protocol ?? "http",
IPAddress = configBinding.IPAddress,
};
ingress.Bindings.Add(binding);
}

3
src/Microsoft.Tye.Core/AzureFunctionServiceBuilder.cs

@ -2,6 +2,8 @@
// 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.Collections.Generic;
namespace Microsoft.Tye
{
public class AzureFunctionServiceBuilder : ServiceBuilder
@ -17,5 +19,6 @@ namespace Microsoft.Tye
public string FunctionPath { get; }
public string? FuncExecutablePath { get; set; }
public string? ProjectFile { get; set; }
public List<EnvironmentVariableBuilder> EnvironmentVariables { get; } = new List<EnvironmentVariableBuilder>();
}
}

2
src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs

@ -24,6 +24,8 @@ namespace Microsoft.Tye.ConfigModel
public string? Name { get; set; }
public int? DashboardPort { get; set; }
public string? Namespace { get; set; }
public string? Registry { get; set; }

1
src/Microsoft.Tye.Core/ConfigModel/ConfigIngressBinding.cs

@ -9,5 +9,6 @@ namespace Microsoft.Tye.ConfigModel
public string? Name { get; set; }
public int? Port { get; set; }
public string? Protocol { get; set; } // HTTP or HTTPS
public string? IPAddress { get; set; } // Can be * or any address to listen on
}
}

3
src/Microsoft.Tye.Core/CoreStrings.resx

@ -153,6 +153,9 @@
<data name="MustBeAnInteger" xml:space="preserve">
<value>"{value}" value must be an integer.</value>
</data>
<data name="MustBeAnIPAddress" xml:space="preserve">
<value>"{value}" value must be an IP address, "*" or "localhost".</value>
</data>
<data name="MustBePositive" xml:space="preserve">
<value>"{value}" value cannot be negative.</value>
</data>

1
src/Microsoft.Tye.Core/IngressBindingBuilder.cs

@ -9,5 +9,6 @@ namespace Microsoft.Tye
public string? Name { get; set; }
public int? Port { get; set; }
public string? Protocol { get; set; } // HTTP or HTTPS
public string? IPAddress { get; set; }
}
}

4
src/Microsoft.Tye.Core/ProcessExtensions.cs

@ -11,7 +11,7 @@ using System.Runtime.InteropServices;
namespace Microsoft.Tye
{
internal static class ProcessExtensions
public static class ProcessExtensions
{
private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);
@ -97,7 +97,7 @@ namespace Microsoft.Tye
}
}
private static void RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string? stdout)
public static void RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string? stdout)
{
var startInfo = new ProcessStartInfo
{

10
src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs

@ -45,6 +45,16 @@ namespace Tye.Serialization
throw new TyeYamlException($"Unknown container engine: \"{engine}\"");
}
break;
case "dashboardPort":
if (int.TryParse(YamlParser.GetScalarValue(key, child.Value), out var dashboardPort))
{
app.DashboardPort = dashboardPort;
}
else
{
throw new TyeYamlException(child.Key.Start, CoreStrings.FormatMustBeAnInteger(key));
}
break;
case "ingress":
YamlParser.ThrowIfNotYamlSequence(key, child.Value);
ConfigIngressParser.HandleIngress((child.Value as YamlSequenceNode)!, app.Ingress);

8
src/Microsoft.Tye.Core/Serialization/ConfigExtensionsParser.cs

@ -16,13 +16,7 @@ namespace Tye.Serialization
switch (child.NodeType)
{
case YamlNodeType.Mapping:
var extensionDictionary = new Dictionary<string, object>();
foreach (var mapping in (YamlMappingNode)child)
{
var key = YamlParser.GetScalarValue(mapping.Key);
extensionDictionary[key] = YamlParser.GetScalarValue(key, mapping.Value)!;
}
var extensionDictionary = YamlParser.GetDictionary(child);
extensions.Add(extensionDictionary);
break;
default:

13
src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs

@ -2,7 +2,9 @@
// 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.Net;
using Microsoft.Tye.ConfigModel;
using YamlDotNet.RepresentationModel;
@ -144,6 +146,17 @@ namespace Tye.Serialization
binding.Port = port;
break;
case "ip":
if (YamlParser.GetScalarValue(key, child.Value) is string ipString
&& (IPAddress.TryParse(ipString, out var ip) || ipString == "*" || ipString.Equals("localhost", StringComparison.OrdinalIgnoreCase)))
{
binding.IPAddress = ipString;
}
else
{
throw new TyeYamlException(child.Value.Start, CoreStrings.FormatMustBeAnIPAddress(key));
}
break;
case "protocol":
binding.Protocol = YamlParser.GetScalarValue(key, child.Value);
break;

26
src/Microsoft.Tye.Core/Serialization/YamlParser.cs

@ -77,6 +77,32 @@ namespace Tye.Serialization
return app;
}
public static Dictionary<string, object> GetDictionary(YamlNode node)
{
if (node.NodeType != YamlNodeType.Mapping)
{
throw new TyeYamlException(node.Start,
CoreStrings.FormatUnexpectedType(YamlNodeType.Mapping.ToString(), node.NodeType.ToString()));
}
var dictionary = new Dictionary<string, object>();
foreach (var mapping in (YamlMappingNode)node)
{
var key = YamlParser.GetScalarValue(mapping.Key);
dictionary[key] = mapping.Value.NodeType switch
{
YamlNodeType.Scalar => YamlParser.GetScalarValue(key, mapping.Value)!,
YamlNodeType.Mapping => YamlParser.GetDictionary(mapping.Value),
_ => throw new TyeYamlException(mapping.Value.Start,
CoreStrings.FormatUnexpectedType(YamlNodeType.Mapping.ToString(), mapping.Value.NodeType.ToString()))
};
}
return dictionary;
}
public static string GetScalarValue(YamlNode node)
{
if (node.NodeType != YamlNodeType.Scalar)

149
src/Microsoft.Tye.Extensions/Dapr/DaprExtension.cs

@ -7,74 +7,23 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Microsoft.Tye.Extensions.Dapr
{
internal sealed class DaprExtension : Extension
{
public override Task ProcessAsync(ExtensionContext context, ExtensionConfiguration config)
public override async Task ProcessAsync(ExtensionContext context, ExtensionConfiguration config)
{
// If we're getting called then the user configured dapr in their tye.yaml.
// We don't have any of our own config.
if (context.Operation == ExtensionContext.OperationKind.LocalRun)
{
// default placement port number
var daprPlacementImage = "daprio/dapr";
var daprPlacementContainerPort = 50005;
var daprPlacementPort = NextPortFinder.GetNextPort();
var isCustomPlacementPortDefined = false;
// see if a placement port number has been defined
if (config.Data.TryGetValue("placement-port", out var obj) && obj?.ToString() is string && int.TryParse(obj.ToString(), out var customPlacementPort))
{
context.Output.WriteDebugLine($"Using Dapr placement service host port {customPlacementPort} from 'placement-port'");
daprPlacementPort = customPlacementPort;
isCustomPlacementPortDefined = true;
}
// see if a placement image has been defined
if (config.Data.TryGetValue("placement-image", out obj) && obj?.ToString() is string customPlacementImage)
{
context.Output.WriteDebugLine($"Using Dapr placement service image {customPlacementImage} from 'placement-image'");
daprPlacementImage = customPlacementImage;
}
// see if a placement container port has been defined
if (config.Data.TryGetValue("placement-container-port", out obj) && obj?.ToString() is string && int.TryParse(obj.ToString(), out var customPlacementContainerPort))
{
context.Output.WriteDebugLine($"Using Dapr placement service container port {customPlacementContainerPort} from 'placement-container-port'");
daprPlacementContainerPort = customPlacementContainerPort;
}
await VerifyDaprInitialized(context);
// We can only skip injecting a Dapr placement container if a 'placement-port' has been defined and 'exclude-placement-container=true'
if (!(isCustomPlacementPortDefined && config.Data.TryGetValue("exclude-placement-container", out obj) &&
obj?.ToString() is string excludePlacementContainer && excludePlacementContainer == "true"))
{
if (!isCustomPlacementPortDefined)
{
context.Output.WriteDebugLine("A 'placement-port' has not been defined. So the 'exclude-placement-container' will default to 'false'.");
}
context.Output.WriteDebugLine("Injecting Dapr placement service...");
var daprPlacement = new ContainerServiceBuilder("placement", daprPlacementImage, ServiceSource.Extension)
{
Args = "./placement",
Bindings = {
new BindingBuilder() {
Port = daprPlacementPort,
ContainerPort = daprPlacementContainerPort,
Protocol = "http"
}
}
};
context.Application.Services.Add(daprPlacement);
}
else
{
context.Output.WriteDebugLine("Skipping injecting Dapr placement service because 'exclude-placement-container=true'.");
}
var extensionConfiguration = DaprExtensionConfigurationReader.ReadConfiguration(config.Data);
// For local run, enumerate all projects, and add services for each dapr proxy.
var projects = context.Application.Services.OfType<ProjectServiceBuilder>().Cast<LaunchedServiceBuilder>();
@ -83,10 +32,25 @@ namespace Microsoft.Tye.Extensions.Dapr
foreach (var project in services)
{
// Dapr requires http. If this project isn't listening to HTTP then it's not daprized.
extensionConfiguration.Services.TryGetValue(project.Name, out DaprExtensionServiceConfiguration? serviceConfiguration);
if (serviceConfiguration?.Enabled != null && serviceConfiguration.Enabled.Value == false)
{
context.Output.WriteDebugLine($"Dapr has been disabled for service {project.Name}.");
continue;
}
var httpBinding = project.Bindings.FirstOrDefault(b => b.Protocol == "http");
if (httpBinding == null)
if (httpBinding == null && project.Bindings.Count == 1 && project.Bindings[0].Protocol == null)
{
// Assume the only untyped binding is HTTP...
httpBinding = project.Bindings[0];
}
if (httpBinding == null && (serviceConfiguration?.Enabled == null || !serviceConfiguration.Enabled.Value))
{
context.Output.WriteDebugLine($"Dapr has been disabled for unbound service {project.Name}.");
continue;
}
@ -110,12 +74,25 @@ namespace Microsoft.Tye.Extensions.Dapr
// These environment variables are replaced with environment variables
// defined for this service.
Args = $"run --app-id {project.Name} --app-port %APP_PORT% --dapr-grpc-port %DAPR_GRPC_PORT% --dapr-http-port %DAPR_HTTP_PORT% --metrics-port %METRICS_PORT% --placement-host-address localhost:{daprPlacementPort}",
Args = $"run --app-id {project.Name} --dapr-grpc-port %DAPR_GRPC_PORT% --dapr-http-port %DAPR_HTTP_PORT% --metrics-port %METRICS_PORT%",
};
if (httpBinding != null)
{
proxy.Args += $" --app-port %APP_PORT%";
}
var daprPlacementPort = serviceConfiguration?.PlacementPort ?? extensionConfiguration.PlacementPort;
if (daprPlacementPort.HasValue)
{
context.Output.WriteDebugLine($"Using Dapr placement service host port {daprPlacementPort.Value} from 'placement-port' for service {project.Name}.");
proxy.Args += $" --placement-host-address localhost:{daprPlacementPort.Value}";
}
// When running locally `-config` specifies a filename, not a configuration name. By convention
// we'll assume the filename and config name are the same.
if (config.Data.TryGetValue("config", out obj) && obj?.ToString() is string daprConfig)
if (config.Data.TryGetValue("config", out var obj) && obj?.ToString() is string daprConfig)
{
var configFile = Path.Combine(context.Application.Source.DirectoryName!, "components", $"{daprConfig}.yaml");
if (File.Exists(configFile))
@ -171,15 +148,18 @@ namespace Microsoft.Tye.Extensions.Dapr
};
proxy.Bindings.Add(metrics);
// Set APP_PORT based on the project's assigned port for http
var appPort = new EnvironmentVariableBuilder("APP_PORT")
if (httpBinding != null)
{
Source = new EnvironmentVariableSourceBuilder(project.Name, binding: httpBinding.Name)
// Set APP_PORT based on the project's assigned port for http
var appPort = new EnvironmentVariableBuilder("APP_PORT")
{
Kind = EnvironmentVariableSourceBuilder.SourceKind.Port,
},
};
proxy.EnvironmentVariables.Add(appPort);
Source = new EnvironmentVariableSourceBuilder(project.Name, binding: httpBinding.Name)
{
Kind = EnvironmentVariableSourceBuilder.SourceKind.Port,
},
};
proxy.EnvironmentVariables.Add(appPort);
}
// Set DAPR_GRPC_PORT based on this service's assigned port
var daprGrpcPort = new EnvironmentVariableBuilder("DAPR_GRPC_PORT")
@ -265,8 +245,43 @@ namespace Microsoft.Tye.Extensions.Dapr
}
}
}
}
private static Task VerifyDaprInitialized(ExtensionContext context)
{
return Task.Run(
() =>
{
string? stdout = null;
try
{
ProcessExtensions.RunProcessAndWaitForExit("dapr", "--version", TimeSpan.FromSeconds(10), out stdout);
}
catch
{
}
if (stdout != null)
{
var match = Regex.Match(stdout, "^Runtime version: (?<version>.+)$", RegexOptions.Multiline);
if (match.Success)
{
if (match.Groups["version"].Value == "n/a")
{
throw new CommandException("Dapr has not been initialized (e.g. via `dapr init`).");
}
else
{
// Some version of Dapr has been initialized...
return;
}
}
}
return Task.CompletedTask;
context.Output.WriteAlwaysLine($"Unable to determine whether Dapr has been installed and initialized (e.g. via `dapr init`).");
});
}
private string GetDaprExecutablePath()

24
src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfiguration.cs

@ -0,0 +1,24 @@
// 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.Collections.Generic;
namespace Microsoft.Tye.Extensions.Dapr
{
internal abstract class DaprExtensionCommonConfiguration
{
public int? PlacementPort { get; set; }
}
internal sealed class DaprExtensionServiceConfiguration : DaprExtensionCommonConfiguration
{
public bool? Enabled { get; set; }
}
internal sealed class DaprExtensionConfiguration : DaprExtensionCommonConfiguration
{
public IReadOnlyDictionary<string, DaprExtensionServiceConfiguration> Services { get; set; }
= new Dictionary<string, DaprExtensionServiceConfiguration>();
}
}

58
src/Microsoft.Tye.Extensions/Dapr/DaprExtensionConfigurationReader.cs

@ -0,0 +1,58 @@
// 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;
namespace Microsoft.Tye.Extensions.Dapr
{
internal static class DaprExtensionConfigurationReader
{
public static DaprExtensionConfiguration ReadConfiguration(IDictionary<string, object> rawConfiguration)
{
var configuration = new DaprExtensionConfiguration();
ReadCommonConfiguration(rawConfiguration, configuration);
if (rawConfiguration.TryGetValue("services", out var servicesObject) && servicesObject is Dictionary<string, object> rawServicesConfiguration)
{
var services = new Dictionary<string, DaprExtensionServiceConfiguration>();
foreach (var kvp in rawServicesConfiguration)
{
if (kvp.Value is Dictionary<string, object> rawServiceConfiguration)
{
var serviceConfiguration = new DaprExtensionServiceConfiguration();
ReadServiceConfiguration(rawServiceConfiguration, serviceConfiguration);
services.Add(kvp.Key, serviceConfiguration);
}
}
configuration.Services = services;
}
return configuration;
}
private static void ReadServiceConfiguration(IDictionary<string, object> rawConfiguration, DaprExtensionServiceConfiguration serviceConfiguration)
{
ReadCommonConfiguration(rawConfiguration, serviceConfiguration);
if (rawConfiguration.TryGetValue("enabled", out var obj) && obj is string && Boolean.TryParse(obj.ToString(), out var enabled))
{
serviceConfiguration.Enabled = enabled;
}
}
private static void ReadCommonConfiguration(IDictionary<string, object> rawConfiguration, DaprExtensionCommonConfiguration commonConfiguration)
{
if (rawConfiguration.TryGetValue("placement-port", out var obj) && obj?.ToString() is string && int.TryParse(obj.ToString(), out var customPlacementPort))
{
commonConfiguration.PlacementPort = customPlacementPort;
}
}
}
}

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

@ -242,7 +242,8 @@ namespace Microsoft.Tye.Hosting
if (docker.IsAspNet)
{
// 2. Configure ASP.NET Core to bind to those same ports
environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://*:{p.ContainerPort ?? p.Port}"));
var urlPorts = ports.Where(p => p.Protocol == null || p.Protocol == "http" || p.Protocol == "https");
environment["ASPNETCORE_URLS"] = string.Join(";", urlPorts.Select(p => $"{p.Protocol ?? "http"}://*:{p.ContainerPort ?? p.Port}"));
// Set the HTTPS port for the redirect middleware
foreach (var p in ports)

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

@ -76,7 +76,8 @@ namespace Microsoft.Tye.Hosting
var port = binding.ReplicaPorts[i];
ports.Add(port);
var url = $"{binding.Protocol}://localhost:{port}";
var url = $"{binding.Protocol}://{binding.IPAddress ?? "localhost"}:{port}";
urls.Add(url);
}

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

@ -12,13 +12,14 @@ namespace Microsoft.Tye.Hosting.Model
{
public class Application
{
public Application(string name, FileInfo source, Dictionary<string, Service> services, ContainerEngine containerEngine)
public Application(string name, FileInfo source, int? dashboardPort, Dictionary<string, Service> services, ContainerEngine containerEngine)
{
Name = name;
Source = source.FullName;
ContextDirectory = source.DirectoryName!;
Services = services;
ContainerEngine = containerEngine;
DashboardPort = dashboardPort;
}
public string Id { get; } = Guid.NewGuid().ToString();
@ -31,6 +32,8 @@ namespace Microsoft.Tye.Hosting.Model
public ContainerEngine ContainerEngine { get; set; }
public int? DashboardPort { get; set; }
public Dictionary<string, Service> Services { get; }
public Dictionary<object, object> Items { get; } = new Dictionary<object, object>();

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

@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Net;
namespace Microsoft.Tye.Hosting.Model
{
@ -13,6 +14,7 @@ namespace Microsoft.Tye.Hosting.Model
public int? Port { get; set; }
public int? ContainerPort { get; set; }
public string? Host { get; set; }
public string? IPAddress { get; set; }
public string? Protocol { get; set; }
public List<int> ReplicaPorts { get; } = new List<int>();
}

13
src/Microsoft.Tye.Hosting/Model/V1/V1Metric.cs

@ -0,0 +1,13 @@
// 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.Hosting.Model.V1
{
public class V1Metric
{
public string? Name { get; set; }
public string? Value { get; set; }
public V1MetricMetadata? Metadata { get; set; }
}
}

11
src/Microsoft.Tye.Hosting/Model/V1/V1MetricMetadata.cs

@ -0,0 +1,11 @@
// 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.Hosting.Model.V1
{
public class V1MetricMetadata
{
public string? Instance { get; set; }
}
}

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

@ -0,0 +1,14 @@
// 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.Collections.Generic;
namespace Microsoft.Tye.Hosting.Model.V1
{
public class V1ServiceMetrics
{
public string? Service { get; set; }
public List<V1Metric>? Metrics { get; set; }
}
}

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

@ -220,7 +220,8 @@ namespace Microsoft.Tye.Hosting
// 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"}://{application.ContainerEngine.AspNetUrlsHost}:{p.Port}"));
var urlPorts = ports.Where(p => p.Protocol == null || p.Protocol == "http" || p.Protocol == "https");
environment["ASPNETCORE_URLS"] = string.Join(";", urlPorts.Select(p => $"{p.Protocol ?? "http"}://{application.ContainerEngine.AspNetUrlsHost}:{p.Port}"));
// Set the HTTPS port for the redirect middleware
foreach (var p in ports)

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

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
@ -13,6 +14,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;
using Microsoft.Tye.Hosting.Model;
using Microsoft.Tye.Hosting.Model.V1;
@ -262,6 +264,52 @@ namespace Microsoft.Tye.Hosting
{
var app = context.RequestServices.GetRequiredService<Application>();
var acceptHeader = context.Request.Headers[HeaderNames.Accept];
if (acceptHeader == MediaTypeNames.Application.Json)
{
context.Response.ContentType = MediaTypeNames.Application.Json;
var servicesMetricsCollection = CreateServicesMetricsCollectionJson(app);
return JsonSerializer.SerializeAsync(context.Response.Body, servicesMetricsCollection, _options);
}
context.Response.ContentType = MediaTypeNames.Text.Plain;
var response = CreateServicesMetricsCollectionText(app);
return context.Response.WriteAsync(response);
}
private Task Metrics(HttpContext context)
{
var app = context.RequestServices.GetRequiredService<Application>();
var name = (string?)context.Request.RouteValues["name"];
if (string.IsNullOrEmpty(name) || !app.Services.TryGetValue(name, out var service))
{
context.Response.StatusCode = 404;
return JsonSerializer.SerializeAsync(context.Response.Body, new
{
message = $"Unknown service {name}"
},
_options);
}
var acceptHeader = context.Request.Headers[HeaderNames.Accept];
if (acceptHeader == MediaTypeNames.Application.Json)
{
context.Response.ContentType = MediaTypeNames.Application.Json;
var metricsCollectionJson = CreateMetricsCollectionJson(service);
return JsonSerializer.SerializeAsync(context.Response.Body, metricsCollectionJson, _options);
}
context.Response.ContentType = MediaTypeNames.Text.Plain;
var response = CreateMetricsCollectionText(service);
return context.Response.WriteAsync(response);
}
private static string CreateServicesMetricsCollectionText(Application app)
{
var sb = new StringBuilder();
foreach (var (serviceName, service) in app.Services.OrderBy(s => s.Key))
{
@ -283,28 +331,29 @@ namespace Microsoft.Tye.Hosting
sb.AppendLine();
}
return context.Response.WriteAsync(sb.ToString());
return sb.ToString();
}
private Task Metrics(HttpContext context)
private static List<V1ServiceMetrics> CreateServicesMetricsCollectionJson(Application app)
{
var app = context.RequestServices.GetRequiredService<Application>();
var sb = new StringBuilder();
var name = (string?)context.Request.RouteValues["name"];
context.Response.ContentType = "application/json";
if (string.IsNullOrEmpty(name) || !app.Services.TryGetValue(name, out var service))
var servicesMetrics = new List<V1ServiceMetrics>(app.Services.Count);
foreach (var (serviceName, service) in app.Services.OrderBy(s => s.Key))
{
context.Response.StatusCode = 404;
return JsonSerializer.SerializeAsync(context.Response.Body, new
var serviceMetrics = new V1ServiceMetrics
{
message = $"Unknown service {name}"
},
_options);
Service = serviceName,
Metrics = CreateMetricsCollectionJson(service)
};
servicesMetrics.Add(serviceMetrics);
}
return servicesMetrics;
}
private static string CreateMetricsCollectionText(Service service)
{
var sb = new StringBuilder();
foreach (var (instance, replica) in service.Replicas)
{
foreach (var (key, value) in replica.Metrics)
@ -319,7 +368,27 @@ namespace Microsoft.Tye.Hosting
}
}
return context.Response.WriteAsync(sb.ToString());
return sb.ToString();
}
private static List<V1Metric> CreateMetricsCollectionJson(Service service)
{
var metrics = new List<V1Metric>();
foreach (var (instance, replica) in service.Replicas)
{
foreach (var (key, value) in replica.Metrics)
{
var metric = new V1Metric
{
Name = key,
Value = value,
Metadata = new V1MetricMetadata { Instance = instance }
};
metrics.Add(metric);
}
}
return metrics;
}
}
}

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

@ -156,7 +156,7 @@ namespace Microsoft.Tye.Hosting
})
.ConfigureWebHostDefaults(builder =>
{
var port = ComputePort(options.Port);
var port = ComputePort(options.Port ?? application.DashboardPort);
_computedPort = port;
builder.Configure(ConfigureApplication)

8
src/schema/tye-schema.json

@ -26,6 +26,10 @@
"description": "The Docker network to use.",
"type": "string"
},
"dashboardPort": {
"description": "Configure the dashboard port used for `tye run`. Can be overridden using the `--port` CLI argument, and falls back to port 8000 if free, or a random port if 8000 is in use.",
"type": "integer"
},
"ingress": {
"description": "The application's ingresses.",
"type": "array",
@ -459,6 +463,10 @@
"protocol": {
"description": "The protocol used by the binding",
"type": "string"
},
"ip": {
"description": "The ip address the ingress listens on.",
"type": "string"
}
}
},

8
src/tye/ApplicationBuilderExtensions.cs

@ -151,6 +151,11 @@ namespace Microsoft.Tye
replicas = function.Replicas;
liveness = null;
readiness = null;
foreach (var entry in function.EnvironmentVariables)
{
env.Add(entry.ToHostingEnvironmentVariable());
}
}
else
{
@ -207,13 +212,14 @@ namespace Microsoft.Tye
Name = binding.Name,
Port = binding.Port,
Protocol = binding.Protocol,
IPAddress = binding.IPAddress,
});
}
services.Add(ingress.Name, new Service(description, ServiceSource.Host));
}
return new Application(application.Name, application.Source, services, application.ContainerEngine) { Network = application.Network };
return new Application(application.Name, application.Source, application.DashboardPort, services, application.ContainerEngine) { Network = application.Network };
}
public static Tye.Hosting.Model.EnvironmentVariable ToHostingEnvironmentVariable(this EnvironmentVariableBuilder builder)

9
test/E2ETest/Microsoft.Tye.E2ETests.csproj

@ -31,6 +31,13 @@
<Content Include="testassets\**\*" CopyToOutputDirectory="PreserveNewest" />
<Compile Remove="testassets\**\*" />
<None Remove="testassets\generate\apps-with-ingress.1.18.yaml" />
<None Remove="testassets\projects\non-standard-dashboard-port\test-project\appsettings.Development.json" />
<None Remove="testassets\projects\non-standard-dashboard-port\test-project\appsettings.json" />
<None Remove="testassets\projects\non-standard-dashboard-port\test-project\Properties\launchSettings.json" />
<None Remove="testassets\projects\non-standard-dashboard-port\tye.yaml" />
<None Remove="testassets\projects\non-standard-dashboard-port-5.0\test-project\appsettings.Development.json" />
<None Remove="testassets\projects\non-standard-dashboard-port-5.0\test-project\appsettings.json" />
<None Remove="testassets\projects\non-standard-dashboard-port-5.0\tye.yaml" />
<Compile Include="..\..\src\shared\KubectlDetector.cs" Link="KubectlDetector.cs" />
</ItemGroup>
@ -38,4 +45,4 @@
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.6.1" />
</ItemGroup>
</Project>
</Project>

184
test/E2ETest/TyeRunTests.cs

@ -9,6 +9,8 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
@ -767,6 +769,101 @@ services:
});
}
[Fact]
public async Task IngressSpecificIPTest()
{
var allIps = GetLiveIPAddresses().ToList();
var testIp = allIps[new Random().Next(allIps.Count)];
await TestIngressIP($"'{testIp}'", new[] { testIp }, allIps.Where(ip => ip != testIp).Take(1));
}
[Fact]
public async Task IngressAllIPv6Test()
{
var ipV6 = GetLiveIPAddresses(AddressFamily.InterNetworkV6).FirstOrDefault();
if (ipV6 == null) return;
await TestIngressIP($"'{IPAddress.IPv6Any}'", ipV6);
}
[Fact]
public async Task IngressAllIPv4Test()
{
var ipV4 = GetLiveIPAddresses(AddressFamily.InterNetwork).FirstOrDefault();
if (ipV4 == null) return;
var ipV6 = GetLiveIPAddresses(AddressFamily.InterNetworkV6).FirstOrDefault();
var failIp = ipV6 == null ? Enumerable.Empty<IPAddress>() : new[] { ipV6 };
await TestIngressIP($"'{IPAddress.Any}'", new[] { ipV4 }, failIp);
}
[Fact]
public async Task IngressAllIPTest()
{
await TestIngressIP($"'*'", GetLiveIPAddresses().FirstOrDefault());
}
private static IEnumerable<IPAddress> GetLiveIPAddresses(AddressFamily? family = null)
{
return from ni in NetworkInterface.GetAllNetworkInterfaces()
where ni.OperationalStatus == OperationalStatus.Up
let prop = ni.GetIPProperties()
from unicast in prop.UnicastAddresses
let addr = unicast.Address
where addr != IPAddress.IPv6Loopback && (family == null || addr.AddressFamily == family)
select addr;
}
private Task TestIngressIP(string ipSetting, params IPAddress[] mustAnswer) => TestIngressIP(ipSetting, mustAnswer, Enumerable.Empty<IPAddress>());
private async Task TestIngressIP(string ipSetting, IEnumerable<IPAddress> mustAnswer, IEnumerable<IPAddress> mustFail)
{
#if !DEBUG
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return; //disables running this test on windows as it stucks the test runner on the firewall open prompt
#endif
if (!mustAnswer.Any() && !mustFail.Any())
return; // no IP to test against
using var projectDirectory = CopyTestProjectDirectory("apps-with-ingress");
var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-ip_test.yaml"));
File.WriteAllText(projectFile.FullName, File.ReadAllText(projectFile.FullName).Replace("__TEST_IP_STRING__", ipSetting));
var outputContext = new OutputContext(_sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = true
};
var client = new HttpClient(new RetryHandler(handler));
await RunHostingApplication(application, new HostOptions(), async (app, uri) =>
{
foreach (var ip in mustAnswer.Concat(mustFail))
{
try
{
var ingressUri = await GetServiceUrl(client, uri, "ingress");
var reqUri = new UriBuilder(ingressUri + "/index.html")
{
Host = ip.ToString()
};
var htmlRequest = new HttpRequestMessage(HttpMethod.Get, reqUri.Uri);
htmlRequest.Headers.Host = "ui.example.com";
var htmlResponse = await client.SendAsync(htmlRequest);
htmlResponse.EnsureSuccessStatusCode();
}
catch (Exception) when (mustFail.Contains(ip))
{
// this is an expected failure
}
}
});
}
[ConditionalFact]
[SkipIfDockerNotRunning]
public async Task NginxIngressTest()
@ -1094,6 +1191,93 @@ services:
});
}
[ConditionalTheory]
[SkipIfDockerNotRunning]
[InlineData("non-standard-dashboard-port", "mcr.microsoft.com/dotnet/core/aspnet:3.1", 8005)]
[InlineData("non-standard-dashboard-port-5.0", "mcr.microsoft.com/dotnet/aspnet:5.0", 8006)]
public async Task RunUsesYamlDashboardPort(string projectName, string baseImage, int expectedDashboardPort)
{
using var projectDirectory = CopyTestProjectDirectory(projectName);
var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));
var outputContext = new OutputContext(_sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
Assert.Equal(expectedDashboardPort, application.DashboardPort);
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = false
};
var client = new HttpClient(new RetryHandler(handler));
await RunHostingApplication(application, new HostOptions() { Docker = true, }, async (app, uri) =>
{
// Make sure the dashboard is running on the expected port
Assert.Equal(expectedDashboardPort, uri.Port);
// Make sure we're running containers
Assert.True(app.Services.All(s => s.Value.Description.RunInfo is DockerRunInfo));
// Ensure correct image used
var dockerRunInfo = app.Services.Single().Value.Description.RunInfo as DockerRunInfo;
Assert.Equal(baseImage, dockerRunInfo?.Image);
// Ensure app runs
var testProjectUri = await GetServiceUrl(client, uri, "test-project");
var response = await client.GetAsync(testProjectUri);
Assert.True(response.IsSuccessStatusCode);
});
}
[ConditionalTheory]
[SkipIfDockerNotRunning]
[InlineData("non-standard-dashboard-port", "mcr.microsoft.com/dotnet/core/aspnet:3.1", 8005)]
[InlineData("non-standard-dashboard-port-5.0", "mcr.microsoft.com/dotnet/aspnet:5.0", 8006)]
public async Task RunCliPortOverridesYamlDashboardPort(string projectName, string baseImage, int tyeYamlDashboardPort)
{
var cliDashboardPort = 8008;
using var projectDirectory = CopyTestProjectDirectory(projectName);
var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"));
var outputContext = new OutputContext(_sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
Assert.Equal(tyeYamlDashboardPort, application.DashboardPort);
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
AllowAutoRedirect = false
};
var client = new HttpClient(new RetryHandler(handler));
await RunHostingApplication(application, new HostOptions() { Docker = true, Port = cliDashboardPort }, async (app, uri) =>
{
// Make sure the dashboard is running on the expected port passed from the CLI, not the value from tye.yaml
Assert.Equal(cliDashboardPort, uri.Port);
Assert.NotEqual(tyeYamlDashboardPort, uri.Port);
// Make sure we're running containers
Assert.True(app.Services.All(s => s.Value.Description.RunInfo is DockerRunInfo));
// Ensure correct image used
var dockerRunInfo = app.Services.Single().Value.Description.RunInfo as DockerRunInfo;
Assert.Equal(baseImage, dockerRunInfo?.Image);
// Ensure app runs
var testProjectUri = await GetServiceUrl(client, uri, "test-project");
var response = await client.GetAsync(testProjectUri);
Assert.True(response.IsSuccessStatusCode);
});
}
private async Task<string> GetServiceUrl(HttpClient client, Uri uri, string serviceName)
{
var serviceResult = await client.GetStringAsync($"{uri}api/v1/services/{serviceName}");

20
test/E2ETest/testassets/projects/apps-with-ingress/tye-ip_test.yaml

@ -0,0 +1,20 @@
# 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: apps-with-ingress-allip-ui
ingress:
- name: ingress
bindings:
- port: 8080
ip: __TEST_IP_STRING__
rules:
- host: ui.example.com
service: appC-ui
services:
- name: appC-ui
project: ApplicationC-UI/ApplicationC-UI.csproj
replicas: 2

34
test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/single-project.sln

@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test-project", "test-project\test-project.csproj", "{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|x64.ActiveCfg = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|x64.Build.0 = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|x86.ActiveCfg = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|x86.Build.0 = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|Any CPU.Build.0 = Release|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|x64.ActiveCfg = Release|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|x64.Build.0 = Release|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|x86.ActiveCfg = Release|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

30
test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/test-project/Program.cs

@ -0,0 +1,30 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace test_project
{
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>();
});
}
}

44
test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/test-project/Startup.cs

@ -0,0 +1,44 @@
// 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.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 test_project
{
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 =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
}
}

9
test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/test-project/appsettings.Development.json

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

10
test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/test-project/appsettings.json

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

8
test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/test-project/test-project.csproj

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp5.0</TargetFramework>
<RootNamespace>test_project</RootNamespace>
</PropertyGroup>
</Project>

7
test/E2ETest/testassets/projects/non-standard-dashboard-port-5.0/tye.yaml

@ -0,0 +1,7 @@
# tye application configuration file
# read all about it at https://github.com/dotnet/tye
name: non-standard-dashboard-port
dashboardPort: 8006
services:
- name: test-project
project: test-project/test-project.csproj

34
test/E2ETest/testassets/projects/non-standard-dashboard-port/single-project.sln

@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test-project", "test-project\test-project.csproj", "{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|x64.ActiveCfg = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|x64.Build.0 = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|x86.ActiveCfg = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Debug|x86.Build.0 = Debug|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|Any CPU.Build.0 = Release|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|x64.ActiveCfg = Release|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|x64.Build.0 = Release|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|x86.ActiveCfg = Release|Any CPU
{7D3606B2-7B8E-4ABB-BE0A-E0B18285D8F5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

30
test/E2ETest/testassets/projects/non-standard-dashboard-port/test-project/Program.cs

@ -0,0 +1,30 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace test_project
{
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/non-standard-dashboard-port/test-project/Properties/launchSettings.json

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

44
test/E2ETest/testassets/projects/non-standard-dashboard-port/test-project/Startup.cs

@ -0,0 +1,44 @@
// 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.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 test_project
{
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 =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
}
}

9
test/E2ETest/testassets/projects/non-standard-dashboard-port/test-project/appsettings.Development.json

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

10
test/E2ETest/testassets/projects/non-standard-dashboard-port/test-project/appsettings.json

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

8
test/E2ETest/testassets/projects/non-standard-dashboard-port/test-project/test-project.csproj

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>test_project</RootNamespace>
</PropertyGroup>
</Project>

7
test/E2ETest/testassets/projects/non-standard-dashboard-port/tye.yaml

@ -0,0 +1,7 @@
# tye application configuration file
# read all about it at https://github.com/dotnet/tye
name: non-standard-dashboard-port
dashboardPort: 8005
services:
- name: test-project
project: test-project/test-project.csproj

4
test/Test.Infrastructure/TestHelpers.cs

@ -251,8 +251,8 @@ namespace Test.Infrastructure
var processRunner = new ProcessRunner(logger, replicaRegistry, new ProcessRunnerOptions());
var dockerRunner = new DockerRunner(logger, replicaRegistry);
await processRunner.StartAsync(new Application(host.Application.Name, new FileInfo(host.Application.Source), new Dictionary<string, Service>(), ContainerEngine.Default));
await dockerRunner.StartAsync(new Application(host.Application.Name, new FileInfo(host.Application.Source), new Dictionary<string, Service>(), ContainerEngine.Default));
await processRunner.StartAsync(new Application(host.Application.Name, new FileInfo(host.Application.Source), null, new Dictionary<string, Service>(), ContainerEngine.Default));
await dockerRunner.StartAsync(new Application(host.Application.Name, new FileInfo(host.Application.Source), null, new Dictionary<string, Service>(), ContainerEngine.Default));
}
await DoOperationAndWaitForReplicasToChangeState(host, ReplicaState.Stopped, replicas.Length, replicas.ToHashSet(), new HashSet<string>(), TimeSpan.Zero, Purge);

Loading…
Cancel
Save