audit, advisories and doctors/setup work
This commit is contained in:
@@ -37,6 +37,7 @@ internal sealed class RuntimeEventsClient : IRuntimeEventsClient
|
||||
private readonly IOptionsMonitor<ZastavaRuntimeOptions> runtimeOptions;
|
||||
private readonly IOptionsMonitor<ZastavaObserverOptions> observerOptions;
|
||||
private readonly IZastavaRuntimeMetrics runtimeMetrics;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly ILogger<RuntimeEventsClient> logger;
|
||||
|
||||
public RuntimeEventsClient(
|
||||
@@ -45,6 +46,7 @@ internal sealed class RuntimeEventsClient : IRuntimeEventsClient
|
||||
IOptionsMonitor<ZastavaRuntimeOptions> runtimeOptions,
|
||||
IOptionsMonitor<ZastavaObserverOptions> observerOptions,
|
||||
IZastavaRuntimeMetrics runtimeMetrics,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<RuntimeEventsClient> logger)
|
||||
{
|
||||
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
@@ -52,6 +54,7 @@ internal sealed class RuntimeEventsClient : IRuntimeEventsClient
|
||||
this.runtimeOptions = runtimeOptions ?? throw new ArgumentNullException(nameof(runtimeOptions));
|
||||
this.observerOptions = observerOptions ?? throw new ArgumentNullException(nameof(observerOptions));
|
||||
this.runtimeMetrics = runtimeMetrics ?? throw new ArgumentNullException(nameof(runtimeMetrics));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -109,7 +112,7 @@ internal sealed class RuntimeEventsClient : IRuntimeEventsClient
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
var retryAfter = ParseRetryAfter(response.Headers.RetryAfter) ?? TimeSpan.FromSeconds(5);
|
||||
var retryAfter = ParseRetryAfter(response.Headers.RetryAfter, timeProvider) ?? TimeSpan.FromSeconds(5);
|
||||
logger.LogWarning("Runtime events publish rate limited (batchId={BatchId}, retryAfter={RetryAfter}).", request.BatchId, retryAfter);
|
||||
return RuntimeEventPublishResult.FromRateLimit(retryAfter);
|
||||
}
|
||||
@@ -159,8 +162,10 @@ internal sealed class RuntimeEventsClient : IRuntimeEventsClient
|
||||
runtimeMetrics.BackendLatencyMs.Record(elapsedMs, tags);
|
||||
}
|
||||
|
||||
private static TimeSpan? ParseRetryAfter(RetryConditionHeaderValue? retryAfter)
|
||||
internal static TimeSpan? ParseRetryAfter(RetryConditionHeaderValue? retryAfter, TimeProvider timeProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||
|
||||
if (retryAfter is null)
|
||||
{
|
||||
return null;
|
||||
@@ -173,21 +178,21 @@ internal sealed class RuntimeEventsClient : IRuntimeEventsClient
|
||||
|
||||
if (retryAfter.Date.HasValue)
|
||||
{
|
||||
var delta = retryAfter.Date.Value.UtcDateTime - DateTime.UtcNow;
|
||||
var delta = retryAfter.Date.Value - timeProvider.GetUtcNow();
|
||||
return delta > TimeSpan.Zero ? delta : TimeSpan.Zero;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string Truncate(string? value, int maxLength = 512)
|
||||
internal static string Truncate(string? value, int maxLength = 512)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return value.Length <= maxLength ? value : value[..maxLength] + "…";
|
||||
return value.Length <= maxLength ? value : value[..maxLength] + "...";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,27 +19,37 @@ internal sealed class DockerWindowsRuntimeClient : IWindowsContainerRuntimeClien
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private const string DefaultPipeName = "docker_engine";
|
||||
internal const string DefaultPipeName = "docker_engine";
|
||||
internal static readonly TimeSpan DefaultConnectTimeout = TimeSpan.FromSeconds(5);
|
||||
internal static readonly Uri DefaultBaseAddress = new("http://localhost/");
|
||||
|
||||
private readonly string _pipeName;
|
||||
private readonly TimeSpan _connectTimeout;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<DockerWindowsRuntimeClient> _logger;
|
||||
private bool _disposed;
|
||||
|
||||
public DockerWindowsRuntimeClient(
|
||||
HttpClient httpClient,
|
||||
ILogger<DockerWindowsRuntimeClient> logger,
|
||||
string? pipeName = null,
|
||||
TimeSpan? connectTimeout = null)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_pipeName = pipeName ?? DefaultPipeName;
|
||||
_connectTimeout = connectTimeout ?? TimeSpan.FromSeconds(5);
|
||||
_httpClient = CreateHttpClient(_pipeName, _connectTimeout);
|
||||
_connectTimeout = connectTimeout ?? DefaultConnectTimeout;
|
||||
}
|
||||
|
||||
private static HttpClient CreateHttpClient(string pipeName, TimeSpan connectTimeout)
|
||||
internal static SocketsHttpHandler CreateNamedPipeHandler(string pipeName, TimeSpan connectTimeout)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pipeName))
|
||||
{
|
||||
throw new ArgumentException("Pipe name must be provided.", nameof(pipeName));
|
||||
}
|
||||
|
||||
if (connectTimeout <= TimeSpan.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(connectTimeout), "Connect timeout must be positive.");
|
||||
}
|
||||
|
||||
var handler = new SocketsHttpHandler
|
||||
{
|
||||
ConnectCallback = async (context, cancellationToken) =>
|
||||
@@ -54,12 +64,7 @@ internal sealed class DockerWindowsRuntimeClient : IWindowsContainerRuntimeClien
|
||||
},
|
||||
ConnectTimeout = connectTimeout
|
||||
};
|
||||
|
||||
return new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("http://localhost/"),
|
||||
Timeout = Timeout.InfiniteTimeSpan
|
||||
};
|
||||
return handler;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
|
||||
|
||||
@@ -332,7 +332,7 @@ public sealed record ContainerEvent
|
||||
/// <summary>
|
||||
/// Event timestamp.
|
||||
/// </summary>
|
||||
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Zastava.Observer.Backend;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Zastava.Observer.Tests.Backend;
|
||||
|
||||
public sealed class RuntimeEventsClientTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParseRetryAfter_WithDelta_ReturnsDelta()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
var delta = TimeSpan.FromSeconds(12);
|
||||
var header = new RetryConditionHeaderValue(delta);
|
||||
|
||||
var result = RuntimeEventsClient.ParseRetryAfter(header, timeProvider);
|
||||
|
||||
Assert.Equal(delta, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRetryAfter_WithDate_UsesTimeProvider()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
var header = new RetryConditionHeaderValue(timeProvider.GetUtcNow().AddSeconds(30));
|
||||
|
||||
var result = RuntimeEventsClient.ParseRetryAfter(header, timeProvider);
|
||||
|
||||
Assert.Equal(TimeSpan.FromSeconds(30), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseRetryAfter_WithPastDate_ReturnsZero()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 30, TimeSpan.Zero));
|
||||
var header = new RetryConditionHeaderValue(timeProvider.GetUtcNow().AddSeconds(-10));
|
||||
|
||||
var result = RuntimeEventsClient.ParseRetryAfter(header, timeProvider);
|
||||
|
||||
Assert.Equal(TimeSpan.Zero, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Truncate_UsesAsciiSuffix()
|
||||
{
|
||||
var value = "abcdefghijklmnopqrstuvwxyz";
|
||||
var result = RuntimeEventsClient.Truncate(value, maxLength: 3);
|
||||
|
||||
Assert.Equal("abc...", result);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
@@ -228,6 +229,18 @@ public sealed class WindowsContainerRuntimeIntegrationTests
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
Environment.GetEnvironmentVariable("ZASTAVA_WINDOWS_INTEGRATION_TESTS") == "true";
|
||||
|
||||
private static HttpClient CreateDockerHttpClient()
|
||||
{
|
||||
var handler = DockerWindowsRuntimeClient.CreateNamedPipeHandler(
|
||||
DockerWindowsRuntimeClient.DefaultPipeName,
|
||||
DockerWindowsRuntimeClient.DefaultConnectTimeout);
|
||||
return new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = DockerWindowsRuntimeClient.DefaultBaseAddress,
|
||||
Timeout = Timeout.InfiniteTimeSpan
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WindowsLibraryHashCollector_CollectCurrentProcess_ReturnsModules()
|
||||
{
|
||||
@@ -324,7 +337,9 @@ public sealed class WindowsContainerRuntimeIntegrationTests
|
||||
return;
|
||||
}
|
||||
|
||||
await using var client = new DockerWindowsRuntimeClient(NullLogger<DockerWindowsRuntimeClient>.Instance);
|
||||
await using var client = new DockerWindowsRuntimeClient(
|
||||
CreateDockerHttpClient(),
|
||||
NullLogger<DockerWindowsRuntimeClient>.Instance);
|
||||
|
||||
var available = await client.IsAvailableAsync(CancellationToken.None);
|
||||
|
||||
@@ -340,7 +355,9 @@ public sealed class WindowsContainerRuntimeIntegrationTests
|
||||
return;
|
||||
}
|
||||
|
||||
await using var client = new DockerWindowsRuntimeClient(NullLogger<DockerWindowsRuntimeClient>.Instance);
|
||||
await using var client = new DockerWindowsRuntimeClient(
|
||||
CreateDockerHttpClient(),
|
||||
NullLogger<DockerWindowsRuntimeClient>.Instance);
|
||||
|
||||
var identity = await client.GetIdentityAsync(CancellationToken.None);
|
||||
|
||||
@@ -359,7 +376,9 @@ public sealed class WindowsContainerRuntimeIntegrationTests
|
||||
return;
|
||||
}
|
||||
|
||||
await using var client = new DockerWindowsRuntimeClient(NullLogger<DockerWindowsRuntimeClient>.Instance);
|
||||
await using var client = new DockerWindowsRuntimeClient(
|
||||
CreateDockerHttpClient(),
|
||||
NullLogger<DockerWindowsRuntimeClient>.Instance);
|
||||
|
||||
var containers = await client.ListContainersAsync(
|
||||
WindowsContainerState.Running,
|
||||
|
||||
Reference in New Issue
Block a user