public async Task ReadyServiceShouldRestartWhenLivenessFailsTests() { using var projectDirectory = CopyTestProjectDirectory("health-checks"); var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-all.yaml")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); await using var host = new TyeHost(application.ToHostingApplication(), new HostOptions()) { Sink = _sink, }; SetReplicasInitialState(host, true, true); await StartHostAndWaitForReplicasToStart(host, new[] { "health-all" }, ReplicaState.Ready); var replicasToBecomeReady = host.Application.Services["health-all"].Replicas.Select(r => r.Value).ToList(); var replicasNamesToBecomeReady = replicasToBecomeReady.Select(r => r.Name).ToHashSet(); var randomReplica = replicasToBecomeReady[new Random().Next(0, replicasToBecomeReady.Count)]; Assert.True(await DoOperationAndWaitForReplicasToRestart(host, new[] { randomReplica.Name }.ToHashSet(), replicasNamesToBecomeReady.Where(r => r != randomReplica.Name).ToHashSet(), TimeSpan.FromSeconds(1), async _ => { await SetHealthyReadyInReplica(randomReplica, healthy: false); })); }
public async Task HeadersTests() { using var projectDirectory = CopyTestProjectDirectory("health-checks"); var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-all.yaml")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); await using var host = new TyeHost(application.ToHostingApplication(), new HostOptions()) { Sink = _sink, }; SetReplicasInitialState(host, true, true); await StartHostAndWaitForReplicasToStart(host, new[] { "health-all" }, ReplicaState.Ready); var res = await _client.GetAsync($"http://localhost:{host.Application.Services["health-all"].Description.Bindings.First().Port}/livenessHeaders"); Assert.True(res.IsSuccessStatusCode); var headers = JsonSerializer.Deserialize <Dictionary <string, string> >(await res.Content.ReadAsStringAsync()); Assert.Equal("value1", headers["name1"]); Assert.Equal("value2", headers["name2"]); res = await _client.GetAsync($"http://localhost:{host.Application.Services["health-all"].Description.Bindings.First().Port}/readinessHeaders"); Assert.True(res.IsSuccessStatusCode); headers = JsonSerializer.Deserialize <Dictionary <string, string> >(await res.Content.ReadAsStringAsync()); Assert.Equal("value3", headers["name3"]); Assert.Equal("value4", headers["name4"]); }
public async Task ProbeShouldRespectTimeoutTests() { using var projectDirectory = CopyTestProjectDirectory("health-checks"); var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-all.yaml")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); await using var host = new TyeHost(application.ToHostingApplication(), new HostOptions()) { Sink = _sink, }; SetReplicasInitialState(host, true, true); await StartHostAndWaitForReplicasToStart(host, new[] { "health-all" }, ReplicaState.Ready); var replicasToBecomeReady = host.Application.Services["health-all"].Replicas.Select(r => r.Value).ToList(); var replicasNamesToBecomeReady = replicasToBecomeReady.Select(r => r.Name).ToHashSet(); var randomNumber = new Random().Next(0, replicasToBecomeReady.Count); var randomReplica1 = replicasToBecomeReady[randomNumber]; var randomReplica2 = replicasToBecomeReady[(randomNumber + 1) % replicasToBecomeReady.Count]; Assert.True(await DoOperationAndWaitForReplicasToChangeState(host, ReplicaState.Healthy, 1, new[] { randomReplica1.Name }.ToHashSet(), replicasNamesToBecomeReady.Where(r => r != randomReplica1.Name).ToHashSet(), TimeSpan.FromSeconds(2), async _ => { await Task.WhenAll(new[] { SetHealthyReadyInReplica(randomReplica1, readyDelay: 2), SetHealthyReadyInReplica(randomReplica2, readyDelay: 1) }); })); }
private void SetReplicasInitialState(TyeHost host, bool?healthy, bool?ready, string[]?services = null) { if (services == null) { services = host.Application.Services.Select(s => s.Key).ToArray(); } else { if (services.Any(s => !host.Application.Services.ContainsKey(s))) { throw new ArgumentException($"not all services given in {nameof(services)} exist"); } } foreach (var service in services) { if (healthy.HasValue) { host.Application.Services[service].Description.Configuration.Add(new EnvironmentVariable("healthy") { Value = "true" }); } if (ready.HasValue) { host.Application.Services[service].Description.Configuration.Add(new EnvironmentVariable("ready") { Value = "true" }); } } }
private static Command CreateRunCommand(string[] args) { var command = new Command("run", "run the application") { CommonArguments.Path_Required, }; // TODO: We'll need to support a --build-args command.AddOption(new Option("--no-build") { Description = "Do not build project files before running.", Required = false }); command.AddOption(new Option("--port") { Description = "The port to run control plane on.", Argument = new Argument <int>("port"), Required = false }); command.AddOption(new Option("--logs") { Description = "Write structured application logs to the specified log providers. Supported providers are console, elastic (Elasticsearch), ai (ApplicationInsights), seq.", Argument = new Argument <string>("logs"), Required = false }); command.AddOption(new Option("--dtrace") { Description = "Write distributed traces to the specified providers. Supported providers are zipkin.", Argument = new Argument <string>("logs"), Required = false }); command.AddOption(new Option("--debug") { Description = "Wait for debugger attach in all services.", Required = false }); command.Handler = CommandHandler.Create <IConsole, FileInfo>(async(console, path) => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (path is null) { throw new CommandException("No project or solution file was found."); } var application = ConfigFactory.FromFile(path); var serviceCount = application.Services.Count; InitializeThreadPoolSettings(serviceCount); using var host = new TyeHost(application.ToHostingApplication(), args); await host.RunAsync(); }); return(command); }
public async Task IngressShouldNotProxyToNonReadyReplicasTests() { using var projectDirectory = CopyTestProjectDirectory("health-checks"); var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-ingress.yaml")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); await using var host = new TyeHost(application.ToHostingApplication(), new HostOptions()) { Sink = _sink, }; SetReplicasInitialState(host, true, true); await StartHostAndWaitForReplicasToStart(host, new[] { "health-ingress-svc" }, ReplicaState.Ready); var replicasToBecomeReady = host.Application.Services["health-ingress-svc"].Replicas.Select(r => r.Value).ToList(); var ingressBinding = host.Application.Services.First(s => s.Value.Description.RunInfo is IngressRunInfo).Value.Description.Bindings.First(); var uniqueIdUrl = $"{ingressBinding.Protocol}://localhost:{ingressBinding.Port}/api/id"; var uniqueIds = await ProbeNumberOfUniqueReplicas(uniqueIdUrl); Assert.Equal(2, uniqueIds); var firstReplica = replicasToBecomeReady.First(); var secondReplica = replicasToBecomeReady.Skip(1).First(); await DoOperationAndWaitForReplicasToChangeState(host, ReplicaState.Healthy, 1, new[] { firstReplica.Name }.ToHashSet(), null, TimeSpan.Zero, async _ => { await SetHealthyReadyInReplica(firstReplica, ready: false); }); uniqueIds = await ProbeNumberOfUniqueReplicas(uniqueIdUrl); Assert.Equal(1, uniqueIds); await DoOperationAndWaitForReplicasToChangeState(host, ReplicaState.Healthy, 1, new[] { secondReplica.Name }.ToHashSet(), null, TimeSpan.Zero, async _ => { await SetHealthyReadyInReplica(secondReplica, ready: false); }); var res = await _client.GetAsync(uniqueIdUrl); Assert.Equal(HttpStatusCode.BadGateway, res.StatusCode); await DoOperationAndWaitForReplicasToChangeState(host, ReplicaState.Ready, 2, new[] { firstReplica.Name, secondReplica.Name }.ToHashSet(), null, TimeSpan.Zero, async _ => { await SetHealthyReadyInReplica(firstReplica, ready: true); await SetHealthyReadyInReplica(secondReplica, ready: true); }); uniqueIds = await ProbeNumberOfUniqueReplicas(uniqueIdUrl); Assert.Equal(2, uniqueIds); }
public static async Task <bool> DoOperationAndWaitForReplicasToChangeState(TyeHost host, ReplicaState desiredState, int n, HashSet <string> toChange, HashSet <string> rest, Func <ReplicaEvent, string> entitySelector, TimeSpan waitUntilSuccess, Func <TyeHost, Task> operation) { if (toChange != null && rest != null && rest.Overlaps(toChange)) { throw new ArgumentException($"{nameof(toChange)} and {nameof(rest)} can't overlap"); } var changedTask = new TaskCompletionSource <bool>(); var remaining = n; void OnReplicaChange(ReplicaEvent ev) { if (rest != null && rest.Contains(entitySelector(ev))) { changedTask !.TrySetResult(false); } else if ((toChange == null || toChange.Contains(entitySelector(ev))) && ev.State == desiredState) { Interlocked.Decrement(ref remaining); } if (remaining == 0) { Task.Delay(waitUntilSuccess) .ContinueWith(_ => { if (!changedTask !.Task.IsCompleted) { changedTask !.TrySetResult(remaining == 0); } }); } } var servicesStateObserver = host.Application.Services.Select(srv => srv.Value.ReplicaEvents.Subscribe(OnReplicaChange)).ToList(); await operation(host); using var cancellation = new CancellationTokenSource(WaitForServicesTimeout); try { await using (cancellation.Token.Register(() => changedTask.TrySetCanceled())) { return(await changedTask.Task); } } finally { foreach (var observer in servicesStateObserver) { observer.Dispose(); } } }
static async Task Purge(TyeHost host) { var logger = host.DashboardWebApplication !.Logger; var replicaRegistry = new ReplicaRegistry(host.Application.ContextDirectory, logger); var processRunner = new ProcessRunner(logger, replicaRegistry, new ProcessRunnerOptions()); var dockerRunner = new DockerRunner(logger, replicaRegistry); await processRunner.StartAsync(new Application(new FileInfo(host.Application.Source), new Dictionary <string, Service>())); await dockerRunner.StartAsync(new Application(new FileInfo(host.Application.Source), new Dictionary <string, Service>())); }
public async Task MultiProjectPurgeTest() { var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "multi-project")); using var tempDirectory = TempDirectory.Create(); DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); var tyeDir = new DirectoryInfo(Path.Combine(tempDirectory.DirectoryPath, ".tye")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var host = new TyeHost(application.ToHostingApplication(), Array.Empty <string>()) { Sink = _sink, }; try { await TestHelpers.StartHostAndWaitForReplicasToStart(host); try { var pids = GetAllAppPids(host.Application); var containers = GetAllContainerIds(host.Application); Assert.True(Directory.Exists(tyeDir.FullName)); Assert.Subset(new HashSet <int>(GetAllPids()), new HashSet <int>(pids)); Assert.Subset(new HashSet <string>(await DockerAssert.GetRunningContainersIdsAsync(_output)), new HashSet <string>(containers)); await TestHelpers.PurgeHostAndWaitForGivenReplicasToStop(host, GetAllReplicasNames(host.Application)); var runningPids = new HashSet <int>(GetAllPids()); Assert.True(pids.All(pid => !runningPids.Contains(pid))); var runningContainers = new HashSet <string>(await DockerAssert.GetRunningContainersIdsAsync(_output)); Assert.True(containers.All(c => !runningContainers.Contains(c))); } finally { await host.StopAsync(); } } finally { host.Dispose(); Assert.False(Directory.Exists(tyeDir.FullName)); } }
public async Task ServiceWithoutLivenessShouldDefaultToHealthyTests() { using var projectDirectory = CopyTestProjectDirectory("health-checks"); var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-readiness.yaml")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); await using var host = new TyeHost(application.ToHostingApplication(), new HostOptions()) { Sink = _sink, }; await StartHostAndWaitForReplicasToStart(host, new[] { "health-readiness" }, ReplicaState.Healthy); }
public static async Task StartHostAndWaitForReplicasToStart(TyeHost host) { var startedTask = new TaskCompletionSource <bool>(); var alreadyStarted = 0; var totalReplicas = host.Application.Services.Sum(s => s.Value.Description.Replicas); void OnReplicaChange(ReplicaEvent ev) { if (ev.State == ReplicaState.Started) { Interlocked.Increment(ref alreadyStarted); } else if (ev.State == ReplicaState.Stopped) { Interlocked.Decrement(ref alreadyStarted); } if (alreadyStarted == totalReplicas) { startedTask.TrySetResult(true); } } var servicesStateObserver = host.Application.Services.Select(srv => srv.Value.ReplicaEvents.Subscribe(OnReplicaChange)).ToList(); await host.StartAsync(); using var cancellation = new CancellationTokenSource(WaitForServicesTimeout); try { await using (cancellation.Token.Register(() => startedTask.TrySetCanceled())) { await startedTask.Task; } } catch (TaskCanceledException) { await host.StopAsync(); throw; } finally { foreach (var observer in servicesStateObserver) { observer.Dispose(); } } }
public static async Task StartHostAndWaitForReplicasToStart(TyeHost host, string[] services = null, ReplicaState desiredState = ReplicaState.Started) { if (services == null) { await DoOperationAndWaitForReplicasToChangeState(host, desiredState, host.Application.Services.Sum(s => s.Value.Description.Replicas), null, null, TimeSpan.Zero, h => h.StartAsync()); } else { if (services.Any(s => !host.Application.Services.ContainsKey(s))) { throw new ArgumentException($"not all services given in {nameof(services)} exist"); } await DoOperationAndWaitForReplicasToChangeState(host, desiredState, host.Application.Services.Where(s => services.Contains(s.Value.Description.Name)).Sum(s => s.Value.Description.Replicas), services.ToHashSet(), null, ev => ev.Replica.Service.Description.Name, TimeSpan.Zero, h => h.StartAsync()); } }
public async Task NullDebugTargetsDoesNotThrow() { using var projectDirectory = CopyTestProjectDirectory(Path.Combine("single-project", "test-project")); var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "test-project.csproj")); // 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); await using var host = new TyeHost(application.ToHostingApplication(), Array.Empty <string>()) { Sink = _sink, }; await host.StartAsync(); }
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 runningn containers Assert.True(host.Application.Services.All(s => s.Value.Description.RunInfo is DockerRunInfo)); var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (a, b, c, d) => true, AllowAutoRedirect = false }; var client = new HttpClient(new RetryHandler(handler)); var dashboardUri = new Uri(host.DashboardWebApplication !.Addresses.First()); await CheckServiceIsUp(host.Application, client, "backend", dashboardUri, timeout : TimeSpan.FromSeconds(60)); await CheckServiceIsUp(host.Application, client, "frontend", dashboardUri, timeout : TimeSpan.FromSeconds(60)); } finally { await host.StopAsync(); } }
public async Task NullDebugTargetsDoesNotThrow() { var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "single-project", "test-project")); using var tempDirectory = TempDirectory.Create(); DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "test-project.csproj")); // 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>()) { Sink = sink, }; await host.StartAsync(); await host.StopAsync(); }
public async Task ServicWithoutLivenessShouldBecomeReadyWhenReadyTests() { using var projectDirectory = CopyTestProjectDirectory("health-checks"); var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-readiness.yaml")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); await using var host = new TyeHost(application.ToHostingApplication(), new HostOptions()) { Sink = _sink, }; await StartHostAndWaitForReplicasToStart(host, new[] { "health-readiness" }, ReplicaState.Healthy); var replicas = host.Application.Services["health-readiness"].Replicas.Select(r => r.Value).ToList(); Assert.True(await DoOperationAndWaitForReplicasToChangeState(host, ReplicaState.Ready, replicas.Count, replicas.Select(r => r.Name).ToHashSet(), null, TimeSpan.Zero, async _ => { await Task.WhenAll(replicas.Select(r => SetHealthyReadyInReplica(r, ready: true))); })); }
public async Task FrontendBackendRunTest() { var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "frontend-backend")); using var tempDirectory = TempDirectory.Create(); 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(), Array.Empty <string>()) { Sink = sink, }; await host.StartAsync(); try { 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); await CheckServiceIsUp(host.Application, client, "frontend", dashboardUri); } finally { await host.StopAsync(); } }
private async Task RunHostingApplication(ApplicationBuilder application, string[] args, Func <Application, Uri, Task> execute) { await using var host = new TyeHost(application.ToHostingApplication(), args) { Sink = _sink, }; try { await StartHostAndWaitForReplicasToStart(host); var uri = new Uri(host.DashboardWebApplication !.Addresses.First()); await execute(host.Application, uri !); } finally { if (host.DashboardWebApplication != null) { var uri = new Uri(host.DashboardWebApplication !.Addresses.First()); using var client = new HttpClient(); foreach (var s in host.Application.Services.Values) { var logs = await client.GetStringAsync(new Uri(uri, $"/api/v1/logs/{s.Description.Name}")); _output.WriteLine($"Logs for service: {s.Description.Name}"); _output.WriteLine(logs); var description = await client.GetStringAsync(new Uri(uri, $"/api/v1/services/{s.Description.Name}")); _output.WriteLine($"Service defintion: {s.Description.Name}"); _output.WriteLine(description); } } } }
public async Task ProxyShouldNotProxyToNonReadyReplicasTests() { using var projectDirectory = CopyTestProjectDirectory("health-checks"); var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-proxy.yaml")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); await using var host = new TyeHost(application.ToHostingApplication(), new HostOptions()) { Sink = _sink, }; SetReplicasInitialState(host, true, true); await StartHostAndWaitForReplicasToStart(host, new[] { "health-proxy" }, ReplicaState.Ready); var replicasToBecomeReady = host.Application.Services["health-proxy"].Replicas.Select(r => r.Value).ToList(); // we assume that proxy will continue sending http request to the same replica var randomReplicaPortRes1 = await _client.GetAsync($"http://localhost:{host.Application.Services["health-proxy"].Description.Bindings.First().Port}/ports"); var randomReplicaPort1 = JsonSerializer.Deserialize <int[]>(await randomReplicaPortRes1.Content.ReadAsStringAsync()) ![0];
private async Task RunHostingApplication(ApplicationBuilder application, string[] args, TestOutputLogEventSink sink, Func <Uri, Task> execute) { using var host = new TyeHost(application.ToHostingApplication(), args) { Sink = sink, }; await StartHostAndWaitForReplicasToStart(host); var serviceApi = new Uri(host.DashboardWebApplication !.Addresses.First()); try { await execute(serviceApi !); } finally { using (var client = new HttpClient()) { // If we failed, there's a good chance the service isn't running. Let's get the logs either way and put // them in the output. foreach (var s in host.Application.Services.Values) { var request = new HttpRequestMessage(HttpMethod.Get, new Uri(serviceApi, $"/api/v1/logs/{s.Description.Name}")); var response = await client.SendAsync(request); var text = await response.Content.ReadAsStringAsync(); _output.WriteLine($"Logs for service: {s.Description.Name}"); _output.WriteLine(text); } } await host.StopAsync(); } }
public async Task FrontendBackendPurgeTest() { using var projectDirectory = CopySampleProjectDirectory("frontend-backend"); var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml")); var tyeDir = new DirectoryInfo(Path.Combine(projectDirectory.DirectoryPath, ".tye")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var host = new TyeHost(application.ToHostingApplication(), Array.Empty <string>()) { Sink = _sink, }; try { await TestHelpers.StartHostAndWaitForReplicasToStart(host); var pids = GetAllAppPids(host.Application); Assert.True(Directory.Exists(tyeDir.FullName)); Assert.Subset(new HashSet <int>(GetAllPids()), new HashSet <int>(pids)); await TestHelpers.PurgeHostAndWaitForGivenReplicasToStop(host, GetAllReplicasNames(host.Application)); var runningPids = new HashSet <int>(GetAllPids()); Assert.True(pids.All(pid => !runningPids.Contains(pid))); } finally { await host.DisposeAsync(); Assert.False(Directory.Exists(tyeDir.FullName)); } }
public static async Task PurgeHostAndWaitForGivenReplicasToStop(TyeHost host, string[] replicas) {
private static Command CreateRunCommand(string[] args) { var command = new Command("run", "run the application") { CommonArguments.Path_Required, }; // TODO: We'll need to support a --build-args command.AddOption(new Option("--no-build") { Description = "Do not build project files before running.", Required = false }); command.AddOption(new Option("--port") { Description = "The port to run control plane on.", Argument = new Argument <int>("port"), Required = false }); command.AddOption(new Option("--logs") { Description = "Write structured application logs to the specified log providers. Supported providers are console, elastic (Elasticsearch), ai (ApplicationInsights), seq.", Argument = new Argument <string>("logs"), Required = false }); command.AddOption(new Option("--dtrace") { Description = "Write distributed traces to the specified providers. Supported providers are zipkin.", Argument = new Argument <string>("logs"), Required = false }); command.AddOption(new Option("--debug") { Argument = new Argument <string[]>("service"), Description = "Wait for debugger attach to specific service. Specify \"*\" to wait for all services.", Required = false }); command.AddOption(new Option("--docker") { Description = "Run projects as docker containers.", Required = false }); command.Handler = CommandHandler.Create <IConsole, FileInfo, string[]>(async(console, path, debug) => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (path is null) { throw new CommandException("No project or solution file was found."); } var output = new OutputContext(console, Verbosity.Quiet); var application = await ApplicationFactory.CreateAsync(output, path); await application.ProcessExtensionsAsync(ExtensionContext.OperationKind.LocalRun); InitializeThreadPoolSettings(application.Services.Count); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); } await using var host = new TyeHost(application.ToHostingApplication(), args, debug); await host.RunAsync(); }); return(command); }
public async Task SingleProjectRunTest() { var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "single-project", "test-project")); using var tempDirectory = TempDirectory.Create(); DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "test-project.csproj")); using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), Array.Empty <string>()) { Sink = sink, }; await host.StartAsync(); try { var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (a, b, c, d) => true, AllowAutoRedirect = false }; 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 dashboardResponse = await client.GetStringAsync(dashboardUri); // Only one service for single application. var service = host.Application.Services.First().Value; var binding = service.Description.Bindings.First(); var protocol = binding.Protocol?.Length != 0 ? binding.Protocol : "http"; var hostName = binding.Host != null && binding.Host.Length != 0 ? binding.Host : "localhost"; var uriString = $"{protocol}://{hostName}:{binding.Port}"; // Confirm that the uri is in the dashboard response. Assert.Contains(uriString, dashboardResponse); var uriBackendProcess = new Uri(uriString); // 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(); } }
public static Task <bool> DoOperationAndWaitForReplicasToChangeState(TyeHost host, ReplicaState desiredState, int n, HashSet <string> toChange, HashSet <string> rest, TimeSpan waitUntilSuccess, Func <TyeHost, Task> operation) => DoOperationAndWaitForReplicasToChangeState(host, desiredState, n, toChange, rest, ev => ev.Replica.Name, waitUntilSuccess, operation);
public async Task ProxyShouldNotProxyToNonReadyReplicasTests() { using var projectDirectory = CopyTestProjectDirectory("health-checks"); var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-proxy.yaml")); var outputContext = new OutputContext(_sink, Verbosity.Debug); var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); await using var host = new TyeHost(application.ToHostingApplication(), new HostOptions()) { Sink = _sink, }; SetReplicasInitialState(host, true, true); await StartHostAndWaitForReplicasToStart(host, new[] { "health-proxy" }, ReplicaState.Ready); var replicasToBecomeReady = host.Application.Services["health-proxy"].Replicas.Select(r => r.Value).ToList(); // we assume that proxy will continue sending http request to the same replica var randomReplicaPortRes1 = await _client.GetAsync($"http://localhost:{host.Application.Services["health-proxy"].Description.Bindings.First().Port}/ports"); var randomReplicaPort1 = JsonSerializer.Deserialize <int[]>(await randomReplicaPortRes1.Content.ReadAsStringAsync())[0]; var randomReplica1 = replicasToBecomeReady.First(r => r.Bindings.Any(b => b.Port == randomReplicaPort1)); await DoOperationAndWaitForReplicasToChangeState(host, ReplicaState.Healthy, 1, new[] { randomReplica1.Name }.ToHashSet(), null, TimeSpan.Zero, async _ => { await SetHealthyReadyInReplica(randomReplica1, ready: false); }); var randomReplicaPortRes2 = await _client.GetAsync($"http://localhost:{host.Application.Services["health-proxy"].Description.Bindings.First().Port}/ports"); var randomReplicaPort2 = JsonSerializer.Deserialize <int[]>(await randomReplicaPortRes2.Content.ReadAsStringAsync())[0]; var randomReplica2 = replicasToBecomeReady.First(r => r.Bindings.Any(b => b.Port == randomReplicaPort2)); Assert.NotEqual(randomReplicaPort1, randomReplicaPort2); await DoOperationAndWaitForReplicasToChangeState(host, ReplicaState.Healthy, 1, new[] { randomReplica2.Name }.ToHashSet(), null, TimeSpan.Zero, async _ => { await SetHealthyReadyInReplica(randomReplica2, ready: false); }); try { var resShouldFail = await _client.GetAsync($"http://localhost:{host.Application.Services["health-proxy"].Description.Bindings.First().Port}/ports"); Assert.False(resShouldFail.IsSuccessStatusCode); } catch (HttpRequestException) { } await DoOperationAndWaitForReplicasToChangeState(host, ReplicaState.Ready, 1, new[] { randomReplica2.Name }.ToHashSet(), null, TimeSpan.Zero, async _ => { await SetHealthyReadyInReplica(randomReplica2, ready: true); }); var randomReplicaPortRes3 = await _client.GetAsync($"http://localhost:{host.Application.Services["health-proxy"].Description.Bindings.First().Port}/ports"); var randomReplicaPort3 = JsonSerializer.Deserialize <int[]>(await randomReplicaPortRes3.Content.ReadAsStringAsync())[0]; Assert.Equal(randomReplicaPort3, randomReplicaPort2); }
public async Task IngressRunTest() { var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "apps-with-ingress")); using var tempDirectory = TempDirectory.Create(); 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(), Array.Empty <string>()) { Sink = sink, }; var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (a, b, c, d) => true, AllowAutoRedirect = false }; using var client = new HttpClient(new RetryHandler(handler)); await host.StartAsync(); var serviceApi = new Uri(host.DashboardWebApplication !.Addresses.First()); try { var ingressService = await client.GetStringAsync($"{serviceApi}api/v1/services/ingress"); var service = JsonSerializer.Deserialize <V1Service>(ingressService, _options); var binding = service.Description !.Bindings.Single(); var ingressUri = $"http://localhost:{binding.Port}"; var responseA = await client.GetAsync(ingressUri + "/A"); var responseB = await client.GetAsync(ingressUri + "/B"); Assert.StartsWith("Hello from Application A", await responseA.Content.ReadAsStringAsync()); Assert.StartsWith("Hello from Application B", await responseB.Content.ReadAsStringAsync()); var requestA = new HttpRequestMessage(HttpMethod.Get, ingressUri); requestA.Headers.Host = "a.example.com"; var requestB = new HttpRequestMessage(HttpMethod.Get, ingressUri); requestB.Headers.Host = "b.example.com"; responseA = await client.SendAsync(requestA); responseB = await client.SendAsync(requestB); 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(); } }
public async Task SingleProjectRunTest() { var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "single-project", "test-project")); using var tempDirectory = TempDirectory.Create(); DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); 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 { ServerCertificateCustomValidationCallback = (a, b, c, d) => true, AllowAutoRedirect = false }; 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"); 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}"); // 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(); } }
public static Task <bool> DoOperationAndWaitForReplicasToRestart(TyeHost host, HashSet <string> toRestart, HashSet <string> rest, TimeSpan waitUntilSuccess, Func <TyeHost, Task> operation) => DoOperationAndWaitForReplicasToRestart(host, toRestart, rest, ev => ev.Replica.Name, waitUntilSuccess, operation);
private static Command CreateRunCommand() { var command = new Command("run", "run the application") { CommonArguments.Path_Required, new Option("--no-build") { Description = "Do not build project files before running.", Required = false }, new Option("--port") { Description = "The port to run control plane on.", Argument = new Argument <int?>("port"), Required = false }, new Option("--logs") { Description = "Write structured application logs to the specified log provider. Supported providers are 'console', 'elastic' (Elasticsearch), 'ai' (ApplicationInsights), 'seq'.", Argument = new Argument <string>("logs"), Required = false }, new Option("--dtrace") { Description = "Write distributed traces to the specified tracing provider. Supported providers are 'zipkin'.", Argument = new Argument <string>("trace"), Required = false, }, new Option("--metrics") { Description = "Write metrics to the specified metrics provider.", Argument = new Argument <string>("metrics"), Required = false }, new Option("--debug") { Argument = new Argument <string[]>("service") { Arity = ArgumentArity.ZeroOrMore, }, Description = "Wait for debugger attach to specific service. Specify \"*\" to wait for all services.", Required = false }, new Option("--docker") { Description = "Run projects as docker containers.", Required = false }, new Option("--dashboard") { Description = "Launch dashboard on run.", Required = false }, new Option("--watch") { Description = "Watches for code changes for all dotnet projects.", Required = false }, StandardOptions.Framework, StandardOptions.Tags, StandardOptions.Verbosity, }; command.Handler = CommandHandler.Create <RunCommandArguments>(async args => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (args.Path is null) { throw new CommandException("No project or solution file was found."); } var output = new OutputContext(args.Console, args.Verbosity); output.WriteInfoLine("Loading Application Details..."); var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(args.Tags); var application = await ApplicationFactory.CreateAsync(output, args.Path, args.Framework, filter); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); } var options = new HostOptions() { Dashboard = args.Dashboard, Docker = args.Docker, NoBuild = args.NoBuild, Port = args.Port, // parsed later by the diagnostics code DistributedTraceProvider = args.Dtrace, LoggingProvider = args.Logs, MetricsProvider = args.Metrics, LogVerbosity = args.Verbosity, Watch = args.Watch }; options.Debug.AddRange(args.Debug); await application.ProcessExtensionsAsync(options, output, ExtensionContext.OperationKind.LocalRun); InitializeThreadPoolSettings(application.Services.Count); output.WriteInfoLine("Launching Tye Host..."); output.WriteInfoLine(string.Empty); await using var host = new TyeHost(application.ToHostingApplication(), options); await host.RunAsync(); }); return(command); }