Files
git.stella-ops.org/src/Zastava/StellaOps.Zastava.Observer/ContainerRuntime/Cri/CriRuntimeClient.cs
2025-10-28 15:10:40 +02:00

255 lines
8.2 KiB
C#

using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text.Json;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.Extensions.Logging;
using StellaOps.Zastava.Observer.Configuration;
using StellaOps.Zastava.Observer.Cri;
namespace StellaOps.Zastava.Observer.ContainerRuntime.Cri;
internal interface ICriRuntimeClient : IAsyncDisposable
{
ContainerRuntimeEndpointOptions Endpoint { get; }
Task<CriRuntimeIdentity> GetIdentityAsync(CancellationToken cancellationToken);
Task<IReadOnlyList<CriContainerInfo>> ListContainersAsync(ContainerState state, CancellationToken cancellationToken);
Task<CriContainerInfo?> GetContainerStatusAsync(string containerId, CancellationToken cancellationToken);
}
internal sealed class CriRuntimeClient : ICriRuntimeClient
{
private static readonly object SwitchLock = new();
private static bool http2SwitchApplied;
private readonly GrpcChannel channel;
private readonly RuntimeService.RuntimeServiceClient client;
private readonly ILogger<CriRuntimeClient> logger;
public CriRuntimeClient(ContainerRuntimeEndpointOptions endpoint, ILogger<CriRuntimeClient> logger)
{
ArgumentNullException.ThrowIfNull(endpoint);
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
Endpoint = endpoint;
EnsureHttp2Switch();
channel = CreateChannel(endpoint);
client = new RuntimeService.RuntimeServiceClient(channel);
}
public ContainerRuntimeEndpointOptions Endpoint { get; }
public async Task<CriRuntimeIdentity> GetIdentityAsync(CancellationToken cancellationToken)
{
var response = await client.VersionAsync(new VersionRequest(), cancellationToken: cancellationToken).ConfigureAwait(false);
return new CriRuntimeIdentity(
RuntimeName: response.RuntimeName ?? Endpoint.Engine.ToEngineString(),
RuntimeVersion: response.RuntimeVersion ?? "unknown",
RuntimeApiVersion: response.RuntimeApiVersion ?? response.Version ?? "unknown");
}
public async Task<IReadOnlyList<CriContainerInfo>> ListContainersAsync(ContainerState state, CancellationToken cancellationToken)
{
var request = new ListContainersRequest
{
Filter = new ContainerFilter
{
State = new ContainerStateValue
{
State = state
}
}
};
try
{
var response = await client.ListContainersAsync(request, cancellationToken: cancellationToken).ConfigureAwait(false);
if (response.Containers is null || response.Containers.Count == 0)
{
return Array.Empty<CriContainerInfo>();
}
return response.Containers
.Select(CriConversions.ToContainerInfo)
.ToArray();
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unimplemented)
{
logger.LogWarning(ex, "Runtime endpoint {Endpoint} does not support ListContainers for state {State}.", Endpoint.Endpoint, state);
throw;
}
}
public async Task<CriContainerInfo?> GetContainerStatusAsync(string containerId, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(containerId))
{
return null;
}
try
{
var response = await client.ContainerStatusAsync(new ContainerStatusRequest
{
ContainerId = containerId,
Verbose = true
}, cancellationToken: cancellationToken).ConfigureAwait(false);
if (response.Status is null)
{
return null;
}
var baseline = CriConversions.ToContainerInfo(new Container
{
Id = response.Status.Id,
PodSandboxId = response.Status.Metadata?.Name ?? string.Empty,
Metadata = response.Status.Metadata,
Image = response.Status.Image,
ImageRef = response.Status.ImageRef,
Labels = { response.Status.Labels },
Annotations = { response.Status.Annotations },
CreatedAt = response.Status.CreatedAt
});
var merged = CriConversions.MergeStatus(baseline, response.Status);
if (response.Info is { Count: > 0 } && TryExtractPid(response.Info, out var pid))
{
merged = merged with { Pid = pid };
}
return merged;
}
catch (RpcException ex) when (ex.StatusCode is StatusCode.NotFound or StatusCode.DeadlineExceeded)
{
logger.LogDebug(ex, "Container {ContainerId} no longer available when querying status.", containerId);
return null;
}
}
private static bool TryExtractPid(IDictionary<string, string> info, out int pid)
{
if (info.TryGetValue("pid", out var value) && int.TryParse(value, out pid))
{
return true;
}
foreach (var entry in info.Values)
{
if (string.IsNullOrWhiteSpace(entry))
{
continue;
}
try
{
using var document = JsonDocument.Parse(entry);
if (document.RootElement.TryGetProperty("pid", out var pidElement) && pidElement.TryGetInt32(out pid))
{
return true;
}
}
catch (JsonException)
{
}
}
pid = default;
return false;
}
public ValueTask DisposeAsync()
{
try
{
channel.Dispose();
}
catch (InvalidOperationException)
{
// Channel already disposed.
}
return ValueTask.CompletedTask;
}
private static void EnsureHttp2Switch()
{
if (http2SwitchApplied)
{
return;
}
lock (SwitchLock)
{
if (!http2SwitchApplied)
{
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
http2SwitchApplied = true;
}
}
}
private GrpcChannel CreateChannel(ContainerRuntimeEndpointOptions endpoint)
{
if (IsUnixEndpoint(endpoint.Endpoint, out var unixPath))
{
var resolvedPath = unixPath;
var handler = new SocketsHttpHandler
{
ConnectCallback = (context, cancellationToken) => ConnectUnixDomainSocketAsync(resolvedPath, cancellationToken),
EnableMultipleHttp2Connections = true
};
if (endpoint.ConnectTimeout is { } timeout && timeout > TimeSpan.Zero)
{
handler.ConnectTimeout = timeout;
}
return GrpcChannel.ForAddress("http://unix.local", new GrpcChannelOptions
{
HttpHandler = handler,
DisposeHttpClient = true
});
}
return GrpcChannel.ForAddress(endpoint.Endpoint, new GrpcChannelOptions
{
DisposeHttpClient = true
});
}
private static bool IsUnixEndpoint(string endpoint, out string path)
{
if (endpoint.StartsWith("unix://", StringComparison.OrdinalIgnoreCase))
{
path = endpoint["unix://".Length..];
return true;
}
path = string.Empty;
return false;
}
private static async ValueTask<Stream> ConnectUnixDomainSocketAsync(string unixPath, CancellationToken cancellationToken)
{
var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified)
{
NoDelay = true
};
try
{
var endpoint = new UnixDomainSocketEndPoint(unixPath);
await socket.ConnectAsync(endpoint, cancellationToken).ConfigureAwait(false);
return new NetworkStream(socket, ownsSocket: true);
}
catch
{
socket.Dispose();
throw;
}
}
}