| File: Utils\EnvironmentChecker\DcpKubeconfig.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Cryptography.X509Certificates; using System.Text; using Aspire.Cli.Resources; namespace Aspire.Cli.Utils.EnvironmentChecker; internal sealed class DcpKubeconfig : IDisposable { private const int ReadAttempts = 3; private static readonly TimeSpan s_readRetryDelay = TimeSpan.FromMilliseconds(100); public required Uri Server { get; init; } public string? Token { get; init; } public List<X509Certificate2> CertificateAuthorityCertificates { get; init; } = []; public X509Certificate2? ClientCertificate { get; init; } internal static async Task<DcpKubeconfig> ReadFileWithRetryAsync(string path, Func<TimeSpan, CancellationToken, Task>? delayAsync = null, CancellationToken cancellationToken = default) { delayAsync ??= Task.Delay; for (var attempt = 1; ; attempt++) { var content = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false); try { return Parse(content); } catch (Exception ex) when (ex is not OperationCanceledException && attempt < ReadAttempts) { // DCP creates the kubeconfig path before all content may be flushed. A brief retry // avoids treating a transient partial file as a failed connection check. await delayAsync(s_readRetryDelay, cancellationToken).ConfigureAwait(false); } } } public static DcpKubeconfig Parse(string content) { string? server = null; string? token = null; string? certificateAuthorityData = null; string? clientCertificateData = null; string? clientKeyData = null; // DCP emits a compact kubeconfig in this shape: // clusters: // - name: dcp // cluster: // server: https://127.0.0.1:<port> // certificate-authority-data: <base64 PEM> // users: // - name: dcp // user: // client-certificate-data: <base64 PEM> // client-key-data: <base64 PEM> // The doctor probe only needs connection material, so parse the scalar fields directly // instead of adding a YAML dependency to the NativeAOT CLI. foreach (var line in content.Split('\n')) { server ??= TryReadScalar(line, "server"); token ??= TryReadScalar(line, "token"); certificateAuthorityData ??= TryReadScalar(line, "certificate-authority-data"); clientCertificateData ??= TryReadScalar(line, "client-certificate-data"); clientKeyData ??= TryReadScalar(line, "client-key-data"); } if (string.IsNullOrWhiteSpace(server) || !Uri.TryCreate(server, UriKind.Absolute, out var serverUri)) { throw new InvalidOperationException(DoctorCommandStrings.DcpKubeconfigMissingServerDetails); } return new DcpKubeconfig { Server = serverUri, Token = token, CertificateAuthorityCertificates = certificateAuthorityData is null ? [] : LoadCertificates(certificateAuthorityData), ClientCertificate = clientCertificateData is not null && clientKeyData is not null ? LoadClientCertificate(clientCertificateData, clientKeyData) : null }; } public void Dispose() { foreach (var certificate in CertificateAuthorityCertificates) { certificate.Dispose(); } ClientCertificate?.Dispose(); } private static string? TryReadScalar(string line, string key) { var trimmed = line.Trim(); if (trimmed.Length == 0 || trimmed[0] == '#') { return null; } var prefix = key + ":"; if (!trimmed.StartsWith(prefix, StringComparison.Ordinal)) { return null; } var value = trimmed[prefix.Length..].Trim(); if (value.Length == 0) { return null; } return TrimYamlQuotes(value); } private static string TrimYamlQuotes(string value) { return value.Length >= 2 && ((value[0] == '"' && value[^1] == '"') || (value[0] == '\'' && value[^1] == '\'')) ? value[1..^1] : value; } private static List<X509Certificate2> LoadCertificates(string base64Data) { var certificatePem = Encoding.UTF8.GetString(Convert.FromBase64String(base64Data)); var certificates = new X509Certificate2Collection(); certificates.ImportFromPem(certificatePem); return certificates.OfType<X509Certificate2>().ToList(); } private static X509Certificate2 LoadClientCertificate(string certificateData, string keyData) { var certificatePem = Encoding.UTF8.GetString(Convert.FromBase64String(certificateData)); var keyPem = Encoding.UTF8.GetString(Convert.FromBase64String(keyData)); using var certificate = X509Certificate2.CreateFromPem(certificatePem, keyPem); return X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pkcs12), password: null, X509KeyStorageFlags.EphemeralKeySet); } }