diff --git a/src/bin/start-keycloak/getQuayIoKeycloakDockerImageTags.ts b/src/bin/start-keycloak/getQuayIoKeycloakDockerImageTags.ts deleted file mode 100644 index 95ea50e0..00000000 --- a/src/bin/start-keycloak/getQuayIoKeycloakDockerImageTags.ts +++ /dev/null @@ -1,117 +0,0 @@ -import fetch from "make-fetch-happen"; -import type { BuildContext } from "../shared/buildContext"; -import { assert } from "tsafe/assert"; -import { z } from "zod"; -import { SemVer } from "../tools/SemVer"; -import { exclude } from "tsafe/exclude"; -import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig"; - -export type BuildContextLike = { - fetchOptions: BuildContext["fetchOptions"]; -}; - -assert; - -let cache: string[] | undefined = undefined; - -export async function getKeycloakDockerImageLatestSemVerTagsForEveryMajors(params: { - buildContext: BuildContextLike; -}) { - if (cache !== undefined) { - return cache; - } - - const { buildContext } = params; - - const tags: string[] = []; - - await (async function callee(url: string) { - const r = await fetch(url, buildContext.fetchOptions); - - await Promise.all([ - (async () => { - tags.push( - ...z - .object({ - tags: z.array(z.string()) - }) - .parse(await r.json()).tags - ); - })(), - (async () => { - const link = r.headers.get("link"); - - if (link === null) { - return; - } - - const split = link.split(";").map(s => s.trim()); - - assert(split.length === 2); - - assert(split[1] === 'rel="next"'); - - const match = split[0].match(/^<(.+)>$/); - - assert(match !== null); - - const nextUrl = new URL(url).origin + match[1]; - - await callee(nextUrl); - })() - ]); - })("https://quay.io/v2/keycloak/keycloak/tags/list"); - - const arr = tags - .map(tag => ({ - tag, - version: (() => { - if (tag.includes("-")) { - return undefined; - } - - let version: SemVer; - - try { - version = SemVer.parse(tag); - } catch { - return undefined; - } - - return version; - })() - })) - .map(({ tag, version }) => (version === undefined ? undefined : { tag, version })) - .filter(exclude(undefined)); - - const versionByMajor: Record = {}; - - for (const { version } of arr) { - const version_current = versionByMajor[version.major]; - - if ( - version_current === undefined || - SemVer.compare(version_current, version) === -1 - ) { - versionByMajor[version.major] = version; - } - } - - const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions(); - - cache = Object.entries(versionByMajor) - .sort(([a], [b]) => parseInt(b) - parseInt(a)) - .map(([, version]) => version) - .map(version => { - assert(version !== undefined); - - if (!supportedKeycloakMajorVersions.includes(version.major)) { - return undefined; - } - - return SemVer.stringify(version); - }) - .filter(exclude(undefined)); - - return cache; -} diff --git a/src/bin/start-keycloak/getSupportedDockerImageTags.ts b/src/bin/start-keycloak/getSupportedDockerImageTags.ts new file mode 100644 index 00000000..fdc33cb1 --- /dev/null +++ b/src/bin/start-keycloak/getSupportedDockerImageTags.ts @@ -0,0 +1,230 @@ +import fetch from "make-fetch-happen"; +import type { BuildContext } from "../shared/buildContext"; +import { assert, type Equals } from "tsafe/assert"; +import { id } from "tsafe/id"; +import { z } from "zod"; +import { SemVer } from "../tools/SemVer"; +import { exclude } from "tsafe/exclude"; +import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig"; +import { join as pathJoin, dirname as pathDirname } from "path"; +import * as fs from "fs/promises"; +import { existsAsync } from "../tools/fs.existsAsync"; +import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion"; + +export type BuildContextLike = { + fetchOptions: BuildContext["fetchOptions"]; + cacheDirPath: string; +}; + +assert; + +export async function getSupportedDockerImageTags(params: { + buildContext: BuildContextLike; +}) { + const { buildContext } = params; + + { + const result = await getCachedValue({ cacheDirPath: buildContext.cacheDirPath }); + + if (result !== undefined) { + return result; + } + } + + const tags: string[] = []; + + await (async function callee(url: string) { + const r = await fetch(url, buildContext.fetchOptions); + + await Promise.all([ + (async () => { + tags.push( + ...z + .object({ + tags: z.array(z.string()) + }) + .parse(await r.json()).tags + ); + })(), + (async () => { + const link = r.headers.get("link"); + + if (link === null) { + return; + } + + const split = link.split(";").map(s => s.trim()); + + assert(split.length === 2); + + assert(split[1] === 'rel="next"'); + + const match = split[0].match(/^<(.+)>$/); + + assert(match !== null); + + const nextUrl = new URL(url).origin + match[1]; + + await callee(nextUrl); + })() + ]); + })("https://quay.io/v2/keycloak/keycloak/tags/list"); + + const arr = tags + .map(tag => ({ + tag, + version: (() => { + if (tag.includes("-")) { + return undefined; + } + + let version: SemVer; + + try { + version = SemVer.parse(tag); + } catch { + return undefined; + } + + return version; + })() + })) + .map(({ tag, version }) => (version === undefined ? undefined : { tag, version })) + .filter(exclude(undefined)); + + const versionByMajor: Record = {}; + + for (const { version } of arr) { + const version_current = versionByMajor[version.major]; + + if ( + version_current === undefined || + SemVer.compare(version_current, version) === -1 + ) { + versionByMajor[version.major] = version; + } + } + + const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions(); + + const result = Object.entries(versionByMajor) + .sort(([a], [b]) => parseInt(b) - parseInt(a)) + .map(([, version]) => version) + .map(version => { + assert(version !== undefined); + + if (!supportedKeycloakMajorVersions.includes(version.major)) { + return undefined; + } + + return SemVer.stringify(version); + }) + .filter(exclude(undefined)); + + await setCachedValue({ cacheDirPath: buildContext.cacheDirPath, result }); + + return result; +} + +const { getCachedValue, setCachedValue } = (() => { + type Cache = { + keycloakifyVersion: string; + time: number; + result: string[]; + }; + + const zCache = (() => { + type TargetType = Cache; + + const zTargetType = z.object({ + keycloakifyVersion: z.string(), + time: z.number(), + result: z.array(z.string()) + }); + + type InferredType = z.infer; + + assert>; + + return id>(zTargetType); + })(); + + let inMemoryCachedResult: Cache["result"] | undefined = undefined; + + function getCacheFilePath(params: { cacheDirPath: string }) { + const { cacheDirPath } = params; + + return pathJoin(cacheDirPath, "supportedDockerImageTags.json"); + } + + async function getCachedValue(params: { cacheDirPath: string }) { + const { cacheDirPath } = params; + + if (inMemoryCachedResult !== undefined) { + return inMemoryCachedResult; + } + + const cacheFilePath = getCacheFilePath({ cacheDirPath }); + + if (!(await existsAsync(cacheFilePath))) { + return undefined; + } + + let cache: Cache | undefined; + + try { + cache = zCache.parse(JSON.parse(await fs.readFile(cacheFilePath, "utf8"))); + } catch { + return undefined; + } + + if (cache.keycloakifyVersion !== readThisNpmPackageVersion()) { + return undefined; + } + + if (Date.now() - cache.time > 3_600 * 24) { + return undefined; + } + + inMemoryCachedResult = cache.result; + + return cache.result; + } + + async function setCachedValue(params: { + cacheDirPath: string; + result: Cache["result"]; + }) { + const { cacheDirPath, result } = params; + + inMemoryCachedResult = result; + + const cacheFilePath = getCacheFilePath({ cacheDirPath }); + + { + const dirPath = pathDirname(cacheFilePath); + + if (!(await existsAsync(dirPath))) { + await fs.mkdir(dirPath, { recursive: true }); + } + } + + await fs.writeFile( + cacheFilePath, + JSON.stringify( + zCache.parse({ + keycloakifyVersion: readThisNpmPackageVersion(), + time: Date.now(), + result + }), + null, + 2 + ) + ); + } + + return { + getCachedValue, + setCachedValue + }; +})(); diff --git a/src/bin/start-keycloak/start-keycloak.ts b/src/bin/start-keycloak/start-keycloak.ts index 69b9d175..3116fb71 100644 --- a/src/bin/start-keycloak/start-keycloak.ts +++ b/src/bin/start-keycloak/start-keycloak.ts @@ -31,7 +31,7 @@ import { rm } from "../tools/fs.rm"; import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive"; import { startViteDevServer } from "./startViteDevServer"; import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig"; -import { getKeycloakDockerImageLatestSemVerTagsForEveryMajors } from "./getQuayIoKeycloakDockerImageTags"; +import { getSupportedDockerImageTags } from "./getSupportedDockerImageTags"; import { getRealmConfig } from "./realmConfig"; export async function command(params: { @@ -96,7 +96,7 @@ export async function command(params: { const { cliCommandOptions, buildContext } = params; - const availableTags = await getKeycloakDockerImageLatestSemVerTagsForEveryMajors({ + const availableTags = await getSupportedDockerImageTags({ buildContext });