Browse Source

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
jkotalik/fixAzureFunctions
areller 6 years ago
committed by GitHub
parent
commit
e23a6506e9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 165
      docs/recipes/probes.md
  2. 119
      docs/reference/schema.md
  3. 20
      samples/liveness-and-readiness/tye.yaml
  4. 30
      samples/liveness-and-readiness/webapi/Program.cs
  5. 27
      samples/liveness-and-readiness/webapi/Properties/launchSettings.json
  6. 107
      samples/liveness-and-readiness/webapi/Startup.cs
  7. 9
      samples/liveness-and-readiness/webapi/appsettings.Development.json
  8. 10
      samples/liveness-and-readiness/webapi/appsettings.json
  9. 5
      samples/liveness-and-readiness/webapi/webapi.csproj
  10. 20
      src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs
  11. 18
      src/Microsoft.Tye.Hosting/ReplicaMonitor.cs
  12. 122
      test/UnitTests/TyeDeserializationTests.cs
  13. 32
      test/UnitTests/TyeDeserializationValidationTests.cs

165
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
```

119
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.

20
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

30
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<Startup>();
});
}
}

27
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"
}
}
}
}

107
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");
});
});
}
}
}

9
samples/liveness-and-readiness/webapi/appsettings.Development.json

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

10
samples/liveness-and-readiness/webapi/appsettings.json

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

5
samples/liveness-and-readiness/webapi/webapi.csproj

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

20
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));
}
}
}

18
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);
}
}

122
test/UnitTests/TyeDeserializationTests.cs

@ -508,5 +508,127 @@ services:
var exception = Assert.Throws<TyeYamlException>(() => 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<TyeYamlException>(() => 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<TyeYamlException>(() => 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<TyeYamlException>(() => 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<TyeYamlException>(() => 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<TyeYamlException>(() => 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<TyeYamlException>(() => 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<TyeYamlException>(() => 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<TyeYamlException>(() => parser.ParseConfigApplication());
Assert.Contains(CoreStrings.FormatUnrecognizedKey("something"), exception.Message);
}
}
}

32
test/UnitTests/TyeDeserializationValidationTests.cs

@ -241,5 +241,37 @@ services:
var exception = Assert.Throws<TyeYamlException>(() => 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<TyeYamlException>(() => 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<TyeYamlException>(() => app.Validate());
Assert.Contains(errorMessage, exception.Message);
}
}
}

Loading…
Cancel
Save