feat(v5): add app configuration inspector

Add a sheet-based dashboard inspector with overview, networking, and
advanced tabs for applications.

Switch Caddy proxy actions to the generic Flux ingress apply and stop
payloads.
This commit is contained in:
Andras Bacsai
2026-06-21 18:58:01 +02:00
parent a42785189e
commit 22e3d6960e
8 changed files with 317 additions and 34 deletions
+16 -1
View File
@@ -27,7 +27,7 @@ class StartCaddyIngress
}
$configuration = GenerateCaddyIngressConfiguration::run($this->applications($server));
$output = $this->fluxClient->applyCaddyIngress($hostId, $configuration['caddyfile'], $configuration['apps']);
$output = $this->fluxClient->applyIngress($hostId, 'caddy', $configuration['caddyfile'], $this->ingressApps($configuration['apps']));
if ($server->exists) {
$server->update([
@@ -39,6 +39,21 @@ class StartCaddyIngress
return $output;
}
/**
* @param array<int, array{name: string, caddyfile: string}> $apps
* @return array<int, array{name: string, config: string}>
*/
private function ingressApps(array $apps): array
{
return array_map(
fn (array $app): array => [
'name' => $app['name'],
'config' => $app['caddyfile'],
],
$apps
);
}
/**
* @return Collection<int, Application>
*/
+1 -1
View File
@@ -20,7 +20,7 @@ class StopCaddyIngress
return 'Server is missing its Flux host id.';
}
$output = $this->fluxClient->stopCaddyIngress($hostId);
$output = $this->fluxClient->stopIngress($hostId, 'caddy');
if ($server->exists) {
$server->update(['ingress_status' => 'exited']);
+10 -8
View File
@@ -22,27 +22,29 @@ class FluxClient
}
/**
* @param array<int, array{name: string, caddyfile: string}> $apps
* @param array<int, array{name: string, config: string}> $apps
*/
public function applyCaddyIngress(string $hostId, string $caddyfile, array $apps = [], string $meshNetwork = 'coolify-default-mesh'): string
public function applyIngress(string $hostId, string $kind, string $config, array $apps = [], string $meshNetwork = 'coolify-default-mesh'): string
{
$payload = $this->dispatch($hostId, [
'type' => 'apply_caddy_ingress',
'caddyfile' => $caddyfile,
'type' => 'ingress.apply',
'kind' => $kind,
'config' => $config,
'apps' => $apps,
'mesh_network' => $meshNetwork,
]);
return $this->output($payload, 'Caddy ingress applied.');
return $this->output($payload, 'Ingress applied.');
}
public function stopCaddyIngress(string $hostId): string
public function stopIngress(string $hostId, string $kind): string
{
$payload = $this->dispatch($hostId, [
'type' => 'stop_caddy_ingress',
'type' => 'ingress.stop',
'kind' => $kind,
]);
return $this->output($payload, 'Caddy ingress stopped.');
return $this->output($payload, 'Ingress stopped.');
}
/**
+169
View File
@@ -6,6 +6,9 @@ import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Field, FieldLabel } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Textarea } from '@/components/ui/textarea';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { resolveCanvasNodeLayout, resolveCanvasNodePosition, type CanvasNodeBounds } from '@/lib/canvas-collision';
import { csrfToken } from '@/lib/csrf';
@@ -152,6 +155,7 @@ export default function Dashboard({
const [connections, setConnections] = useState<CanvasConnection[]>(initialResourceConnections);
const [selectedConnectionId, setSelectedConnectionId] = useState<string | null>(null);
const [selectedApplicationId, setSelectedApplicationId] = useState<string | null>(null);
const [selectedInspectorApplicationId, setSelectedInspectorApplicationId] = useState<string | null>(null);
const [connectionPortInput, setConnectionPortInput] = useState<Record<string, string>>({});
const [draftConnection, setDraftConnection] = useState<DraftConnection | null>(null);
const [viewport, setViewport] = useState<Viewport>({ x: 0, y: 0, zoom: 1 });
@@ -173,6 +177,10 @@ export default function Dashboard({
}),
[applications],
);
const selectedInspectorApplication = useMemo(
() => applications.find((application) => application.id === selectedInspectorApplicationId) ?? null,
[applications, selectedInspectorApplicationId],
);
useEffect(() => {
const settledResources = settleCanvasResources(initialApplications, caddyIngresses);
@@ -183,6 +191,7 @@ export default function Dashboard({
setSelectedNginxServerId((currentServerId) => currentServerId || nginxServers[0]?.id || '');
setSelectedConnectionId(null);
setSelectedApplicationId(null);
setSelectedInspectorApplicationId(null);
centerOnCanvasNodes(settledResources.applications, settledResources.ingresses);
}, [initialApplications, caddyIngresses, initialResourceConnections, selectedProjectUuid, selectedEnvironmentUuid]);
@@ -1029,6 +1038,12 @@ function normalizeConnection(connection: V5ResourceConnection): CanvasConnection
setSelectedApplicationId(null);
}
function openApplicationInspector(event: MouseEvent<HTMLElement>, application: V5Application): void {
event.stopPropagation();
setSelectedApplicationId(application.id);
setSelectedInspectorApplicationId(application.id);
}
function connectionTargetFromPointer(event: PointerEvent<HTMLDivElement>): HTMLElement | null {
const pointerTarget = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement | null;
@@ -1586,6 +1601,7 @@ function normalizeConnection(connection: V5ResourceConnection): CanvasConnection
transform: `translate3d(${application.canvasX}px, ${application.canvasY}px, 0)`,
}}
onPointerDown={(event) => startApplicationDrag(event, application)}
onDoubleClick={(event) => openApplicationInspector(event, application)}
>
{CONNECTOR_SIDES.map((side) => (
<button
@@ -1626,6 +1642,14 @@ function normalizeConnection(connection: V5ResourceConnection): CanvasConnection
>
{application.status}
</span>
<button
type="button"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => openApplicationInspector(event, application)}
className="rounded-md border border-border px-2 py-1 text-[0.625rem] font-semibold uppercase tracking-wide text-foreground transition hover:bg-muted"
>
Configure
</button>
<button
type="button"
onPointerDown={(event) => event.stopPropagation()}
@@ -1672,6 +1696,151 @@ function normalizeConnection(connection: V5ResourceConnection): CanvasConnection
</main>
</div>
<Sheet
open={selectedInspectorApplication !== null}
onOpenChange={(open) => {
if (!open) {
setSelectedInspectorApplicationId(null);
}
}}
>
<SheetContent side="right" className="w-full overflow-y-auto bg-background sm:rounded-l-xl sm:border data-[side=right]:sm:!inset-y-4 data-[side=right]:sm:!h-auto data-[side=right]:sm:!w-[45vw] data-[side=right]:sm:!max-w-[45vw]" showCloseButton blurOverlay={false}>
{selectedInspectorApplication && (
<>
<SheetHeader className="p-6 pb-4">
<SheetTitle>App configuration</SheetTitle>
<SheetDescription>
Double-click an application card to open configuration. Review runtime, networking, and advanced settings for{' '}
{selectedInspectorApplication.name}.
</SheetDescription>
</SheetHeader>
<div className="flex flex-1 flex-col gap-6 px-6 pb-6">
<Tabs defaultValue="overview" className="gap-4">
<TabsList className="w-full justify-start overflow-x-auto" variant="line">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="networking">Networking</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="flex flex-col gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Field>
<FieldLabel>Name</FieldLabel>
<Input value={selectedInspectorApplication.name} readOnly />
</Field>
<Field>
<FieldLabel>Status</FieldLabel>
<Input value={selectedInspectorApplication.status} readOnly />
</Field>
<Field>
<FieldLabel>Image</FieldLabel>
<Input value={selectedInspectorApplication.image} readOnly />
</Field>
<Field>
<FieldLabel>Server</FieldLabel>
<Input value={selectedInspectorApplication.serverName ?? 'Unknown'} readOnly />
</Field>
<Field>
<FieldLabel>Container</FieldLabel>
<Input value={selectedInspectorApplication.containerName} readOnly />
</Field>
<Field>
<FieldLabel>Runtime container ID</FieldLabel>
<Input value={selectedInspectorApplication.runtimeContainerId ?? 'Not available'} readOnly />
</Field>
</div>
<Field>
<FieldLabel>Status message</FieldLabel>
<Textarea
value={selectedInspectorApplication.statusMessage ?? 'No status message yet.'}
readOnly
className="min-h-20"
/>
</Field>
</TabsContent>
<TabsContent value="networking" className="flex flex-col gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Field>
<FieldLabel>Mesh namespace</FieldLabel>
<Input value={selectedInspectorApplication.meshNamespace} readOnly />
</Field>
<Field>
<FieldLabel>Mesh FQDN</FieldLabel>
<Input value={selectedInspectorApplication.meshFqdn} readOnly />
</Field>
<Field>
<FieldLabel>Internal port</FieldLabel>
<Input value={selectedInspectorApplication.internalPort?.toString() ?? 'Not configured'} readOnly />
</Field>
<Field>
<FieldLabel>Public ingress</FieldLabel>
<Input value={selectedInspectorApplication.ingressEnabled ? 'Enabled' : 'Private'} readOnly />
</Field>
</div>
<Field>
<FieldLabel>Domains</FieldLabel>
<Textarea
value={
selectedInspectorApplication.domains.length > 0
? selectedInspectorApplication.domains.join('\n')
: 'No public domains configured.'
}
readOnly
/>
</Field>
<div className="flex flex-wrap items-center gap-2">
{renderIngressButton(selectedInspectorApplication)}
<span className="text-xs text-muted-foreground">
Use this action to publish or private-route the app through the server ingress.
</span>
</div>
</TabsContent>
<TabsContent value="advanced" className="flex flex-col gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Field>
<FieldLabel>Application ID</FieldLabel>
<Input value={selectedInspectorApplication.id} readOnly />
</Field>
<Field>
<FieldLabel>Canvas position</FieldLabel>
<Input
value={`${selectedInspectorApplication.canvasX}, ${selectedInspectorApplication.canvasY}`}
readOnly
/>
</Field>
</div>
<Field>
<FieldLabel>Raw app config</FieldLabel>
<Textarea
value={JSON.stringify(selectedInspectorApplication, null, 2)}
readOnly
className="min-h-80 font-mono text-xs"
/>
</Field>
</TabsContent>
</Tabs>
</div>
</>
)}
</SheetContent>
</Sheet>
{ingressModal && (
<Dialog
open
+12 -3
View File
@@ -21,12 +21,19 @@ function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
function SheetOverlay({
className,
blur = true,
...props
}: SheetPrimitive.Backdrop.Props & {
blur?: boolean;
}) {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
'fixed inset-0 z-50 bg-black/10 text-xs/relaxed transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs',
'fixed inset-0 z-50 bg-black/10 text-xs/relaxed transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0',
blur && 'supports-backdrop-filter:backdrop-blur-xs',
className,
)}
{...props}
@@ -39,14 +46,16 @@ function SheetContent({
children,
side = 'right',
showCloseButton = true,
blurOverlay = true,
...props
}: SheetPrimitive.Popup.Props & {
side?: 'top' | 'right' | 'bottom' | 'left';
showCloseButton?: boolean;
blurOverlay?: boolean;
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetOverlay blur={blurOverlay} />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
+63
View File
@@ -0,0 +1,63 @@
import { Tabs as TabsPrimitive } from '@base-ui/react/tabs';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
function Tabs({ className, orientation = 'horizontal', ...props }: TabsPrimitive.Root.Props) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn('group/tabs flex gap-2 data-horizontal:flex-col', className)}
{...props}
/>
);
}
const tabsListVariants = cva(
'group/tabs-list inline-flex w-fit items-center justify-center rounded-none p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none',
{
variants: {
variant: {
default: 'bg-muted',
line: 'gap-1 bg-transparent',
},
},
defaultVariants: {
variant: 'default',
},
},
);
function TabsList({ className, variant = 'default', ...props }: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
);
}
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
return (
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-none border border-transparent px-1.5 py-0.5 text-xs font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:py-[calc(--spacing(1.25))] hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
'data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground',
'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
className,
)}
{...props}
/>
);
}
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
return <TabsPrimitive.Panel data-slot="tabs-content" className={cn('flex-1 text-xs/relaxed outline-none', className)} {...props} />;
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
+40 -16
View File
@@ -184,15 +184,16 @@ it('generates http-only caddy routes for application ingress', function () {
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient
->shouldReceive('applyCaddyIngress')
->shouldReceive('applyIngress')
->once()
->with(
'100.64.0.10',
'caddy',
Mockery::on(fn (string $caddyfile): bool => str_contains($caddyfile, 'import apps/*.caddy')),
Mockery::on(fn (array $apps): bool => count($apps) === 1
&& str_contains($apps[0]['caddyfile'], 'http://app.example.com {')
&& ! str_contains($apps[0]['caddyfile'], 'https://')
&& str_contains($apps[0]['caddyfile'], 'reverse_proxy coolify-v5-nginx-test.default.coolify.internal:3000'))
&& str_contains($apps[0]['config'], 'http://app.example.com {')
&& ! str_contains($apps[0]['config'], 'https://')
&& str_contains($apps[0]['config'], 'reverse_proxy coolify-v5-nginx-test.default.coolify.internal:3000'))
)
->andReturn('Caddy ingress applied.');
app()->instance(FluxClient::class, $fluxClient);
@@ -3241,7 +3242,7 @@ it('rejects application ingress when server ingress is disabled', function () {
]);
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient->shouldNotReceive('applyCaddyIngress');
$fluxClient->shouldNotReceive('applyIngress');
app()->instance(FluxClient::class, $fluxClient);
$this
@@ -3305,10 +3306,11 @@ it('enables application ingress without publishing domains by default', function
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient
->shouldReceive('applyCaddyIngress')
->shouldReceive('applyIngress')
->once()
->with(
'100.64.0.10',
'caddy',
Mockery::on(fn (string $caddyfile): bool => str_contains($caddyfile, 'import apps/*.caddy')),
[]
)
@@ -3371,7 +3373,7 @@ it('validates application ingress domains', function () {
]);
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient->shouldNotReceive('applyCaddyIngress');
$fluxClient->shouldNotReceive('applyIngress');
app()->instance(FluxClient::class, $fluxClient);
$this
@@ -3430,14 +3432,15 @@ it('enables application ingress with explicit domains and port', function () {
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient
->shouldReceive('applyCaddyIngress')
->shouldReceive('applyIngress')
->once()
->with(
'100.64.0.10',
'caddy',
Mockery::on(fn (string $caddyfile): bool => str_contains($caddyfile, 'import apps/*.caddy')),
Mockery::on(fn (array $apps): bool => count($apps) === 1
&& str_contains($apps[0]['caddyfile'], 'http://app.example.com {')
&& str_contains($apps[0]['caddyfile'], 'reverse_proxy coolify-v5-nginx-test.default.coolify.internal:3000'))
&& str_contains($apps[0]['config'], 'http://app.example.com {')
&& str_contains($apps[0]['config'], 'reverse_proxy coolify-v5-nginx-test.default.coolify.internal:3000'))
)
->andReturn('Caddy ingress applied.');
app()->instance(FluxClient::class, $fluxClient);
@@ -3502,7 +3505,7 @@ it('returns flux error details when application ingress sync fails', function ()
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient
->shouldReceive('applyCaddyIngress')
->shouldReceive('applyIngress')
->once()
->andThrow(new RuntimeException('start Caddy ingress: podman exited with status 125'));
app()->instance(FluxClient::class, $fluxClient);
@@ -3572,15 +3575,16 @@ it('syncs caddy ingress routes through flux when enabling ingress on an installe
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient
->shouldReceive('applyCaddyIngress')
->shouldReceive('applyIngress')
->once()
->with(
'100.64.0.10',
'caddy',
Mockery::on(fn (string $caddyfile): bool => str_contains($caddyfile, 'import apps/*.caddy')),
Mockery::on(fn (array $apps): bool => count($apps) === 1
&& str_contains($apps[0]['caddyfile'], 'http://nginx.example.com {')
&& str_contains($apps[0]['caddyfile'], 'http://www.nginx.example.com {')
&& str_contains($apps[0]['caddyfile'], 'reverse_proxy coolify-v5-nginx-test.default.coolify.internal:8080'))
&& str_contains($apps[0]['config'], 'http://nginx.example.com {')
&& str_contains($apps[0]['config'], 'http://www.nginx.example.com {')
&& str_contains($apps[0]['config'], 'reverse_proxy coolify-v5-nginx-test.default.coolify.internal:8080'))
)
->andReturn('Caddy ingress applied.');
app()->instance(FluxClient::class, $fluxClient);
@@ -3651,7 +3655,7 @@ it('returns flux error details when server ingress activation fails', function (
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient
->shouldReceive('applyCaddyIngress')
->shouldReceive('applyIngress')
->once()
->andThrow(new RuntimeException('validate Caddyfile: unrecognized directive'));
app()->instance(FluxClient::class, $fluxClient);
@@ -4076,6 +4080,8 @@ it('defines the v5 dashboard page as a shadcn styled canvas shell', function ()
->not->toContain('function csrfToken()')
->toContain("import { csrfToken } from '@/lib/csrf';")
->toContain("import { Button } from '@/components/ui/button';")
->toContain("import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';")
->toContain("import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';")
->not->toContain("fetch('/v5/clusters'")
->toContain('<AppNavbar')
->toContain('bg-background text-foreground')
@@ -4087,6 +4093,20 @@ it('defines the v5 dashboard page as a shadcn styled canvas shell', function ()
->toContain('server_id: selectedNginxServerId || null')
->toContain('Center')
->toContain('Delete')
->toContain('App configuration')
->toContain('openApplicationInspector')
->toContain('selectedInspectorApplication')
->toContain('onDoubleClick={(event) => openApplicationInspector(event, application)}')
->toContain('open={selectedInspectorApplication !== null}')
->toContain('<SheetContent side="right" className="w-full overflow-y-auto bg-background sm:rounded-l-xl sm:border data-[side=right]:sm:!inset-y-4 data-[side=right]:sm:!h-auto data-[side=right]:sm:!w-[45vw] data-[side=right]:sm:!max-w-[45vw]"')
->toContain('showCloseButton blurOverlay={false}')
->toContain('<SheetHeader className="p-6 pb-4">')
->toContain('<div className="flex flex-1 flex-col gap-6 px-6 pb-6">')
->toContain('<Tabs defaultValue="overview"')
->toContain('<TabsTrigger value="overview">Overview</TabsTrigger>')
->toContain('<TabsTrigger value="networking">Networking</TabsTrigger>')
->toContain('<TabsTrigger value="advanced">Advanced</TabsTrigger>')
->toContain('Double-click an application card to open configuration.')
->toContain("method: 'DELETE'")
->toContain('removeApplication')
->toContain('useEffect(() => {')
@@ -4106,6 +4126,10 @@ it('defines the v5 dashboard page as a shadcn styled canvas shell', function ()
->not->toContain("import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';")
->not->toContain("fetch('/v5/selection'");
expect(file_get_contents($sheetPath))
->toContain('blurOverlay = true')
->toContain('<SheetOverlay blur={blurOverlay} />');
expect($navbar)
->toContain("import { Link, router, usePage } from '@inertiajs/react';")
->toContain("import { cn } from '@/lib/utils';")
@@ -110,10 +110,11 @@ it('applies caddy ingress configuration through flux instead of ssh', function (
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient
->shouldReceive('applyCaddyIngress')
->shouldReceive('applyIngress')
->once()
->with(
'100.64.0.10',
'caddy',
Mockery::on(fn (string $caddyfile): bool => str_contains($caddyfile, 'respond /coolify-health 200')),
[]
)
@@ -144,9 +145,9 @@ it('stops caddy ingress through flux instead of ssh', function () {
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient
->shouldReceive('stopCaddyIngress')
->shouldReceive('stopIngress')
->once()
->with('100.64.0.10')
->with('100.64.0.10', 'caddy')
->andReturn('Caddy ingress stopped.');
app()->instance(FluxClient::class, $fluxClient);
@@ -173,7 +174,7 @@ it('includes flux error response details when dispatch returns a non success sta
'Content-Length: '.strlen($body)."\r\n".
"\r\n".
$body,
fn () => (new FluxClient)->applyCaddyIngress('100.64.0.10', 'example.com { respond "ok" }')
fn () => (new FluxClient)->applyIngress('100.64.0.10', 'caddy', 'example.com { respond "ok" }')
);
})->throws(RuntimeException::class, 'start Caddy ingress: podman exited with status 125');
@@ -184,7 +185,7 @@ it('uses a friendly message when flux returns an invalid http response', functio
withFakeFluxSocket(
'',
fn () => (new FluxClient)->applyCaddyIngress('100.64.0.10', 'example.com { respond "ok" }')
fn () => (new FluxClient)->applyIngress('100.64.0.10', 'caddy', 'example.com { respond "ok" }')
);
})->throws(RuntimeException::class, 'Flux did not return a response before the timeout.');