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