288 lines
9.0 KiB
C#
288 lines
9.0 KiB
C#
// -----------------------------------------------------------------------------
|
|
// TileEndpoints.cs
|
|
// Sprint: SPRINT_20260125_002_Attestor_trust_automation
|
|
// Task: PROXY-002 - Implement tile-proxy service
|
|
// Description: Tile proxy API endpoints
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using StellaOps.Attestor.TileProxy.Services;
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.Attestor.TileProxy.Endpoints;
|
|
|
|
/// <summary>
|
|
/// API endpoints for tile proxy service.
|
|
/// </summary>
|
|
public static class TileEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Maps all tile proxy endpoints.
|
|
/// </summary>
|
|
public static IEndpointRouteBuilder MapTileProxyEndpoints(this IEndpointRouteBuilder endpoints)
|
|
{
|
|
// Tile endpoints (passthrough)
|
|
endpoints.MapGet("/tile/{level:int}/{index:long}", GetTile)
|
|
.WithName("GetTile")
|
|
.WithTags("Tiles")
|
|
.Produces<byte[]>(StatusCodes.Status200OK, "application/octet-stream")
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.Produces(StatusCodes.Status502BadGateway);
|
|
|
|
endpoints.MapGet("/tile/{level:int}/{index:long}.p/{partialWidth:int}", GetPartialTile)
|
|
.WithName("GetPartialTile")
|
|
.WithTags("Tiles")
|
|
.Produces<byte[]>(StatusCodes.Status200OK, "application/octet-stream")
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.Produces(StatusCodes.Status502BadGateway);
|
|
|
|
// Checkpoint endpoint
|
|
endpoints.MapGet("/checkpoint", GetCheckpoint)
|
|
.WithName("GetCheckpoint")
|
|
.WithTags("Checkpoint")
|
|
.Produces<string>(StatusCodes.Status200OK, "text/plain")
|
|
.Produces(StatusCodes.Status502BadGateway);
|
|
|
|
// Admin endpoints
|
|
var admin = endpoints.MapGroup("/_admin");
|
|
|
|
admin.MapGet("/cache/stats", GetCacheStats)
|
|
.WithName("GetCacheStats")
|
|
.WithTags("Admin")
|
|
.Produces<CacheStatsResponse>(StatusCodes.Status200OK);
|
|
|
|
admin.MapGet("/metrics", GetMetrics)
|
|
.WithName("GetMetrics")
|
|
.WithTags("Admin")
|
|
.Produces<MetricsResponse>(StatusCodes.Status200OK);
|
|
|
|
admin.MapPost("/cache/sync", TriggerSync)
|
|
.WithName("TriggerSync")
|
|
.WithTags("Admin")
|
|
.Produces<SyncResponse>(StatusCodes.Status200OK);
|
|
|
|
admin.MapDelete("/cache/prune", PruneCache)
|
|
.WithName("PruneCache")
|
|
.WithTags("Admin")
|
|
.Produces<PruneResponse>(StatusCodes.Status200OK);
|
|
|
|
admin.MapGet("/health", HealthCheck)
|
|
.WithName("HealthCheck")
|
|
.WithTags("Admin")
|
|
.Produces<HealthResponse>(StatusCodes.Status200OK);
|
|
|
|
admin.MapGet("/ready", ReadinessCheck)
|
|
.WithName("ReadinessCheck")
|
|
.WithTags("Admin")
|
|
.Produces(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status503ServiceUnavailable);
|
|
|
|
return endpoints;
|
|
}
|
|
|
|
private static async Task<IResult> GetTile(
|
|
int level,
|
|
long index,
|
|
[FromServices] TileProxyService proxyService,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var result = await proxyService.GetTileAsync(level, index, cancellationToken: cancellationToken);
|
|
|
|
if (!result.Success)
|
|
{
|
|
return Results.Problem(
|
|
detail: result.Error,
|
|
statusCode: StatusCodes.Status502BadGateway);
|
|
}
|
|
|
|
if (result.Content == null)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
return Results.Bytes(result.Content, "application/octet-stream");
|
|
}
|
|
|
|
private static async Task<IResult> GetPartialTile(
|
|
int level,
|
|
long index,
|
|
int partialWidth,
|
|
[FromServices] TileProxyService proxyService,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (partialWidth <= 0 || partialWidth > 256)
|
|
{
|
|
return Results.BadRequest("Invalid partial width");
|
|
}
|
|
|
|
var result = await proxyService.GetTileAsync(level, index, partialWidth, cancellationToken);
|
|
|
|
if (!result.Success)
|
|
{
|
|
return Results.Problem(
|
|
detail: result.Error,
|
|
statusCode: StatusCodes.Status502BadGateway);
|
|
}
|
|
|
|
if (result.Content == null)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
return Results.Bytes(result.Content, "application/octet-stream");
|
|
}
|
|
|
|
private static async Task<IResult> GetCheckpoint(
|
|
[FromServices] TileProxyService proxyService,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var result = await proxyService.GetCheckpointAsync(cancellationToken);
|
|
|
|
if (!result.Success)
|
|
{
|
|
return Results.Problem(
|
|
detail: result.Error,
|
|
statusCode: StatusCodes.Status502BadGateway);
|
|
}
|
|
|
|
return Results.Text(result.Content ?? "", "text/plain");
|
|
}
|
|
|
|
private static async Task<IResult> GetCacheStats(
|
|
[FromServices] ContentAddressedTileStore tileStore,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var stats = await tileStore.GetStatsAsync(cancellationToken);
|
|
|
|
return Results.Ok(new CacheStatsResponse
|
|
{
|
|
TotalTiles = stats.TotalTiles,
|
|
TotalBytes = stats.TotalBytes,
|
|
TotalMb = Math.Round(stats.TotalBytes / (1024.0 * 1024.0), 2),
|
|
PartialTiles = stats.PartialTiles,
|
|
UsagePercent = Math.Round(stats.UsagePercent, 2),
|
|
OldestTile = stats.OldestTile,
|
|
NewestTile = stats.NewestTile
|
|
});
|
|
}
|
|
|
|
private static IResult GetMetrics(
|
|
[FromServices] TileProxyService proxyService)
|
|
{
|
|
var metrics = proxyService.GetMetrics();
|
|
|
|
return Results.Ok(new MetricsResponse
|
|
{
|
|
CacheHits = metrics.CacheHits,
|
|
CacheMisses = metrics.CacheMisses,
|
|
HitRatePercent = Math.Round(metrics.HitRate, 2),
|
|
UpstreamRequests = metrics.UpstreamRequests,
|
|
UpstreamErrors = metrics.UpstreamErrors,
|
|
InflightRequests = metrics.InflightRequests
|
|
});
|
|
}
|
|
|
|
private static IResult TriggerSync(
|
|
[FromServices] IServiceProvider services,
|
|
[FromServices] ILogger<TileEndpoints> logger)
|
|
{
|
|
// TODO: Trigger background sync job
|
|
logger.LogInformation("Manual sync triggered");
|
|
|
|
return Results.Ok(new SyncResponse
|
|
{
|
|
Message = "Sync job queued",
|
|
QueuedAt = DateTimeOffset.UtcNow
|
|
});
|
|
}
|
|
|
|
private static async Task<IResult> PruneCache(
|
|
[FromServices] ContentAddressedTileStore tileStore,
|
|
[FromQuery] long? targetSizeBytes,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var prunedCount = await tileStore.PruneAsync(targetSizeBytes ?? 0, cancellationToken);
|
|
|
|
return Results.Ok(new PruneResponse
|
|
{
|
|
TilesPruned = prunedCount,
|
|
PrunedAt = DateTimeOffset.UtcNow
|
|
});
|
|
}
|
|
|
|
private static IResult HealthCheck()
|
|
{
|
|
return Results.Ok(new HealthResponse
|
|
{
|
|
Status = "healthy",
|
|
Timestamp = DateTimeOffset.UtcNow
|
|
});
|
|
}
|
|
|
|
private static async Task<IResult> ReadinessCheck(
|
|
[FromServices] TileProxyService proxyService,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// Check if we can reach upstream
|
|
var checkpoint = await proxyService.GetCheckpointAsync(cancellationToken);
|
|
|
|
if (checkpoint.Success)
|
|
{
|
|
return Results.Ok(new { ready = true, checkpoint = checkpoint.TreeSize });
|
|
}
|
|
|
|
return Results.Json(
|
|
new { ready = false, error = checkpoint.Error },
|
|
statusCode: StatusCodes.Status503ServiceUnavailable);
|
|
}
|
|
}
|
|
|
|
// Response models
|
|
public sealed record CacheStatsResponse
|
|
{
|
|
public int TotalTiles { get; init; }
|
|
public long TotalBytes { get; init; }
|
|
public double TotalMb { get; init; }
|
|
public int PartialTiles { get; init; }
|
|
public double UsagePercent { get; init; }
|
|
public DateTimeOffset? OldestTile { get; init; }
|
|
public DateTimeOffset? NewestTile { get; init; }
|
|
}
|
|
|
|
public sealed record MetricsResponse
|
|
{
|
|
public long CacheHits { get; init; }
|
|
public long CacheMisses { get; init; }
|
|
public double HitRatePercent { get; init; }
|
|
public long UpstreamRequests { get; init; }
|
|
public long UpstreamErrors { get; init; }
|
|
public int InflightRequests { get; init; }
|
|
}
|
|
|
|
public sealed record SyncResponse
|
|
{
|
|
public string Message { get; init; } = string.Empty;
|
|
public DateTimeOffset QueuedAt { get; init; }
|
|
}
|
|
|
|
public sealed record PruneResponse
|
|
{
|
|
public int TilesPruned { get; init; }
|
|
public DateTimeOffset PrunedAt { get; init; }
|
|
}
|
|
|
|
public sealed record HealthResponse
|
|
{
|
|
public string Status { get; init; } = string.Empty;
|
|
public DateTimeOffset Timestamp { get; init; }
|
|
}
|
|
|
|
// Logger class for endpoint logging
|
|
file static class TileEndpoints
|
|
{
|
|
}
|