Skip to content

Commit 5474ace

Browse files
committed
Fixed docker image health check bug and cleanup
1 parent 083d966 commit 5474ace

File tree

5 files changed

+249
-142
lines changed

5 files changed

+249
-142
lines changed

DUUIRestService/pom.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
<maven.compiler.target>21</maven.compiler.target>
4141
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
4242
<maven.javadoc.skip>true</maven.javadoc.skip>
43-
<duui.version>7a7eb64f3a13229a264931d8a86c7aaad444ae3b</duui.version>
43+
<duui.version>main-SNAPSHOT</duui.version>
4444
<uima.version>3.5.0</uima.version>
4545
<typesystem.version>3.0.14</typesystem.version>
4646
<maven.javadoc.skip>false</maven.javadoc.skip>
@@ -107,6 +107,7 @@
107107
<!-- sometimes used by a few libs -->
108108
<exclude>META-INF/*.EC</exclude>
109109
<exclude>META-INF/*SIG*</exclude>
110+
<exclude>module-info.class</exclude>
110111
</excludes>
111112
</filter>
112113
</filters>

DUUIRestService/src/main/java/org/texttechnologylab/duui/api/Methods.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,15 @@ public static void init() {
7171
if (!request.url().endsWith("metrics")) {
7272
DUUIHTTPMetrics.incrementTotalRequests();
7373
DUUIHTTPMetrics.incrementActiveRequests();
74-
log.info("Incoming request: {} {}", request.requestMethod(), request.url());
74+
String acceptHeader = request.headers("Accept");
75+
if (acceptHeader == null || acceptHeader.isEmpty()) {
76+
acceptHeader = "*/*";
77+
}
78+
String originHeader = request.headers("Origin");
79+
if (originHeader == null || originHeader.isEmpty()) {
80+
originHeader = request.ip();
81+
}
82+
log.info("{} {} – Origin: {} – Accept: {}", request.requestMethod(), request.pathInfo(), originHeader, acceptHeader);
7583
}
7684
response.header("Access-Control-Allow-Origin", "*");
7785
});

DUUIWeb/src/lib/svelte/components/Drawer/ComponentDrawer.svelte

Lines changed: 106 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@
260260
drawerStore.close()
261261
}
262262
263-
type DockerCheckStatus = 'idle' | 'pending' | 'valid' | 'invalid' | 'timeout'
263+
type DockerCheckStatus = 'idle' | 'pending' | 'valid' | 'invalid' | 'timeout' | 'warning'
264264
type DockerCheckState = {
265265
status: DockerCheckStatus
266266
message: string
@@ -269,6 +269,8 @@
269269
type DockerCheckResult = {
270270
ok: boolean
271271
timedOut?: boolean
272+
message?: string
273+
warning?: boolean
272274
}
273275
274276
const requiresDockerRegistry = (driver: string): boolean => RegistryDrivers.includes(driver)
@@ -277,13 +279,6 @@
277279
let dockerCheckAbort: AbortController | null = null
278280
let dockerCheckDebounce: ReturnType<typeof setTimeout> | null = null
279281
280-
class TimeoutError extends Error {
281-
constructor(message = 'Request timed out') {
282-
super(message)
283-
this.name = 'TimeoutError'
284-
}
285-
}
286-
287282
const resetDockerCheck = () => {
288283
if (dockerCheckAbort) {
289284
dockerCheckAbort.abort()
@@ -300,108 +295,45 @@
300295
resetDockerCheck()
301296
}
302297
303-
const fetchWithTimeout = async (
304-
url: string,
305-
init: RequestInit = {},
306-
timeoutMs: number | null = 5000,
307-
externalSignal?: AbortSignal
308-
): Promise<Response> => {
309-
const controller = new AbortController()
310-
const { signal } = controller
311-
let timedOut = false
312-
313-
let timeoutId: ReturnType<typeof setTimeout> | null = null
314-
if (timeoutMs !== null && Number.isFinite(timeoutMs) && timeoutMs > 0) {
315-
timeoutId = setTimeout(() => {
316-
timedOut = true
317-
controller.abort()
318-
}, timeoutMs)
319-
}
320-
321-
const cleanup = () => {
322-
if (timeoutId) clearTimeout(timeoutId)
323-
if (externalSignal && abortHandler) {
324-
externalSignal.removeEventListener('abort', abortHandler)
325-
}
326-
}
327-
328-
let abortHandler: (() => void) | null = null
329-
if (externalSignal) {
330-
if (externalSignal.aborted) {
331-
controller.abort()
332-
} else {
333-
abortHandler = () => controller.abort()
334-
externalSignal.addEventListener('abort', abortHandler)
335-
}
336-
}
337-
338-
try {
339-
return await fetch(url, { ...init, signal })
340-
} catch (error) {
341-
if (timedOut) {
342-
throw new TimeoutError()
343-
}
344-
throw error
345-
} finally {
346-
cleanup()
347-
}
348-
}
349-
350298
const checkDockerImageAvailability = async (
351299
imageUri: string,
352300
signal?: AbortSignal,
353301
timeoutMs: number | null = 5000
354302
): Promise<DockerCheckResult> => {
355303
if (!imageUri?.trim()) return { ok: false }
356304
357-
const normalized = imageUri.trim().replace(/^https?:\/\//, '')
358-
const protocol = imageUri.startsWith('http://') ? 'http' : 'https'
359-
360-
const lastAt = normalized.lastIndexOf('@')
361-
const lastColon = normalized.lastIndexOf(':')
362-
const separatorIndex = lastAt > -1 ? lastAt : lastColon
363-
364-
if (separatorIndex === -1) {
365-
return { ok: false }
366-
}
367-
368-
const hostAndRepo = normalized.slice(0, separatorIndex)
369-
const reference = normalized.slice(separatorIndex + 1)
370-
371-
if (!hostAndRepo || !reference) {
372-
return { ok: false }
373-
}
374-
375-
const [host, ...repoParts] = hostAndRepo.split('/')
376-
if (!host || repoParts.length === 0) {
377-
return { ok: false }
378-
}
379-
380-
const repository = repoParts.join('/')
381-
const manifestUrl = `${protocol}://${host}/v2/${repository}/manifests/${reference}`
382-
383305
try {
384-
const response = await fetchWithTimeout(
385-
manifestUrl,
386-
{
387-
method: 'GET',
388-
headers: {
389-
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
390-
}
306+
const response = await fetch('/api/components/registry', {
307+
method: 'POST',
308+
headers: {
309+
'Content-Type': 'application/json'
391310
},
392-
timeoutMs,
311+
body: JSON.stringify({
312+
imageUri,
313+
timeoutMs
314+
}),
393315
signal
394-
)
316+
})
395317
396-
return { ok: response.ok }
397-
} catch (error) {
398-
if ((error as Error).name === 'TimeoutError') {
399-
return { ok: false, timedOut: true }
318+
const payload = (await response.json().catch(() => null)) as DockerCheckResult | null
319+
if (!payload) {
320+
return {
321+
ok: false,
322+
message: 'Invalid response from verification service.'
323+
}
400324
}
325+
326+
return payload
327+
} catch (error) {
401328
if ((error as DOMException).name === 'AbortError') {
402329
throw error
403330
}
404-
throw error
331+
332+
return {
333+
ok: false,
334+
warning: true,
335+
message: 'Failed to contact verification service.'
336+
}
405337
}
406338
}
407339
@@ -436,17 +368,35 @@
436368
}
437369
} else if (result.ok) {
438370
dockerCheck = { status: 'valid', message: '' }
371+
} else if (result.warning) {
372+
dockerCheck = {
373+
status: 'warning',
374+
message:
375+
result.message ||
376+
'Could not verify Docker image because the request failed unexpectedly.'
377+
}
439378
} else {
440379
dockerCheck = {
441380
status: 'invalid',
442-
message: 'Docker image not reachable. Please ensure the image is available.'
381+
message:
382+
result.message ||
383+
'Docker image not reachable. Please ensure the image is available.'
443384
}
444385
}
445386
})
446387
.catch((error) => {
447388
if ((error as DOMException).name === 'AbortError') {
448389
return
449390
}
391+
392+
if (error instanceof TypeError) {
393+
dockerCheck = {
394+
status: 'warning',
395+
message: 'Could not contact the verification service. Please check your connection.'
396+
}
397+
return
398+
}
399+
450400
dockerCheck = {
451401
status: 'invalid',
452402
message: 'Failed to verify Docker image. Please check the URL and network.'
@@ -698,53 +648,75 @@
698648
{#if dockerCheck.status === 'invalid'}
699649
<Tip tipTheme="error">
700650
{dockerCheck.message}
701-
</Tip>
702-
{:else if dockerCheck.status === 'timeout'}
703-
<Tip tipTheme="tertiary">
704-
<div class="flex flex-wrap items-center justify-between gap-2">
705-
<span>{dockerCheck.message}</span>
706-
<div class="flex flex-wrap gap-2">
707-
<button
708-
type="button"
709-
class="button-warning"
710-
on:click={() => runDockerCheck(component.target, null)}
711-
>
712-
Retry (no timeout)
651+
</Tip>
652+
{:else if dockerCheck.status === 'timeout'}
653+
<Tip tipTheme="tertiary">
654+
<div class="flex flex-wrap items-center justify-between gap-2">
655+
<span>{dockerCheck.message}</span>
656+
<div class="flex flex-wrap gap-2">
657+
<button
658+
type="button"
659+
class="button-warning"
660+
on:click={() => runDockerCheck(component.target, null)}
661+
>
662+
Retry (no timeout)
663+
</button>
664+
<button
665+
type="button"
666+
class="button-neutral"
667+
on:click={cancelDockerCheck}
668+
>
669+
Cancel
670+
</button>
671+
</div>
672+
</div>
673+
</Tip>
674+
{:else if dockerCheck.status === 'pending'}
675+
<Tip tipTheme="primary">
676+
<div class="flex flex-wrap items-center justify-between gap-2">
677+
<span>Checking image availability…</span>
678+
<button type="button" class="button-neutral" on:click={cancelDockerCheck}>
679+
Cancel
713680
</button>
681+
</div>
682+
</Tip>
683+
{:else if dockerCheck.status === 'warning'}
684+
<Tip tipTheme="warning">
685+
<div class="flex flex-wrap items-center justify-between gap-2">
686+
<span>{dockerCheck.message}</span>
714687
<button
715688
type="button"
716689
class="button-neutral"
717-
on:click={cancelDockerCheck}
690+
on:click={() => runDockerCheck(component.target, null)}
718691
>
719-
Cancel
692+
Retry anyway
720693
</button>
721694
</div>
722-
</div>
723-
</Tip>
724-
{:else if dockerCheck.status === 'pending'}
725-
<Tip tipTheme="primary">
726-
<div class="flex flex-wrap items-center justify-between gap-2">
727-
<span>Checking image availability…</span>
728-
<button type="button" class="button-neutral" on:click={cancelDockerCheck}>
729-
Cancel
730-
</button>
731-
</div>
732-
</Tip>
695+
</Tip>
696+
{/if}
733697
{/if}
734-
{/if}
735-
736-
<TextInput
737-
style="md:col-span-2"
738-
label="Target"
739-
name="target"
740-
bind:value={component.target}
741-
error={component.target === '' ? "Target can't be empty" : ''}
742-
/>
743698

744-
<div class="hidden group-focus-within:block">
745-
<Tip>
746-
The target can be a Docker image name (Docker, Swarm and Kubernetes Driver), a URL
747-
(Remote Driver) or a Java class path (UIMADriver).
699+
<TextInput
700+
style="md:col-span-2"
701+
label="Target"
702+
name="target"
703+
bind:value={component.target}
704+
error={component.target === '' ? "Target can't be empty" : ''}
705+
>
706+
<span slot="labelContent" class="flex w-full items-center justify-between gap-3">
707+
<span>Target</span>
708+
{#if dockerCheck.status === 'valid'}
709+
<span class="badge variant-soft-success font-bold text-xs tracking-wide">
710+
IMAGE ONLINE
711+
</span>
712+
{/if}
713+
</span>
714+
</TextInput>
715+
716+
<div class="hidden group-focus-within:block">
717+
<Tip>
718+
The target can be a Docker image name (Docker, Swarm and Kubernetes Driver), a URL
719+
(Remote Driver) or a Java class path (UIMADriver).
748720
</Tip>
749721
</div>
750722
</div>

DUUIWeb/src/lib/svelte/components/Input/TextInput.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
{/if}
4343

4444
<label class="{label ? 'label' : ''} flex flex-col {hidden ? 'hidden' : ''} {style}">
45-
<span class="form-label flex-center-4">{label} </span>
45+
<span class="form-label flex-center-4">
46+
<slot name="labelContent">{label}</slot>
47+
</span>
4648

4749
<input
4850
{disabled}

0 commit comments

Comments
 (0)