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 GetIdentityAsync(CancellationToken cancellationToken); Task> ListContainersAsync(ContainerState state, CancellationToken cancellationToken); Task 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 logger; public CriRuntimeClient(ContainerRuntimeEndpointOptions endpoint, ILogger 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 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> 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(); } 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 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 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 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; } } }