From e23a6506e918cdaec21acc99ab70358e5122fa47 Mon Sep 17 00:00:00 2001 From: areller Date: Fri, 29 May 2020 14:29:49 -0400 Subject: [PATCH] Liveness and Readiness Documentation, Samples and Small Changes (#502) * sample app and beggining of recipe * fix link * sample, recipe and schema docs * change http failures to debug logs * add model validation to probes and http prober * add deserialization and validation tests * license * format * PR fixes * remove some Console.WriteLine's --- docs/recipes/probes.md | 165 ++++++++++++++++++ docs/reference/schema.md | 119 +++++++++++++ samples/liveness-and-readiness/tye.yaml | 20 +++ .../liveness-and-readiness/webapi/Program.cs | 30 ++++ .../webapi/Properties/launchSettings.json | 27 +++ .../liveness-and-readiness/webapi/Startup.cs | 107 ++++++++++++ .../webapi/appsettings.Development.json | 9 + .../webapi/appsettings.json | 10 ++ .../webapi/webapi.csproj | 5 + .../ConfigModel/ConfigApplication.cs | 20 ++- src/Microsoft.Tye.Hosting/ReplicaMonitor.cs | 18 +- test/UnitTests/TyeDeserializationTests.cs | 122 +++++++++++++ .../TyeDeserializationValidationTests.cs | 32 ++++ 13 files changed, 673 insertions(+), 11 deletions(-) create mode 100644 docs/recipes/probes.md create mode 100644 samples/liveness-and-readiness/tye.yaml create mode 100644 samples/liveness-and-readiness/webapi/Program.cs create mode 100644 samples/liveness-and-readiness/webapi/Properties/launchSettings.json create mode 100644 samples/liveness-and-readiness/webapi/Startup.cs create mode 100644 samples/liveness-and-readiness/webapi/appsettings.Development.json create mode 100644 samples/liveness-and-readiness/webapi/appsettings.json create mode 100644 samples/liveness-and-readiness/webapi/webapi.csproj diff --git a/docs/recipes/probes.md b/docs/recipes/probes.md new file mode 100644 index 00000000..fdaccb15 --- /dev/null +++ b/docs/recipes/probes.md @@ -0,0 +1,165 @@ +# Probes in Tye + +Just like in a [Kubernetes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) deployment, you can define `liveness` and `readiness` ports in Tye, as part of the service. + +Probes serve two main functions + +* Keeping traffic away from replicas which are not ready to receive it. +* Restarting replicas which are in a Bad/Unhealthy state. + +## Life cycle of a Replica + +Before we get to the definition of a probe and how it works, it's important to understand the life cycle of a replica. + +When using the local orchestrator (`tye run`), each replica in Tye can be in one of these three states + +* `Started` - A replica is in this state when it has just been started, and hasn't been probed yet. +* `Healthy` - A replica is in this state when it passes the `liveness` probe, but not the `readiness` probe. +* `Ready` - A replica is in this state when it passes both the `liveness` and `readiness` probes. + +(*Internally, there are more states that a replica can be in, but those are not relevant to this discussion*) + +The orchestrator is responsible for switching between the different states based on the feedback from the probes, as described in later sections. + +When deploying the application to Kubernetes (via `tye deploy`), the life cycle is represented differently and is managed by Kubernetes. Click [here](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/) to read more about the life cycle of a Pod in Kubernetes. + +Throughout the rest of the document, we'll be referring to the life cycle of the replica as it is represented in the local orchestrator (i.e. `Started`, `Healthy`, `Ready`). + +## Types of Probes + +There are two types of probes, both have a similar schema but serve a different purpose. + +### Liveness + +The `liveness` probe is used to let Tye know when it can restart a replica. + +When a replica is restarted due to a failed `liveness` probe, a new replica is created in its stead. + +### Readiness + +The `readiness` probe is used to let Tye know when it's okay to route traffic to a replica. + +The `readiness` probe cannot kill/restart a replica, it can only do two things + +* Promote `Healthy` replica to `Ready`, when the probe succeeds. +* Demote a `Ready` replica to `Healthy` when the probe fails. + +Tye only routes traffic (either via a service binding, or an ingress) to `Ready` replicas. + +## Life cycle of a Replica in the Absence of Probes + +By now, the life cycle of a replica when both `liveness` and `readiness` probes are configured should be clear, but how does the life cycle of a replica look like when both probes or one of the probes is absent? + +* When neither `liveness` nor `readiness` probes are present, a replica gets promoted from `Started` to `Ready` automatically, upon creation. +* When only the `liveness` probe is present, a replica gets promoted from `Started` to `Healthy` only after it passes the `liveness` probe, but upon being promoted to `Healthy`, it's automatically promoted again, to `Ready`. +* When only the `readiness` probe is present, a replica gets promoted from `Started` to `Healthy` automatically, upon creation, but gets promoted from `Healthy` to `Ready` only upon passing the `readiness` probe. + +## Running Locally with Liveness and Readiness Probes + +*The section will refer to this [sample application](/samples/liveness-and-readiness/) which demonstrates basic usage.* + +To run the sample locally, clone this repository or download the source and navigate to the `/samples/liveness-and-readiness/` directory in your terminal. + +``` +tye run +``` + +The sample has a single service with a `liveness` and a `readiness` probe, as shown in this snippet + +``` +services: + - name: simple-webapi + project: webapi/webapi.csproj + replicas: 2 + liveness: + http: + path: /healthy + readiness: + http: + path: /ready +``` + +The service is configured to respond successfully to both the `liveness` and `readiness` probes, so after executing `tye run`, you should see these log lines + +``` +[18:14:05 INF] Replica simple-webapi_a2d67bd9-4 is moving to an healthy state +[18:14:05 INF] Replica simple-webapi_a9c2e2f4-d is moving to an healthy state +[18:14:07 INF] Replica simple-webapi_a9c2e2f4-d is moving to a ready state +[18:14:07 INF] Replica simple-webapi_a2d67bd9-4 is moving to a ready state +``` + +As you can see, both replicas pass the `liveness` probe and get prompted to an `Healthy` state, and shortly after, both replica pass the `readiness` probe and get promoted to a `Ready` state. + +The sample application exposes an endpoint that allows you modify the responses that the `/healthy` and `/ready` endpoints, in order to see the probes in action. + +For example, if you send an *HTTP GET* to `http://localhost:8080/set?ready=false&timeout=10` or enter that address in the browser, +It will make `/ready` return *HTTP 500* for 10 seconds. + +Shortly after issuing that requests, you should see this log line in the terminal + +``` +[18:14:18 INF] Replica simple-webapi_a2d67bd9-4 is moving to an healthy state +``` + +meaning that the replica got demoted from `Ready` to `Healthy`, due to failing the `readiness` probe. + +After *about* 10 seconds, you should see this log line in the terminal + +``` +[18:14:26 INF] Replica simple-webapi_a2d67bd9-4 is moving to a ready state +``` + +meaning that the replica got demoted from `Healthy` to `Ready` again, due to passing the `readiness` probe. + +(*The reason it's a bit less than 10 seconds, is because Tye doesn't fail the probe immediately. It waits for a certain number of consecutive failures, as described in the schema document.*) + +You can use the same method to make the `liveness` probe fail, and watch as Tye restarts the replica. + +Send an *HTTP GET* request to this endpoint `http://localhost:8080/set?healthy=false` + + +And watch for this log line + +``` +[18:25:08 INF] Killing replica simple-webapi_a9c2e2f4-d because it has failed the liveness probe +``` + +Shortly after, you should see this log lines + +``` +[18:25:08 INF] Launching service simple-webapi_0e7fe12d-7 +[18:25:09 INF] Replica simple-webapi_0e7fe12d-7 is moving to an healthy state +[18:25:11 INF] Replica simple-webapi_0e7fe12d-7 is moving to a ready state +``` + +Showing that Tye launches a new replica instead of the replica that it has killed. + +## Deploying with Liveness and Readiness Probes + +When you deploy an application with `liveness` and/or `readiness` probes to Kubernetes, these probes get translated to their [equivalent representation in Kubernetes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) + +After running + +``` +tye deploy --interactive +``` + +You should see the `simple-webapi` deployment in Kubernetes + +``` +NAME READY UP-TO-DATE AVAILABLE AGE +simple-webapi 2/2 2 2 90s +``` + +Run + +``` +kubectl describe deploy simple-webapi +``` + +And you will notice that the deployment has `Liveness` and `Readiness` in its description + +``` +Liveness: http-get http://:80/healthy delay=0s timeout=1s period=1s #success=1 #failure=3 +Readiness: http-get http://:80/ready delay=0s timeout=1s period=1s #success=1 #failure=3 +``` \ No newline at end of file diff --git a/docs/reference/schema.md b/docs/reference/schema.md index 576f7b97..a14909a2 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -502,3 +502,122 @@ The path `/mypath` or `/mypath/` will match: #### `host` (`string`) The host to match. + +## Liveness and Readiness + +`liveness` and `readiness` elements appear within the properties of a `Service`. + +Tye uses the `liveness` and `readiness` to probe the replicas of a service. +Each replica of a service is probed independently. + +### Replica State + +Each replica of a service has three states it can be in + +* `Started` - A replica is in this state when it has just been started, and hasn't been probed yet. +* `Healthy` - A replica is in this state when it passes the `liveness` probe, but not the `readiness` probe. +* `Ready` - A replica is in this state when it passes both the `liveness` and `readiness` probes. + +`liveness` and `readiness` have similar schemas, but have different meaning to the life cycle of the service. + +If a `liveness` probe fails for a replica, Tye restarts that replica. +If a `readiness` probe fails for a replica, Tye doesn't restart that replica, but demotes the state of that replica from `Ready` to `Healthy`, until the probe becomes successful again. + +`Healthy` replicas are kept alive but Tye doesn't route traffic to them. (Neither via the service binding, nor via an ingress that routes to that service) + +`liveness` and `readiness` are optional, and may only be defined once per `Service`. + +### Liveness and Readiness Example + +``` +name: myapplication +services: + - name: webapi + project: webapi/webapi.csproj + replicas: 3 + liveness: + http: + path: /healthy + readiness: + http: + path: /ready +``` + +In this example, the `webapi` service has both a `liveness` probe and a `readiness` probe. +The `liveness` probe periodically calls the `/healthy` endpoint in the service and the `readiness` probe periodically calls the `/ready` endpoint in the service. + +### Probe Properties + +(This refers both to the `liveness` probe and the `readiness` probe, since both have similar properties) + +#### `http` (`HttpProber`) *required* + +The properties of the `HttpProber` that tell Tye how to probe a replica using HTTP. + +#### `period` (`integer`) + +The period (in seconds) in which Tye probes a replica. (Default value: `1`, Minimum value: `1`) + +#### `timeout` (`integer`) + +The time (in seconds) that Tye waits for a replica to respond to a probe, before the probe fails. (Default value: `1`, Minimum value: `1`) + +#### `successThreshold` (`integer`) *only relevant for readiness probe* + +Tye will wait for this number of successes from prober, before marking a `Healthy` replica as `Ready`. (Default value: `1`, Minimum value: `1`) + +#### `failureThreshold` (`integer`) + +Tye will wait for this number of failures from the prober, before giving up. (Default value: `3`, Minimum value: `1`) + +Giving up in the context of `liveness` probe means killing the replica, and in the context of `readiness` probe it means marking a `Ready` replica as `Healthy`. + +## HttpProber + +`HttpProber` appears within the `http` property of the `liveness` and `readiness` elements. +The `HttpProber` tells Tye which endpoint, port, protocol and which headers to use to probe the replicas of a service. + +### HttpProber Example + +``` +... +liveness: + # An HttpProber + http: + path: /healthy + port: 8080 + protocol: http + headers: + - name: HeaderA + value: ValueA + - name: HeaderB + value: ValueB + ... +... +``` + +In this example, the `liveness` probe defines an `HttpProber` that will probe the replicas of the service at the `/healthy` endpoint (*HTTP GET*), on port `8080`, using HTTP (*unsecure*), and providing two headers (`HeaderA` and `HeaderB`) with the values `ValueA` and `ValueB` respectively. + +### HttpProber Properties + +#### `path` (`string`) *required* + +Tye will probe the replicas of the service at that path, using the *GET* method. + +#### `port` (`integer`) + +The service binding port that is used to probe the replicas of the service. + +#### `protocol` (`string`) + +The service binding protocol that is used to probe the replicas of the server. (i.e. `http`/`https`) + +*Note: +If neither `port` nor `protocol` are provided, Tye selects the first binding of the service. +If just `port` is provided, Tye selects the first binding with that `port`. +If just `protocol` is provided, Tye selects the first binding with that `protocol`. +If both `port` and `protocol` are provided, Tye selects the first biding with that `port` and `protocol`.* + +#### `headers` (`(name, value)[]`) + +Array of headers that are sent as part of the HTTP request that probes the replicas of the service. \ No newline at end of file diff --git a/samples/liveness-and-readiness/tye.yaml b/samples/liveness-and-readiness/tye.yaml new file mode 100644 index 00000000..aa471576 --- /dev/null +++ b/samples/liveness-and-readiness/tye.yaml @@ -0,0 +1,20 @@ +name: liveness-and-readiness +ingress: + - name: ingress + bindings: + - port: 8080 + rules: + - path: / + service: simple-webapi +services: + - name: simple-webapi + project: webapi/webapi.csproj + replicas: 2 + liveness: + http: + path: /healthy + initialDelay: 1 + readiness: + http: + path: /ready + initialDelay: 1 \ No newline at end of file diff --git a/samples/liveness-and-readiness/webapi/Program.cs b/samples/liveness-and-readiness/webapi/Program.cs new file mode 100644 index 00000000..bd27786b --- /dev/null +++ b/samples/liveness-and-readiness/webapi/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 webapi +{ + 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(); + }); + } +} diff --git a/samples/liveness-and-readiness/webapi/Properties/launchSettings.json b/samples/liveness-and-readiness/webapi/Properties/launchSettings.json new file mode 100644 index 00000000..de5a95cb --- /dev/null +++ b/samples/liveness-and-readiness/webapi/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:31334", + "sslPort": 44363 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "webapi": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/liveness-and-readiness/webapi/Startup.cs b/samples/liveness-and-readiness/webapi/Startup.cs new file mode 100644 index 00000000..442d0652 --- /dev/null +++ b/samples/liveness-and-readiness/webapi/Startup.cs @@ -0,0 +1,107 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +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 webapi +{ + class SetDTO + { + public bool? Healthy { get; set; } + public bool? Ready { get; set; } + public int? Timeout{ get; set; } + } + + public class Startup + { + private string _id; + private bool _healthy; + private bool _ready; + + public Startup() + { + _id = Guid.NewGuid().ToString(); + _healthy = true; + _ready = true; + } + + // 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! Process Id: {_id}"); + }); + + endpoints.MapGet("/healthy", async context => + { + context.Response.StatusCode = _healthy ? 200 : 500; + await context.Response.WriteAsync($"Status Code: {context.Response.StatusCode}"); + }); + + endpoints.MapGet("/ready", async context => + { + context.Response.StatusCode = _ready ? 200 : 500; + await context.Response.WriteAsync($"Status Code: {context.Response.StatusCode}"); + }); + + // Should be technically POST/PUT, but it's just for tests... + endpoints.MapGet("/set", async context => + { + var query = context.Request.Query.ToDictionary(kv => kv.Key).ToDictionary(kv => kv.Key, kv => kv.Value.Value.First()); + + var originalHealthy = _healthy; + var originalReady = _ready; + + if (query.ContainsKey("healthy") && bool.TryParse(query["healthy"], out var healthy)) + { + _healthy = healthy; + } + + if (query.ContainsKey("ready") && bool.TryParse(query["ready"], out var ready)) + { + _ready = ready; + } + + if (query.ContainsKey("timeout") && int.TryParse(query["timeout"], out var timeout)) + { + var _ = Task.Delay(TimeSpan.FromSeconds(timeout)) + .ContinueWith(_ => + { + _healthy = originalHealthy; + _ready = originalReady; + }); + } + + await context.Response.WriteAsync("Done"); + }); + }); + } + } +} diff --git a/samples/liveness-and-readiness/webapi/appsettings.Development.json b/samples/liveness-and-readiness/webapi/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/samples/liveness-and-readiness/webapi/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/liveness-and-readiness/webapi/appsettings.json b/samples/liveness-and-readiness/webapi/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/samples/liveness-and-readiness/webapi/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/liveness-and-readiness/webapi/webapi.csproj b/samples/liveness-and-readiness/webapi/webapi.csproj new file mode 100644 index 00000000..fd91193e --- /dev/null +++ b/samples/liveness-and-readiness/webapi/webapi.csproj @@ -0,0 +1,5 @@ + + + netcoreapp3.1 + + \ No newline at end of file diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs index d795640c..114c33bd 100644 --- a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs @@ -139,9 +139,12 @@ namespace Microsoft.Tye.ConfigModel var probes = new[] { (Name: "liveness", Probe: service.Liveness), (Name: "readiness", Probe: service.Readiness) }.Where(p => p.Probe != null).ToArray(); foreach (var probe in probes) { - if (probe.Name == "liveness" && probe.Probe!.SuccessThreshold != 1) + context = new ValidationContext(probe.Probe); + if (!Validator.TryValidateObject(probe.Probe, context, results, validateAllProperties: true)) { - throw new TyeYamlException(CoreStrings.FormatSuccessThresholdMustBeOne(probe.Name)); + throw new TyeYamlException( + $"Probe '{probe.Name}' in service '{service.Name}' validation failed." + Environment.NewLine + + string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage))); } // right now only http is supported, so it must be set @@ -149,6 +152,19 @@ namespace Microsoft.Tye.ConfigModel { throw new TyeYamlException(CoreStrings.FormatProberRequired(probe.Name)); } + + context = new ValidationContext(probe.Probe!.Http); + if (!Validator.TryValidateObject(probe.Probe!.Http, context, results, validateAllProperties: true)) + { + throw new TyeYamlException( + $"Http in probe '{probe.Name}' in service '{service.Name}' validation failed." + Environment.NewLine + + string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage))); + } + + if (probe.Name == "liveness" && probe.Probe!.SuccessThreshold != 1) + { + throw new TyeYamlException(CoreStrings.FormatSuccessThresholdMustBeOne(probe.Name)); + } } } diff --git a/src/Microsoft.Tye.Hosting/ReplicaMonitor.cs b/src/Microsoft.Tye.Hosting/ReplicaMonitor.cs index 0ea8138f..b4446b92 100644 --- a/src/Microsoft.Tye.Hosting/ReplicaMonitor.cs +++ b/src/Microsoft.Tye.Hosting/ReplicaMonitor.cs @@ -230,19 +230,19 @@ namespace Microsoft.Tye.Hosting private void MoveToHealthy(ReplicaState from) { - _logger.LogDebug("Replica {name} is moving to an healthy state", _replica.Name); + _logger.LogInformation("Replica {name} is moving to an healthy state", _replica.Name); ChangeState(ReplicaState.Healthy); } private void MoveToReady() { - _logger.LogDebug("Replica {name} is moving to a ready state", _replica.Name); + _logger.LogInformation("Replica {name} is moving to a ready state", _replica.Name); ChangeState(ReplicaState.Ready); } private void Kill() { - _logger.LogDebug("Killing replica {name} because it has failed the liveness probe", _replica.Name); + _logger.LogInformation("Killing replica {name} because it has failed the liveness probe", _replica.Name); // it is assumed that a `Started` replica should have an initialized stopping token source _replica.StoppingTokenSource!.Cancel(); @@ -360,7 +360,7 @@ namespace Microsoft.Tye.Hosting var res = await _httpClient.SendAsync(req, timeoutCts.Token); if (!res.IsSuccessStatusCode) { - ShowWarning($"Replica {_replica.Name} failed http probe at address '{_httpProberSettings.Path}' due to a failed status ({res.StatusCode})"); + DebugWarning($"Replica {_replica.Name} failed http probe at address '{_httpProberSettings.Path}' due to a failed status ({res.StatusCode})"); Send(false); return; } @@ -369,12 +369,12 @@ namespace Microsoft.Tye.Hosting } catch (HttpRequestException ex) { - ShowWarning($"Replica {_replica.Name} failed http probe at address '{_httpProberSettings.Path}' due to an http exception", ex); + DebugWarning($"Replica {_replica.Name} failed http probe at address '{_httpProberSettings.Path}' due to an http exception", ex); Send(false); } catch (TaskCanceledException) { - ShowWarning($"Replica {_replica.Name} failed http probe at address '{_httpProberSettings.Path}' due to timeout"); + DebugWarning($"Replica {_replica.Name} failed http probe at address '{_httpProberSettings.Path}' due to timeout"); Send(false); } finally @@ -395,7 +395,7 @@ namespace Microsoft.Tye.Hosting _lastStatus = status; } - private void ShowWarning(string message, Exception? ex = null) + private void DebugWarning(string message, Exception? ex = null) { if (!_lastStatus) { @@ -404,11 +404,11 @@ namespace Microsoft.Tye.Hosting if (ex != null) { - _logger.LogWarning(ex, message); + _logger.LogDebug(ex, message); } else { - _logger.LogWarning(message); + _logger.LogDebug(message); } } diff --git a/test/UnitTests/TyeDeserializationTests.cs b/test/UnitTests/TyeDeserializationTests.cs index 8a476c16..a1c9dd8c 100644 --- a/test/UnitTests/TyeDeserializationTests.cs +++ b/test/UnitTests/TyeDeserializationTests.cs @@ -508,5 +508,127 @@ services: var exception = Assert.Throws(() => parser.ParseConfigApplication()); Assert.Contains(CoreStrings.FormatExpectedYamlSequence("env"), exception.Message); } + + [Theory] + [InlineData("liveness")] + [InlineData("readiness")] + public void Probe_UnrecognizedKey(string probe) + { + using var parser = new YamlParser($@" +services: + - name: sample + {probe}: + something: something"); + + var exception = Assert.Throws(() => parser.ParseConfigApplication()); + Assert.Contains(CoreStrings.FormatUnrecognizedKey("something"), exception.Message); + } + + [Theory] + [InlineData("initialDelay")] + [InlineData("period")] + [InlineData("timeout")] + [InlineData("successThreshold")] + [InlineData("failureThreshold")] + public void Probe_ScalarFields_MustBeInteger(string field) + { + using var parser = new YamlParser($@" +services: + - name: sample + liveness: + {field}: 3.5"); + + var exception = Assert.Throws(() => parser.ParseConfigApplication()); + Assert.Contains(CoreStrings.FormatMustBeAnInteger(field), exception.Message); + } + + [Theory] + [InlineData("initialDelay")] + public void Probe_ScalarFields_MustBePositive(string field) + { + using var parser = new YamlParser($@" +services: + - name: sample + liveness: + {field}: -1"); + + var exception = Assert.Throws(() => parser.ParseConfigApplication()); + Assert.Contains(CoreStrings.FormatMustBePositive(field), exception.Message); + } + + [Theory] + [InlineData("period")] + [InlineData("timeout")] + [InlineData("successThreshold")] + [InlineData("failureThreshold")] + public void Probe_ScalarFields_MustBeGreaterThanZero(string field) + { + using var parser = new YamlParser($@" +services: + - name: sample + liveness: + {field}: 0"); + + var exception = Assert.Throws(() => parser.ParseConfigApplication()); + Assert.Contains(CoreStrings.FormatMustBeGreaterThanZero(field), exception.Message); + } + + [Fact] + public void Probe_HttpProber_UnrecognizedKey() + { + using var parser = new YamlParser(@" +services: + - name: sample + liveness: + http: + something: something"); + + var exception = Assert.Throws(() => parser.ParseConfigApplication()); + Assert.Contains(CoreStrings.FormatUnrecognizedKey("something"), exception.Message); + } + + [Fact] + public void Probe_HttpProber_PortMustBeScalar() + { + using var parser = new YamlParser($@" +services: + - name: sample + liveness: + http: + port: 3.5"); + + var exception = Assert.Throws(() => parser.ParseConfigApplication()); + Assert.Contains(CoreStrings.FormatMustBeAnInteger("port"), exception.Message); + } + + [Fact] + public void Probe_HttpProber_HeadersMustBeSequence() + { + using var parser = new YamlParser(@" +services: + - name: sample + liveness: + http: + headers: abc"); + + var exception = Assert.Throws(() => parser.ParseConfigApplication()); + Assert.Contains(CoreStrings.FormatExpectedYamlSequence("headers"), exception.Message); + } + + [Fact] + public void Probe_HttpProber_Headers_UnrecognizedKey() + { + using var parser = new YamlParser(@" +services: + - name: sample + liveness: + http: + headers: + - name: header1 + something: something"); + + var exception = Assert.Throws(() => parser.ParseConfigApplication()); + Assert.Contains(CoreStrings.FormatUnrecognizedKey("something"), exception.Message); + } } } diff --git a/test/UnitTests/TyeDeserializationValidationTests.cs b/test/UnitTests/TyeDeserializationValidationTests.cs index e74f5fa5..2c813777 100644 --- a/test/UnitTests/TyeDeserializationValidationTests.cs +++ b/test/UnitTests/TyeDeserializationValidationTests.cs @@ -241,5 +241,37 @@ services: var exception = Assert.Throws(() => app.Validate()); Assert.Contains(errorMessage, exception.Message); } + + [Fact] + public void ProberRequired() + { + var input = @" +services: + - name: sample + liveness: + period: 1"; + var errorMessage = CoreStrings.FormatProberRequired("liveness"); + using var parser = new YamlParser(input); + var app = parser.ParseConfigApplication(); + var exception = Assert.Throws(() => app.Validate()); + Assert.Contains(errorMessage, exception.Message); + } + + [Fact] + public void LivenessProbeSuccessThresholdMustBeOne() + { + var input = @" +services: + - name: sample + liveness: + successThreshold: 2 + http: + path: /"; + var errorMessage = CoreStrings.FormatSuccessThresholdMustBeOne("liveness"); + using var parser = new YamlParser(input); + var app = parser.ParseConfigApplication(); + var exception = Assert.Throws(() => app.Validate()); + Assert.Contains(errorMessage, exception.Message); + } } }