OPC # 0002: Improvements to Client provisioning workflows
This commit is contained in:
@@ -16,6 +16,7 @@ public static class InfraEndpoints
|
|||||||
g.MapPost("/{container}/restart",(string container) => ServiceAction(container, "restart"));
|
g.MapPost("/{container}/restart",(string container) => ServiceAction(container, "restart"));
|
||||||
g.MapGet ("/compose/up/stream", ComposeUpStream);
|
g.MapGet ("/compose/up/stream", ComposeUpStream);
|
||||||
g.MapGet ("/compose/up-force/stream", ComposeUpForceStream);
|
g.MapGet ("/compose/up-force/stream", ComposeUpForceStream);
|
||||||
|
g.MapGet ("/compose/nuke/stream", ComposeNukeStream);
|
||||||
g.MapGet ("/compose/down/stream", ComposeDownStream);
|
g.MapGet ("/compose/down/stream", ComposeDownStream);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
@@ -131,17 +132,49 @@ public static class InfraEndpoints
|
|||||||
private static Task ComposeUpForceStream(HttpContext ctx, IConfiguration config, CancellationToken ct) =>
|
private static Task ComposeUpForceStream(HttpContext ctx, IConfiguration config, CancellationToken ct) =>
|
||||||
StreamComposeOutput(ctx, config, "up -d --force-recreate --remove-orphans", ct);
|
StreamComposeOutput(ctx, config, "up -d --force-recreate --remove-orphans", ct);
|
||||||
|
|
||||||
|
// Nuke: force-removes every known platform container by name first (kills orphans that
|
||||||
|
// --remove-orphans won't touch because they belong to a different compose project),
|
||||||
|
// then runs a fresh compose up.
|
||||||
|
private static async Task ComposeNukeStream(HttpContext ctx, IConfiguration config, CancellationToken ct)
|
||||||
|
{
|
||||||
|
ctx.Response.Headers.ContentType = "text/event-stream";
|
||||||
|
ctx.Response.Headers.CacheControl = "no-cache";
|
||||||
|
ctx.Response.Headers.Connection = "keep-alive";
|
||||||
|
|
||||||
|
async Task Send(string line)
|
||||||
|
{
|
||||||
|
await ctx.Response.WriteAsync($"data: {line}\n\n", ct);
|
||||||
|
await ctx.Response.Body.FlushAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Send("▶ Removing all known platform containers…");
|
||||||
|
foreach (var container in PlatformContainers)
|
||||||
|
{
|
||||||
|
var (code, _) = await DockerAsync($"rm -f {container}");
|
||||||
|
await Send(code == 0
|
||||||
|
? $" ✔ removed {container}"
|
||||||
|
: $" · {container} not found (skipped)");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Send("▶ Running compose up…");
|
||||||
|
await StreamComposeOutput(ctx, config, "up -d", ct, skipHeaders: true);
|
||||||
|
}
|
||||||
|
|
||||||
private static Task ComposeDownStream(HttpContext ctx, IConfiguration config, CancellationToken ct) =>
|
private static Task ComposeDownStream(HttpContext ctx, IConfiguration config, CancellationToken ct) =>
|
||||||
StreamComposeOutput(ctx, config, "down", ct);
|
StreamComposeOutput(ctx, config, "down", ct);
|
||||||
|
|
||||||
private static async Task StreamComposeOutput(
|
private static async Task StreamComposeOutput(
|
||||||
HttpContext ctx, IConfiguration config, string composeArgs, CancellationToken ct)
|
HttpContext ctx, IConfiguration config, string composeArgs, CancellationToken ct,
|
||||||
|
bool skipHeaders = false)
|
||||||
{
|
{
|
||||||
var infraDir = ResolveInfraPath(config);
|
var infraDir = ResolveInfraPath(config);
|
||||||
|
|
||||||
|
if (!skipHeaders)
|
||||||
|
{
|
||||||
ctx.Response.Headers.ContentType = "text/event-stream";
|
ctx.Response.Headers.ContentType = "text/event-stream";
|
||||||
ctx.Response.Headers.CacheControl = "no-cache";
|
ctx.Response.Headers.CacheControl = "no-cache";
|
||||||
ctx.Response.Headers.Connection = "keep-alive";
|
ctx.Response.Headers.Connection = "keep-alive";
|
||||||
|
}
|
||||||
|
|
||||||
var channel = System.Threading.Channels.Channel.CreateUnbounded<string?>(
|
var channel = System.Threading.Channels.Channel.CreateUnbounded<string?>(
|
||||||
new System.Threading.Channels.UnboundedChannelOptions { SingleWriter = false, SingleReader = true });
|
new System.Threading.Channels.UnboundedChannelOptions { SingleWriter = false, SingleReader = true });
|
||||||
|
|||||||
@@ -44,6 +44,18 @@ export function streamComposeForceUp(onLine: (line: string) => void, onDone: ()
|
|||||||
return src;
|
return src;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nuke & Recreate — force-removes every known platform container by name first
|
||||||
|
* (kills orphans that --remove-orphans won't touch), then runs compose up fresh.
|
||||||
|
* Use this when Force Recreate still fails with "container name already in use".
|
||||||
|
*/
|
||||||
|
export function streamComposeNuke(onLine: (line: string) => void, onDone: () => void): EventSource {
|
||||||
|
const src = new EventSource(`${BASE_URL}/api/infra/compose/nuke/stream`);
|
||||||
|
src.onmessage = (e) => onLine(e.data);
|
||||||
|
src.onerror = () => { onDone(); src.close(); };
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
|
||||||
export function streamComposeDown(onLine: (line: string) => void, onDone: () => void): EventSource {
|
export function streamComposeDown(onLine: (line: string) => void, onDone: () => void): EventSource {
|
||||||
const src = new EventSource(`${BASE_URL}/api/infra/compose/down/stream`);
|
const src = new EventSource(`${BASE_URL}/api/infra/compose/down/stream`);
|
||||||
src.onmessage = (e) => onLine(e.data);
|
src.onmessage = (e) => onLine(e.data);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { getImageStatus, getBuildHistory, type ImageBuildStatus, type BuildRecord } from '../api/provisioningApi';
|
import { getImageStatus, getBuildHistory, type ImageBuildStatus, type BuildRecord } from '../api/provisioningApi';
|
||||||
import {
|
import {
|
||||||
getInfraStatus, streamComposeUp, streamComposeForceUp, streamComposeDown,
|
getInfraStatus, streamComposeUp, streamComposeForceUp, streamComposeNuke, streamComposeDown,
|
||||||
type InfraService,
|
type InfraService,
|
||||||
} from '../api/infraApi';
|
} from '../api/infraApi';
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ function BuildHistoryTable({ records }: { records: BuildRecord[] }) {
|
|||||||
function PlatformTab() {
|
function PlatformTab() {
|
||||||
const [services, setServices] = useState<InfraService[]>([]);
|
const [services, setServices] = useState<InfraService[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [composeBusy, setBusy] = useState<'up' | 'force' | 'down' | null>(null);
|
const [composeBusy, setBusy] = useState<'up' | 'force' | 'nuke' | 'down' | null>(null);
|
||||||
const [lines, setLines] = useState<string[]>([]);
|
const [lines, setLines] = useState<string[]>([]);
|
||||||
const sseRef = useRef<EventSource | null>(null);
|
const sseRef = useRef<EventSource | null>(null);
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ function PlatformTab() {
|
|||||||
|
|
||||||
function startStream(
|
function startStream(
|
||||||
streamer: (onLine: (l: string) => void, onDone: () => void) => EventSource,
|
streamer: (onLine: (l: string) => void, onDone: () => void) => EventSource,
|
||||||
label: 'up' | 'force' | 'down',
|
label: 'up' | 'force' | 'nuke' | 'down',
|
||||||
) {
|
) {
|
||||||
sseRef.current?.close();
|
sseRef.current?.close();
|
||||||
setLines([`▶ compose ${label}…`]);
|
setLines([`▶ compose ${label}…`]);
|
||||||
@@ -161,6 +161,12 @@ function PlatformTab() {
|
|||||||
onClick={() => startStream(streamComposeForceUp, 'force')}
|
onClick={() => startStream(streamComposeForceUp, 'force')}
|
||||||
title="Force-recreate all containers and remove orphans. Fixes 'container name already in use' errors."
|
title="Force-recreate all containers and remove orphans. Fixes 'container name already in use' errors."
|
||||||
>Force Recreate</Button>
|
>Force Recreate</Button>
|
||||||
|
<Button
|
||||||
|
small icon="flame" intent={Intent.DANGER}
|
||||||
|
loading={composeBusy === 'nuke'} disabled={composeBusy !== null}
|
||||||
|
onClick={() => startStream(streamComposeNuke, 'nuke')}
|
||||||
|
title="Force-removes every platform container by name then runs compose up. Use when Force Recreate still fails with name conflicts."
|
||||||
|
>Nuke & Recreate</Button>
|
||||||
<Button
|
<Button
|
||||||
small icon="stop" intent={Intent.DANGER}
|
small icon="stop" intent={Intent.DANGER}
|
||||||
loading={composeBusy === 'down'} disabled={composeBusy !== null}
|
loading={composeBusy === 'down'} disabled={composeBusy !== null}
|
||||||
|
|||||||
Reference in New Issue
Block a user