Implement missing backend endpoints for release orchestration
TASK-002: 11 deployment monitoring endpoints in JobEngine (list, get, logs, events, metrics, pause/resume/cancel/rollback/retry) TASK-003: 6 evidence management endpoints in JobEngine (list, get, verify, export, raw, timeline) TASK-005: 3 release dashboard endpoints in JobEngine (dashboard summary, approve/reject promotion) TASK-006: 2 registry image search endpoints in Scanner (search with 9 mock images, digests lookup) All endpoints return seed/mock data for testing. Auth policies match existing patterns. Dual route registration on both /api/ and /api/v1/ prefixes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// RegistryEndpoints.cs
|
||||
// Description: HTTP endpoints for registry image search and digest lookup.
|
||||
// Returns mock data until real registry integration is wired.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Scanner.WebService.Security;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints for container registry image search and digest queries.
|
||||
/// Used by Create Version / Create Hotfix wizards in the UI.
|
||||
/// </summary>
|
||||
internal static class RegistryEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps registry image endpoints under /registries.
|
||||
/// </summary>
|
||||
public static void MapRegistryEndpoints(this RouteGroupBuilder apiGroup, string prefix = "/registries")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(apiGroup);
|
||||
|
||||
var group = apiGroup.MapGroup(prefix)
|
||||
.WithTags("Registries");
|
||||
|
||||
// GET /api/v1/registries/images/search?q={query}
|
||||
group.MapGet("/images/search", HandleSearchImages)
|
||||
.WithName("scanner.registries.images.search")
|
||||
.WithDescription("Search container registry images by name")
|
||||
.Produces<RegistrySearchResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.RequireAuthorization(ScannerPolicies.ScansRead);
|
||||
|
||||
// GET /api/v1/registries/images/digests?repository={repo}
|
||||
group.MapGet("/images/digests", HandleGetImageDigests)
|
||||
.WithName("scanner.registries.images.digests")
|
||||
.WithDescription("Get image digests and tags for a repository")
|
||||
.Produces<RegistryDigestResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.RequireAuthorization(ScannerPolicies.ScansRead);
|
||||
}
|
||||
|
||||
private static IResult HandleSearchImages(string? q)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(q) || q.Length < 2)
|
||||
{
|
||||
return Results.BadRequest(new { error = "Query must be at least 2 characters" });
|
||||
}
|
||||
|
||||
var filtered = MockImages.Where(i =>
|
||||
i.Name.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||||
i.Repository.Contains(q, StringComparison.OrdinalIgnoreCase)
|
||||
).ToArray();
|
||||
|
||||
return Results.Ok(new RegistrySearchResponse
|
||||
{
|
||||
Items = filtered,
|
||||
TotalCount = filtered.Length,
|
||||
RegistryId = null
|
||||
});
|
||||
}
|
||||
|
||||
private static IResult HandleGetImageDigests(string? repository)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(repository))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Repository parameter is required" });
|
||||
}
|
||||
|
||||
var match = MockImages.FirstOrDefault(i =>
|
||||
string.Equals(i.Repository, repository, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (match is null)
|
||||
{
|
||||
// Return a stub for unknown repositories
|
||||
var repoName = repository.Contains('/')
|
||||
? repository.Split('/').Last()
|
||||
: repository;
|
||||
|
||||
return Results.Ok(new RegistryDigestResponse
|
||||
{
|
||||
Name = repoName,
|
||||
Repository = repository,
|
||||
Tags = new[] { "latest" },
|
||||
Digests = new[]
|
||||
{
|
||||
new RegistryDigestEntry
|
||||
{
|
||||
Tag = "latest",
|
||||
Digest = $"sha256:{Guid.NewGuid():N}",
|
||||
PushedAt = "2026-03-20T10:00:00Z"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Ok(new RegistryDigestResponse
|
||||
{
|
||||
Name = match.Name,
|
||||
Repository = match.Repository,
|
||||
Tags = match.Tags,
|
||||
Digests = match.Digests
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Response DTOs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
internal sealed class RegistrySearchResponse
|
||||
{
|
||||
public RegistryImageDto[] Items { get; set; } = Array.Empty<RegistryImageDto>();
|
||||
public int TotalCount { get; set; }
|
||||
public string? RegistryId { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class RegistryDigestResponse
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Repository { get; set; } = string.Empty;
|
||||
public string[] Tags { get; set; } = Array.Empty<string>();
|
||||
public RegistryDigestEntry[] Digests { get; set; } = Array.Empty<RegistryDigestEntry>();
|
||||
}
|
||||
|
||||
internal sealed class RegistryImageDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Repository { get; set; } = string.Empty;
|
||||
public string[] Tags { get; set; } = Array.Empty<string>();
|
||||
public RegistryDigestEntry[] Digests { get; set; } = Array.Empty<RegistryDigestEntry>();
|
||||
public string LastPushed { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
internal sealed class RegistryDigestEntry
|
||||
{
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
public string Digest { get; set; } = string.Empty;
|
||||
public string PushedAt { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Mock data (to be replaced with real registry integration)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static readonly RegistryImageDto[] MockImages = new[]
|
||||
{
|
||||
new RegistryImageDto
|
||||
{
|
||||
Name = "nginx",
|
||||
Repository = "library/nginx",
|
||||
Tags = new[] { "latest", "1.27", "1.27-alpine" },
|
||||
Digests = new[]
|
||||
{
|
||||
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", PushedAt = "2026-03-20T10:00:00Z" },
|
||||
new RegistryDigestEntry { Tag = "1.27", Digest = "sha256:b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3", PushedAt = "2026-03-18T14:30:00Z" },
|
||||
},
|
||||
LastPushed = "2026-03-20T10:00:00Z"
|
||||
},
|
||||
new RegistryImageDto
|
||||
{
|
||||
Name = "redis",
|
||||
Repository = "library/redis",
|
||||
Tags = new[] { "latest", "7.4", "7.4-alpine" },
|
||||
Digests = new[]
|
||||
{
|
||||
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4", PushedAt = "2026-03-19T08:00:00Z" },
|
||||
new RegistryDigestEntry { Tag = "7.4", Digest = "sha256:d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5", PushedAt = "2026-03-17T16:45:00Z" },
|
||||
},
|
||||
LastPushed = "2026-03-19T08:00:00Z"
|
||||
},
|
||||
new RegistryImageDto
|
||||
{
|
||||
Name = "postgres",
|
||||
Repository = "library/postgres",
|
||||
Tags = new[] { "latest", "16.2", "16.2-alpine" },
|
||||
Digests = new[]
|
||||
{
|
||||
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6", PushedAt = "2026-03-21T06:15:00Z" },
|
||||
new RegistryDigestEntry { Tag = "16.2", Digest = "sha256:f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7", PushedAt = "2026-03-15T09:30:00Z" },
|
||||
},
|
||||
LastPushed = "2026-03-21T06:15:00Z"
|
||||
},
|
||||
new RegistryImageDto
|
||||
{
|
||||
Name = "api-gateway",
|
||||
Repository = "stella-ops/api-gateway",
|
||||
Tags = new[] { "latest", "2.8.1", "2.8.0" },
|
||||
Digests = new[]
|
||||
{
|
||||
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8", PushedAt = "2026-03-22T11:00:00Z" },
|
||||
new RegistryDigestEntry { Tag = "2.8.1", Digest = "sha256:b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9", PushedAt = "2026-03-22T11:00:00Z" },
|
||||
},
|
||||
LastPushed = "2026-03-22T11:00:00Z"
|
||||
},
|
||||
new RegistryImageDto
|
||||
{
|
||||
Name = "payment-svc",
|
||||
Repository = "stella-ops/payment-svc",
|
||||
Tags = new[] { "latest", "3.1.0", "3.0.9" },
|
||||
Digests = new[]
|
||||
{
|
||||
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0", PushedAt = "2026-03-21T17:20:00Z" },
|
||||
new RegistryDigestEntry { Tag = "3.1.0", Digest = "sha256:d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1", PushedAt = "2026-03-21T17:20:00Z" },
|
||||
},
|
||||
LastPushed = "2026-03-21T17:20:00Z"
|
||||
},
|
||||
new RegistryImageDto
|
||||
{
|
||||
Name = "auth-service",
|
||||
Repository = "stella-ops/auth-service",
|
||||
Tags = new[] { "latest", "1.14.2", "1.14.1" },
|
||||
Digests = new[]
|
||||
{
|
||||
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2", PushedAt = "2026-03-20T13:45:00Z" },
|
||||
new RegistryDigestEntry { Tag = "1.14.2", Digest = "sha256:f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3", PushedAt = "2026-03-20T13:45:00Z" },
|
||||
},
|
||||
LastPushed = "2026-03-20T13:45:00Z"
|
||||
},
|
||||
new RegistryImageDto
|
||||
{
|
||||
Name = "node",
|
||||
Repository = "library/node",
|
||||
Tags = new[] { "latest", "22-alpine", "20-slim" },
|
||||
Digests = new[]
|
||||
{
|
||||
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4", PushedAt = "2026-03-22T07:00:00Z" },
|
||||
new RegistryDigestEntry { Tag = "22-alpine", Digest = "sha256:b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5", PushedAt = "2026-03-22T07:00:00Z" },
|
||||
},
|
||||
LastPushed = "2026-03-22T07:00:00Z"
|
||||
},
|
||||
new RegistryImageDto
|
||||
{
|
||||
Name = "python",
|
||||
Repository = "library/python",
|
||||
Tags = new[] { "latest", "3.13-slim", "3.12-bookworm" },
|
||||
Digests = new[]
|
||||
{
|
||||
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", PushedAt = "2026-03-21T09:30:00Z" },
|
||||
new RegistryDigestEntry { Tag = "3.13-slim", Digest = "sha256:d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7", PushedAt = "2026-03-21T09:30:00Z" },
|
||||
},
|
||||
LastPushed = "2026-03-21T09:30:00Z"
|
||||
},
|
||||
new RegistryImageDto
|
||||
{
|
||||
Name = "golang",
|
||||
Repository = "library/golang",
|
||||
Tags = new[] { "latest", "1.23-alpine", "1.22-bookworm" },
|
||||
Digests = new[]
|
||||
{
|
||||
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8", PushedAt = "2026-03-20T15:00:00Z" },
|
||||
new RegistryDigestEntry { Tag = "1.23-alpine", Digest = "sha256:f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9", PushedAt = "2026-03-20T15:00:00Z" },
|
||||
},
|
||||
LastPushed = "2026-03-20T15:00:00Z"
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -805,6 +805,7 @@ apiGroup.MapUnknownsEndpoints();
|
||||
apiGroup.MapSecretDetectionSettingsEndpoints(); // Sprint: SPRINT_20260104_006_BE
|
||||
apiGroup.MapSecurityAdapterEndpoints(); // Pack v2 security adapter routes
|
||||
apiGroup.MapScanPolicyEndpoints(); // Sprint: S1-T03 Scan Policy CRUD
|
||||
apiGroup.MapRegistryEndpoints(); // Registry image search + digest lookup for release wizards
|
||||
|
||||
if (resolvedOptions.Features.EnablePolicyPreview)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user