Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 3 additions & 11 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,12 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v5
with:
dotnet-version: '8.0.x'

- name: Add Garage Group NuGet Source
run: >
dotnet nuget add source ${{ vars.GG_NUGET_SOURCE_URL }}
-n garage
-u ${{ secrets.GG_NUGET_SOURCE_USER_NAME }}
-p ${{ secrets.GG_NUGET_SOURCE_USER_PASSWORD }}
--store-password-in-clear-text
dotnet-version: '10.0.x'

- name: Restore
run: dotnet restore
Expand Down
30 changes: 13 additions & 17 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,47 @@ on:
types:
- created

permissions:
contents: read
packages: write

jobs:
build-and-push-docker-image:
name: Build and push Docker image
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Set output
id: vars
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
run: echo "tag=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT"

- name: Setup .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v5
with:
dotnet-version: '8.0.x'
dotnet-version: '10.0.x'

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Add Garage Group NuGet Source
run: >
dotnet nuget add source ${{ vars.GG_NUGET_SOURCE_URL }}
-n garage
-u ${{ secrets.GG_NUGET_SOURCE_USER_NAME }}
-p ${{ secrets.GG_NUGET_SOURCE_USER_PASSWORD }}
--store-password-in-clear-text
uses: docker/setup-buildx-action@v4

- name: Publish Console.csproj
run: dotnet publish ./src/*/Console.csproj -c Release -o './publish'

- name: Login to Github Packages
uses: docker/login-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build image and push to GitHub Container Registry
uses: docker/build-push-action@v4
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
tags: |
ghcr.io/garagegroup/gg-ping-health-check:${{ steps.vars.outputs.tag }}
ghcr.io/garagegroup/gg-ping-health-check:latest
push: true
push: true
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/runtime:8.0
FROM mcr.microsoft.com/dotnet/runtime:10.0

WORKDIR /app
COPY ./publish ./
Expand Down
28 changes: 0 additions & 28 deletions Platform.Ping.sln

This file was deleted.

9 changes: 9 additions & 0 deletions Platform.Ping.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Solution>
<Folder Name="/.sln/">
<File Path="action.yml" />
<File Path="Dockerfile" />
<File Path="./.github/workflows/build.yml" />
<File Path="./.github/workflows/push.yml" />
</Folder>
<Project Path="src/Console/Console.csproj" />
</Solution>
15 changes: 10 additions & 5 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
name: 'gg-ping-health-check'
description: 'Ping web service health check'
description: 'Checks the health status of a web service by pinging its health check endpoint.'
inputs:
health_check_url:
description: 'Web service health check URL'
description: 'The URL of the web service health check endpoint.'
required: true
contains:
description: 'Expected message'
description: 'The expected text or pattern that must be present in the health check response.'
required: false
is_end_of_line_match:
description: 'Indicates whether the expected value should match only at the end of a line.'
required: false
default: 'false'
retry_delay_in_seconds:
description: 'Retry delay in seconds'
description: 'The delay, in seconds, between retry attempts.'
required: false
max_attempts:
description: 'Max number of attempts'
description: 'The maximum number of health check attempts before failing.'
required: false
runs:
using: 'composite'
Expand All @@ -20,6 +24,7 @@ runs:
docker run --rm \
-e In__HealthCheckUrl="${{ inputs.health_check_url }}" \
-e In__ContainedMessage="${{ inputs.contains }}" \
-e In__IsEndOfLineMatch="${{ inputs.is_end_of_line_match }}" \
-e In__RetryDelayInSeconds="${{ inputs.retry_delay_in_seconds }}" \
-e In__MaxAttempts="${{ inputs.max_attempts }}" \
ghcr.io/garagegroup/gg-ping-health-check:latest
Expand Down
1 change: 1 addition & 0 deletions src/Console/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ internal static Dependency<IHandler<PingIn, Unit>> UsePingHandler()
=>
PrimaryHandler.UseStandardSocketsHttpHandler()
.UseLogging("PingHandler")
.UseHttpApi()
.Map<IHandler<PingIn, Unit>>(PingHandler.Resolve);
}
9 changes: 6 additions & 3 deletions src/Console/Console.csproj
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<InvariantGlobalization>false</InvariantGlobalization>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NoWarn>$(NoWarn);IDE0130;CA1859;CA1873</NoWarn>
<RootNamespace>GarageGroup.Platform.Ping</RootNamespace>
<AssemblyName>GarageGroup.Platform.Ping.Console</AssemblyName>
</PropertyGroup>
Expand All @@ -15,8 +16,10 @@
<None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="GarageGroup.Infra.Handler.Console" Version="0.8.0" />
<ItemGroup>
<PackageReference Include="EarlyFuncPack.Core.AsyncPipeline" Version="0.3.0" />
<PackageReference Include="GarageGroup.Infra.Handler.Console" Version="0.14.1" />
<PackageReference Include="GarageGroup.Infra.Http.Api" Version="1.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
using System;

namespace GarageGroup.Platform.Ping;
namespace GarageGroup.Platform.Ping;

internal sealed record class PingIn
{
public Uri? HealthCheckUrl { get; init; }
public string? HealthCheckUrl { get; init; }

public string? ContainedMessage { get; init; }

public bool? IsEndOfLineMatch { get; init; }

public int? RetryDelayInSeconds { get; init; }

public int? MaxAttempts { get; init; }
Expand Down
69 changes: 39 additions & 30 deletions src/Console/Handler/Handler.Handler.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using GarageGroup.Infra;
Expand All @@ -9,20 +9,21 @@ namespace GarageGroup.Platform.Ping;

partial class PingHandler
{
public async ValueTask<Result<Unit, Failure<HandlerFailureCode>>> HandleAsync(PingIn? input, CancellationToken cancellationToken)
public async ValueTask<Result<Unit, Failure<HandlerFailureCode>>> HandleAsync(
PingIn? input, CancellationToken cancellationToken)
{
if (input?.HealthCheckUrl is null)
if (string.IsNullOrWhiteSpace(input?.HealthCheckUrl))
{
return Failure.Create(HandlerFailureCode.Persistent, "HealthCheckUrl must be specified");
return Failure.Create(HandlerFailureCode.Persistent, "HealthCheckUrl must be specified.");
}

var retryDelayInSeconds = input.RetryDelayInSeconds > 0 ? input.RetryDelayInSeconds.Value : DefaultRetryDelayInSeconds;
var maxAttempts = input.MaxAttempts > 0 ? input.MaxAttempts.Value : DefaultMaxAttempts;

for (var i = 0; i < maxAttempts; i++)
{
logger.LogInformation("Attempt {attemmpt} of {maxAttempts}", i + 1, maxAttempts);
var result = await PingAsync(input, cancellationToken).ConfigureAwait(false);
logger.LogInformation("Attempt {attemmpt} of {maxAttempts}.", i + 1, maxAttempts);
var result = await InnerPingAsync(input, cancellationToken);

if (result.IsSuccess)
{
Expand All @@ -32,43 +33,51 @@ public async ValueTask<Result<Unit, Failure<HandlerFailureCode>>> HandleAsync(Pi
var failure = result.FailureOrThrow();
logger.LogError(failure.SourceException, "{failureMessage}", failure.FailureMessage);

logger.LogInformation("Delay {delay}s", retryDelayInSeconds);
await Task.Delay(retryDelayInSeconds * 1000, cancellationToken).ConfigureAwait(false);
logger.LogInformation("Delay {delay}s.", retryDelayInSeconds);
await Task.Delay(retryDelayInSeconds * 1000, cancellationToken);
}

return Failure.Create(HandlerFailureCode.Transient, "All attempts were unsuccessful");
return Failure.Create(HandlerFailureCode.Transient, "All attempts were unsuccessful.");
}

private async ValueTask<Result<Unit, Failure<Unit>>> PingAsync(PingIn input, CancellationToken cancellationToken)
private ValueTask<Result<Unit, Failure<Unit>>> InnerPingAsync(PingIn input, CancellationToken cancellationToken)
=>
AsyncPipeline.Pipe(
input, cancellationToken)
.Pipe(
static @in => new HttpSendIn(HttpVerb.Get, @in.HealthCheckUrl.OrEmpty()))
.PipeValue(
httpApi.SendAsync)
.Map(
static success => success.Body.Content?.ToString(),
static failure => failure.ToStandardFailure("An unsuccessful HTTP request:").WithFailureCode<Unit>(default))
.Forward(
response => VerifyResponse(response.OrEmpty(), input));

private static Result<Unit, Failure<Unit>> VerifyResponse(string response, PingIn input)
{
try
Console.WriteLine(response);
if (string.IsNullOrWhiteSpace(input.ContainedMessage))
{
using var httpClient = new HttpClient(httpMessageHandler, disposeHandler: false);
using var httpResponse = await httpClient.GetAsync(input.HealthCheckUrl, cancellationToken).ConfigureAwait(false);

var httpResponseBody = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogInformation("{responseBody}", httpResponseBody);

if (httpResponse.IsSuccessStatusCode is false)
{
return Failure.Create("An unsuccessful HTTP request");
}

if (string.IsNullOrWhiteSpace(input.ContainedMessage))
{
return Result.Success<Unit>(default);
}
return Result.Success<Unit>(default);
}

if (httpResponseBody.Contains(input.ContainedMessage, StringComparison.InvariantCulture))
if (input.IsEndOfLineMatch is not true)
{
if (response.Contains(input.ContainedMessage, StringComparison.InvariantCultureIgnoreCase))
{
return Result.Success<Unit>(default);
}

return Failure.Create($"Response does not contain required message '{input.ContainedMessage}'");
return Failure.Create($"Response does not contain required message '{input.ContainedMessage}'.");
}
catch (OperationCanceledException ex)

var pattern = Regex.Escape(input.ContainedMessage) + "$";
if (Regex.IsMatch(response, pattern, RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
{
return ex.ToFailure("An HTTP request was canceled");
return Result.Success<Unit>(default);
}

return Failure.Create($"Response does not contain strict required message '{input.ContainedMessage}'.");
}
}
14 changes: 7 additions & 7 deletions src/Console/Handler/PingHandler.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
using System;
using System.Net.Http;
using GarageGroup.Infra;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace GarageGroup.Platform.Ping;

internal sealed partial class PingHandler : IPingHandler
{
public static PingHandler Resolve(IServiceProvider serviceProvider, HttpMessageHandler httpMessageHandler)
public static PingHandler Resolve(IServiceProvider serviceProvider, IHttpApi httpApi)
{
ArgumentNullException.ThrowIfNull(serviceProvider);
ArgumentNullException.ThrowIfNull(httpMessageHandler);
ArgumentNullException.ThrowIfNull(httpApi);

return new(
httpMessageHandler: httpMessageHandler,
httpApi: httpApi,
logger: serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<PingHandler>());
}

private const int DefaultRetryDelayInSeconds = 5;

private const int DefaultMaxAttempts = 3;

private readonly HttpMessageHandler httpMessageHandler;
private readonly IHttpApi httpApi;

private readonly ILogger logger;

private PingHandler(HttpMessageHandler httpMessageHandler, ILogger logger)
private PingHandler(IHttpApi httpApi, ILogger logger)
{
this.httpMessageHandler = httpMessageHandler;
this.httpApi = httpApi;
this.logger = logger;
}
}
6 changes: 3 additions & 3 deletions src/Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

namespace GarageGroup.Platform.Ping;

public static class Program
static class Program
{
public static Task Main(string[] args)
static Task Main(string[] args)
=>
Application.UsePingHandler().RunConsoleAsync("In", args);
Application.UsePingHandler().UseConsoleRunner("In", args).RunAsync();
}
Loading