Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Created SignerEndpointsTests to validate the SignDsse and VerifyReferrers endpoints. - Implemented StubBearerAuthenticationDefaults and StubBearerAuthenticationHandler for token-based authentication. - Developed ConcelierExporterClient for managing Trivy DB settings and export operations. - Added TrivyDbSettingsPageComponent for UI interactions with Trivy DB settings, including form handling and export triggering. - Implemented styles and HTML structure for Trivy DB settings page. - Created NotifySmokeCheck tool for validating Redis event streams and Notify deliveries.
199 lines
6.9 KiB
C#
199 lines
6.9 KiB
C#
using System.Globalization;
|
||
using System.Net.Http.Headers;
|
||
using System.Linq;
|
||
using System.Text.Json;
|
||
using StackExchange.Redis;
|
||
|
||
static string RequireEnv(string name)
|
||
{
|
||
var value = Environment.GetEnvironmentVariable(name);
|
||
if (string.IsNullOrWhiteSpace(value))
|
||
{
|
||
throw new InvalidOperationException($"Environment variable '{name}' is required for Notify smoke validation.");
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
static string? GetField(StreamEntry entry, string fieldName)
|
||
{
|
||
foreach (var pair in entry.Values)
|
||
{
|
||
if (string.Equals(pair.Name, fieldName, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return pair.Value.ToString();
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
static void Ensure(bool condition, string message)
|
||
{
|
||
if (!condition)
|
||
{
|
||
throw new InvalidOperationException(message);
|
||
}
|
||
}
|
||
|
||
var redisDsn = RequireEnv("NOTIFY_SMOKE_REDIS_DSN");
|
||
var redisStream = Environment.GetEnvironmentVariable("NOTIFY_SMOKE_STREAM");
|
||
if (string.IsNullOrWhiteSpace(redisStream))
|
||
{
|
||
redisStream = "stella.events";
|
||
}
|
||
|
||
var expectedKindsEnv = RequireEnv("NOTIFY_SMOKE_EXPECT_KINDS");
|
||
|
||
var expectedKinds = expectedKindsEnv
|
||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||
.Select(kind => kind.ToLowerInvariant())
|
||
.Distinct()
|
||
.ToArray();
|
||
Ensure(expectedKinds.Length > 0, "Expected at least one event kind in NOTIFY_SMOKE_EXPECT_KINDS.");
|
||
|
||
var lookbackMinutesEnv = RequireEnv("NOTIFY_SMOKE_LOOKBACK_MINUTES");
|
||
if (!double.TryParse(lookbackMinutesEnv, NumberStyles.Any, CultureInfo.InvariantCulture, out var lookbackMinutes))
|
||
{
|
||
throw new InvalidOperationException("NOTIFY_SMOKE_LOOKBACK_MINUTES must be numeric.");
|
||
}
|
||
Ensure(lookbackMinutes > 0, "NOTIFY_SMOKE_LOOKBACK_MINUTES must be greater than zero.");
|
||
|
||
var now = DateTimeOffset.UtcNow;
|
||
var sinceThreshold = now - TimeSpan.FromMinutes(Math.Max(1, lookbackMinutes));
|
||
|
||
Console.WriteLine($"ℹ️ Checking Redis stream '{redisStream}' for kinds [{string.Join(", ", expectedKinds)}] within the last {lookbackMinutes:F1} minutes.");
|
||
|
||
var redisConfig = ConfigurationOptions.Parse(redisDsn);
|
||
redisConfig.AbortOnConnectFail = false;
|
||
|
||
await using var redisConnection = await ConnectionMultiplexer.ConnectAsync(redisConfig);
|
||
var database = redisConnection.GetDatabase();
|
||
|
||
var streamEntries = await database.StreamRangeAsync(redisStream, "-", "+", count: 200);
|
||
if (streamEntries.Length > 1)
|
||
{
|
||
Array.Reverse(streamEntries);
|
||
}
|
||
Ensure(streamEntries.Length > 0, $"Redis stream '{redisStream}' is empty.");
|
||
|
||
var recentEntries = new List<StreamEntry>();
|
||
foreach (var entry in streamEntries)
|
||
{
|
||
var timestampText = GetField(entry, "ts");
|
||
if (timestampText is null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!DateTimeOffset.TryParse(timestampText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var entryTimestamp))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (entryTimestamp >= sinceThreshold)
|
||
{
|
||
recentEntries.Add(entry);
|
||
}
|
||
}
|
||
|
||
Ensure(recentEntries.Count > 0, $"No Redis events newer than {sinceThreshold:u} located in stream '{redisStream}'.");
|
||
|
||
var missingKinds = new List<string>();
|
||
foreach (var kind in expectedKinds)
|
||
{
|
||
var match = recentEntries.FirstOrDefault(entry =>
|
||
{
|
||
var entryKind = GetField(entry, "kind")?.ToLowerInvariant();
|
||
return entryKind == kind;
|
||
});
|
||
|
||
if (match.Equals(default(StreamEntry)))
|
||
{
|
||
missingKinds.Add(kind);
|
||
}
|
||
}
|
||
|
||
Ensure(missingKinds.Count == 0, $"Missing expected Redis events for kinds: {string.Join(", ", missingKinds)}");
|
||
|
||
Console.WriteLine("✅ Redis event stream contains the expected scanner events.");
|
||
|
||
var notifyBaseUrl = RequireEnv("NOTIFY_SMOKE_NOTIFY_BASEURL").TrimEnd('/');
|
||
var notifyToken = RequireEnv("NOTIFY_SMOKE_NOTIFY_TOKEN");
|
||
var notifyTenant = RequireEnv("NOTIFY_SMOKE_NOTIFY_TENANT");
|
||
var notifyTenantHeader = Environment.GetEnvironmentVariable("NOTIFY_SMOKE_NOTIFY_TENANT_HEADER");
|
||
if (string.IsNullOrWhiteSpace(notifyTenantHeader))
|
||
{
|
||
notifyTenantHeader = "X-StellaOps-Tenant";
|
||
}
|
||
|
||
var notifyTimeoutSeconds = 30;
|
||
var notifyTimeoutEnv = Environment.GetEnvironmentVariable("NOTIFY_SMOKE_NOTIFY_TIMEOUT_SECONDS");
|
||
if (!string.IsNullOrWhiteSpace(notifyTimeoutEnv) && int.TryParse(notifyTimeoutEnv, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedTimeout))
|
||
{
|
||
notifyTimeoutSeconds = Math.Max(5, parsedTimeout);
|
||
}
|
||
|
||
using var httpClient = new HttpClient
|
||
{
|
||
Timeout = TimeSpan.FromSeconds(notifyTimeoutSeconds),
|
||
};
|
||
|
||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", notifyToken);
|
||
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||
httpClient.DefaultRequestHeaders.Add(notifyTenantHeader, notifyTenant);
|
||
|
||
var sinceQuery = Uri.EscapeDataString(sinceThreshold.ToString("O", CultureInfo.InvariantCulture));
|
||
var deliveriesUrl = $"{notifyBaseUrl}/api/v1/deliveries?since={sinceQuery}&limit=200";
|
||
|
||
Console.WriteLine($"ℹ️ Querying Notify deliveries via {deliveriesUrl}.");
|
||
|
||
using var response = await httpClient.GetAsync(deliveriesUrl);
|
||
if (!response.IsSuccessStatusCode)
|
||
{
|
||
var body = await response.Content.ReadAsStringAsync();
|
||
throw new InvalidOperationException($"Notify deliveries request failed with {(int)response.StatusCode} {response.ReasonPhrase}: {body}");
|
||
}
|
||
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
if (string.IsNullOrWhiteSpace(json))
|
||
{
|
||
throw new InvalidOperationException("Notify deliveries response body was empty.");
|
||
}
|
||
|
||
using var document = JsonDocument.Parse(json);
|
||
var root = document.RootElement;
|
||
|
||
IEnumerable<JsonElement> EnumerateDeliveries(JsonElement element)
|
||
{
|
||
return element.ValueKind switch
|
||
{
|
||
JsonValueKind.Array => element.EnumerateArray(),
|
||
JsonValueKind.Object when element.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Array => items.EnumerateArray(),
|
||
_ => throw new InvalidOperationException("Notify deliveries response was not an array or did not contain an 'items' collection.")
|
||
};
|
||
}
|
||
|
||
var deliveries = EnumerateDeliveries(root).ToArray();
|
||
Ensure(deliveries.Length > 0, "Notify deliveries response did not return any records.");
|
||
|
||
var missingDeliveryKinds = new List<string>();
|
||
foreach (var kind in expectedKinds)
|
||
{
|
||
var found = deliveries.Any(delivery =>
|
||
delivery.TryGetProperty("kind", out var kindProperty) &&
|
||
kindProperty.GetString()?.Equals(kind, StringComparison.OrdinalIgnoreCase) == true &&
|
||
delivery.TryGetProperty("status", out var statusProperty) &&
|
||
!string.Equals(statusProperty.GetString(), "failed", StringComparison.OrdinalIgnoreCase));
|
||
|
||
if (!found)
|
||
{
|
||
missingDeliveryKinds.Add(kind);
|
||
}
|
||
}
|
||
|
||
Ensure(missingDeliveryKinds.Count == 0, $"Notify deliveries missing successful records for kinds: {string.Join(", ", missingDeliveryKinds)}");
|
||
|
||
Console.WriteLine("✅ Notify deliveries include the expected scanner events.");
|
||
Console.WriteLine("🎉 Notify smoke validation completed successfully.");
|