255 lines
8.2 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|