Route selected release runtime through gateway

This commit is contained in:
Jeppe Bundgaard
2026-05-20 18:32:21 +02:00
parent ea8db593c5
commit 261d84bd01
5 changed files with 52 additions and 12 deletions
+4 -3
View File
@@ -48,6 +48,7 @@ import Swal from "sweetalert2";
import { SubuserGrants } from "@/components/session/token/SessionUser/Objects/SubuserGrants.vue";
import { Subusers } from "@/components/session/token/SessionUser/Objects/Subusers.vue";
import { configureReleaseRuntime } from "@/services/releaseTimeline.js";
import { fetchReleaseRuntime } from "@/services/releaseBootstrap.js";
import {
isReleaseChannelApiAvailabilityError,
markReleaseChannelApiUnavailable,
@@ -128,9 +129,9 @@ const applyReleaseRuntimeConfig = (runtime) => {
};
export const refreshReleaseRuntime = async () => {
return authenticatedRequest("/release/runtime", "GET", releaseChannelRuntimeRequestParams())
.then((response) => {
applyReleaseRuntimeConfig(response?.data?.data || response?.data || {});
return fetchReleaseRuntime()
.then((runtime) => {
applyReleaseRuntimeConfig(runtime || {});
})
.catch((error) => {
console.warn("Could not refresh release runtime", error);
+3
View File
@@ -26,6 +26,9 @@ export const API_URL = normalizeApiUrl(
export const RELEASE_MANAGER_CONTROL_API_URL = normalizeApiUrl(
import.meta.env.VITE_RELEASE_MANAGER_CONTROL_API_URL || API_URL
);
export const RELEASE_PUBLIC_GATEWAY_API_URL = normalizeApiUrl(
import.meta.env.VITE_RELEASE_PUBLIC_GATEWAY_API_URL || "https://api-v2.truckwash.io"
);
export const RELEASE_MANAGER_CONTROL_API_FALLBACK_URLS = String(
import.meta.env.VITE_RELEASE_MANAGER_CONTROL_API_FALLBACK_URLS ||
(IS_DEV ? "https://api.truckwash.io:4433,https://api.truckwash.io" : "")
+25 -5
View File
@@ -1,4 +1,4 @@
import { API_URL } from "@/config.js";
import { API_URL, RELEASE_PUBLIC_GATEWAY_API_URL } from "@/config.js";
import { buildReleaseHeaders } from "@/services/releaseHeaders.js";
export const RELEASE_RUNTIME_GLOBAL_KEY = "__TRUCKWASH_RELEASE_RUNTIME__";
@@ -30,6 +30,20 @@ const browserStorage = () => {
const readSelectedReleaseChannel = () =>
normalizeReleaseChannelSlug(browserStorage()?.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY) || "");
const releaseChannelGatewayApiBaseUrl = (channelSlug, gatewayBaseUrl = RELEASE_PUBLIC_GATEWAY_API_URL) => {
const slug = normalizeReleaseChannelSlug(channelSlug);
if (!slug || slug === "stable") {
return "";
}
const baseUrl = normalizeBaseUrl(gatewayBaseUrl);
if (!baseUrl || !/^https?:\/\//i.test(baseUrl)) {
return "";
}
return new URL(`${slug}/api/`, `${baseUrl}/`).href.replace(/\/+$/, "");
};
const buildRuntimeHeaders = () => {
const storage = browserStorage();
const headers = {
@@ -47,9 +61,11 @@ const buildRuntimeHeaders = () => {
return headers;
};
export const runtimeApiUrl = (apiBaseUrl = API_URL) => {
const url = new URL("release/runtime", `${normalizeBaseUrl(apiBaseUrl)}/`);
export const runtimeApiUrl = (apiBaseUrl = API_URL, gatewayBaseUrl = RELEASE_PUBLIC_GATEWAY_API_URL) => {
const selectedChannel = readSelectedReleaseChannel();
const selectedChannelApiBaseUrl = releaseChannelGatewayApiBaseUrl(selectedChannel, gatewayBaseUrl);
const runtimeBaseUrl = selectedChannelApiBaseUrl || normalizeBaseUrl(apiBaseUrl);
const url = new URL("release/runtime", `${runtimeBaseUrl}/`);
if (selectedChannel) {
url.searchParams.set("release_channel", selectedChannel);
}
@@ -71,12 +87,16 @@ const parseJsonResponse = async (response, label) => {
return response?.json?.();
};
export const fetchReleaseRuntime = async ({ fetchFn = globalThis.fetch, apiBaseUrl = API_URL } = {}) => {
export const fetchReleaseRuntime = async ({
fetchFn = globalThis.fetch,
apiBaseUrl = API_URL,
gatewayBaseUrl = RELEASE_PUBLIC_GATEWAY_API_URL,
} = {}) => {
if (typeof fetchFn !== "function") {
return null;
}
const response = await fetchFn(runtimeApiUrl(apiBaseUrl), {
const response = await fetchFn(runtimeApiUrl(apiBaseUrl, gatewayBaseUrl), {
method: "GET",
headers: buildRuntimeHeaders(),
credentials: "omit",
+8 -1
View File
@@ -15,6 +15,7 @@ const json = (body, status = 200) => ({
test("non-default release frontend loads from api-v2.truckwash.io without redirecting", async ({ page }) => {
const releaseApiRequests = [];
const releaseEntryRequests = [];
const runtimeRequests = [];
const runtime = {
generated_at: "2026-05-20T10:00:00.000Z",
trace_id: "trace-release-bootstrap",
@@ -46,7 +47,12 @@ test("non-default release frontend loads from api-v2.truckwash.io without redire
},
};
await page.route("**/release/runtime**", async (route) => {
await page.addInitScript(() => {
window.localStorage.setItem("release_channel_selected_slug", "canary");
});
await page.route("https://api-v2.truckwash.io/canary/api/release/runtime**", async (route) => {
runtimeRequests.push(route.request().url());
await route.fulfill(json({ data: runtime }));
});
await page.route("https://api-v2.truckwash.io/canary/frontend/release-entry.json", async (route) => {
@@ -89,6 +95,7 @@ test("non-default release frontend loads from api-v2.truckwash.io without redire
const currentOrigin = await page.evaluate(() => window.location.origin);
expect(page.url()).toContain("/shared/passkey-safe-link");
expect(page.url()).not.toContain("api-v2.truckwash.io");
expect(runtimeRequests).toEqual(["https://api-v2.truckwash.io/canary/api/release/runtime?release_channel=canary"]);
expect(releaseEntryRequests).toEqual(["https://api-v2.truckwash.io/canary/frontend/release-entry.json"]);
await expect.poll(() => releaseApiRequests.length).toBe(1);
expect(releaseApiRequests[0]).toBe("https://api-v2.truckwash.io/canary/api/ping");
+12 -3
View File
@@ -26,7 +26,15 @@ describe("release bootstrap", () => {
localStorage.setItem("release_channel_selected_slug", "Canary Preview!");
expect(runtimeApiUrl("https://api.truckwash.io")).toBe(
"https://api.truckwash.io/release/runtime?release_channel=canary-preview"
"https://api-v2.truckwash.io/canary-preview/api/release/runtime?release_channel=canary-preview"
);
});
it("keeps stable runtime requests on the configured control API", () => {
localStorage.setItem("release_channel_selected_slug", "stable");
expect(runtimeApiUrl("https://api.truckwash.io")).toBe(
"https://api.truckwash.io/release/runtime?release_channel=stable"
);
});
@@ -38,9 +46,10 @@ describe("release bootstrap", () => {
json: async () => ({ data: { channel: { slug: "internal" } } }),
}));
await fetchReleaseRuntime({ fetchFn, apiBaseUrl: "https://api-v2.truckwash.io" });
await fetchReleaseRuntime({ fetchFn, apiBaseUrl: "https://api.truckwash.io" });
const [, options] = fetchFn.mock.calls[0];
const [url, options] = fetchFn.mock.calls[0];
expect(url).toBe("https://api-v2.truckwash.io/internal/api/release/runtime?release_channel=internal");
expect(options.headers).toMatchObject({
"X-Release-Trace": "trace-runtime",
"X-Release-Channel": "internal",