331 lines
12 KiB
C#
331 lines
12 KiB
C#
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;
|
|
}
|
|
}
|