Gaps fill up, fixes, ui restructuring
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Release Control bundle lifecycle endpoints consumed by UI v2 shell.
|
||||
/// </summary>
|
||||
public static class ReleaseControlEndpoints
|
||||
{
|
||||
private const int DefaultLimit = 50;
|
||||
private const int MaxLimit = 200;
|
||||
|
||||
public static IEndpointRouteBuilder MapReleaseControlEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var bundles = app.MapGroup("/api/v1/release-control/bundles")
|
||||
.WithTags("Release Control");
|
||||
|
||||
bundles.MapGet(string.Empty, async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
TimeProvider timeProvider,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] int? offset,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var normalizedOffset = NormalizeOffset(offset);
|
||||
|
||||
var items = await store.ListBundlesAsync(
|
||||
requestContext!.TenantId,
|
||||
normalizedLimit,
|
||||
normalizedOffset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<ReleaseControlBundleSummary>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
items,
|
||||
items.Count,
|
||||
normalizedLimit,
|
||||
normalizedOffset));
|
||||
})
|
||||
.WithName("ListReleaseControlBundles")
|
||||
.WithSummary("List release control bundles")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
bundles.MapGet("/{bundleId:guid}", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
TimeProvider timeProvider,
|
||||
Guid bundleId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var item = await store.GetBundleAsync(
|
||||
requestContext!.TenantId,
|
||||
bundleId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "bundle_not_found", bundleId });
|
||||
}
|
||||
|
||||
return Results.Ok(new PlatformItemResponse<ReleaseControlBundleDetail>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
item));
|
||||
})
|
||||
.WithName("GetReleaseControlBundle")
|
||||
.WithSummary("Get release control bundle by id")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
bundles.MapPost(string.Empty, async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
CreateReleaseControlBundleRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var created = await store.CreateBundleAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var location = $"/api/v1/release-control/bundles/{created.Id}";
|
||||
return Results.Created(location, created);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, bundleId: null, versionId: null);
|
||||
}
|
||||
})
|
||||
.WithName("CreateReleaseControlBundle")
|
||||
.WithSummary("Create release control bundle")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlOperate);
|
||||
|
||||
bundles.MapGet("/{bundleId:guid}/versions", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
TimeProvider timeProvider,
|
||||
Guid bundleId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] int? offset,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var normalizedOffset = NormalizeOffset(offset);
|
||||
|
||||
try
|
||||
{
|
||||
var items = await store.ListVersionsAsync(
|
||||
requestContext!.TenantId,
|
||||
bundleId,
|
||||
normalizedLimit,
|
||||
normalizedOffset,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformListResponse<ReleaseControlBundleVersionSummary>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
items,
|
||||
items.Count,
|
||||
normalizedLimit,
|
||||
normalizedOffset));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, bundleId, versionId: null);
|
||||
}
|
||||
})
|
||||
.WithName("ListReleaseControlBundleVersions")
|
||||
.WithSummary("List bundle versions")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
bundles.MapGet("/{bundleId:guid}/versions/{versionId:guid}", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
TimeProvider timeProvider,
|
||||
Guid bundleId,
|
||||
Guid versionId,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var version = await store.GetVersionAsync(
|
||||
requestContext!.TenantId,
|
||||
bundleId,
|
||||
versionId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (version is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "bundle_version_not_found", bundleId, versionId });
|
||||
}
|
||||
|
||||
return Results.Ok(new PlatformItemResponse<ReleaseControlBundleVersionDetail>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
timeProvider.GetUtcNow(),
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
version));
|
||||
})
|
||||
.WithName("GetReleaseControlBundleVersion")
|
||||
.WithSummary("Get bundle version")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
|
||||
|
||||
bundles.MapPost("/{bundleId:guid}/versions", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
Guid bundleId,
|
||||
PublishReleaseControlBundleVersionRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var created = await store.PublishVersionAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
bundleId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var location = $"/api/v1/release-control/bundles/{bundleId}/versions/{created.Id}";
|
||||
return Results.Created(location, created);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, bundleId, versionId: null);
|
||||
}
|
||||
})
|
||||
.WithName("PublishReleaseControlBundleVersion")
|
||||
.WithSummary("Publish immutable bundle version")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlOperate);
|
||||
|
||||
bundles.MapPost("/{bundleId:guid}/versions/{versionId:guid}/materialize", async Task<IResult>(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
IReleaseControlBundleStore store,
|
||||
Guid bundleId,
|
||||
Guid versionId,
|
||||
MaterializeReleaseControlBundleVersionRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var run = await store.MaterializeVersionAsync(
|
||||
requestContext!.TenantId,
|
||||
requestContext.ActorId,
|
||||
bundleId,
|
||||
versionId,
|
||||
request,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var location = $"/api/v1/release-control/bundles/{bundleId}/versions/{versionId}/materialize/{run.RunId}";
|
||||
return Results.Accepted(location, run);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return MapStoreError(ex, bundleId, versionId);
|
||||
}
|
||||
})
|
||||
.WithName("MaterializeReleaseControlBundleVersion")
|
||||
.WithSummary("Materialize bundle version")
|
||||
.RequireAuthorization(PlatformPolicies.ReleaseControlOperate);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static int NormalizeLimit(int? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => DefaultLimit,
|
||||
< 1 => 1,
|
||||
> MaxLimit => MaxLimit,
|
||||
_ => value.Value
|
||||
};
|
||||
}
|
||||
|
||||
private static int NormalizeOffset(int? value) => value is null or < 0 ? 0 : value.Value;
|
||||
|
||||
private static IResult MapStoreError(InvalidOperationException exception, Guid? bundleId, Guid? versionId)
|
||||
{
|
||||
return exception.Message switch
|
||||
{
|
||||
"bundle_not_found" => Results.NotFound(new { error = "bundle_not_found", bundleId }),
|
||||
"bundle_version_not_found" => Results.NotFound(new { error = "bundle_version_not_found", bundleId, versionId }),
|
||||
"bundle_slug_exists" => Results.Conflict(new { error = "bundle_slug_exists" }),
|
||||
"bundle_slug_required" => Results.BadRequest(new { error = "bundle_slug_required" }),
|
||||
"bundle_name_required" => Results.BadRequest(new { error = "bundle_name_required" }),
|
||||
"request_required" => Results.BadRequest(new { error = "request_required" }),
|
||||
"tenant_required" => Results.BadRequest(new { error = "tenant_required" }),
|
||||
"tenant_id_invalid" => Results.BadRequest(new { error = "tenant_id_invalid" }),
|
||||
_ => Results.BadRequest(new { error = exception.Message })
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user