diff --git a/deploy/helm/mock-fleet/templates/api-deployment.yaml b/deploy/helm/mock-fleet/templates/api-deployment.yaml index b7c1e9c..47b98c4 100644 --- a/deploy/helm/mock-fleet/templates/api-deployment.yaml +++ b/deploy/helm/mock-fleet/templates/api-deployment.yaml @@ -72,6 +72,10 @@ spec: value: wiremock-options.yaml - name: MOCK_FLEET_PROXY_DEPLOYMENT_NAME value: {{ include "mock-fleet.proxyFullname" . | quote }} + - name: MOCK_FLEET_PROXY_ROUTING_MODE + value: {{ .Values.fleet.proxy.routing.mode | quote }} + - name: MOCK_FLEET_PROXY_ROUTING_HOST + value: {{ .Values.ingress.host | quote }} - name: MOCK_FLEET_HAZELCAST_CLUSTER_NAME value: {{ .Values.hazelcast.clusterName | quote }} - name: MOCK_FLEET_HAZELCAST_SERVICE_DNS diff --git a/fleet-api/src/main/java/com/github/letsrokk/MockFleetConfig.java b/fleet-api/src/main/java/com/github/letsrokk/MockFleetConfig.java index 8208c72..920cec5 100644 --- a/fleet-api/src/main/java/com/github/letsrokk/MockFleetConfig.java +++ b/fleet-api/src/main/java/com/github/letsrokk/MockFleetConfig.java @@ -32,6 +32,8 @@ public interface MockFleetConfig { Optional proxyDeploymentName(); + ProxyConfig proxy(); + StorageConfig storage(); HazelcastConfig hazelcast(); @@ -48,6 +50,20 @@ interface S3Config { String path(); } + interface ProxyConfig { + RoutingConfig routing(); + } + + interface RoutingConfig { + RoutingMode mode(); + String host(); + } + + enum RoutingMode { + HOST, + PATH + } + interface HazelcastConfig { String clusterName(); Optional serviceDns(); diff --git a/fleet-api/src/main/java/com/github/letsrokk/WireMockConfigService.java b/fleet-api/src/main/java/com/github/letsrokk/WireMockConfigService.java index 2da4ce9..3426006 100644 --- a/fleet-api/src/main/java/com/github/letsrokk/WireMockConfigService.java +++ b/fleet-api/src/main/java/com/github/letsrokk/WireMockConfigService.java @@ -107,7 +107,13 @@ ConfigView view() { resourceVersion(userConfigMap), List.copyOf(mockIds.stream().sorted().toList()), mocks, - optionDefinitions()); + optionDefinitions(), + routingView()); + } + + private RoutingView routingView() { + MockFleetConfig.RoutingConfig routing = config.proxy().routing(); + return new RoutingView(routing.mode().name(), routing.host()); } ConfigView upsertMockConfig(String mockId, ConfigUpdateRequest request) { @@ -482,7 +488,7 @@ private OptionDefinition select(String name, String label, String group, String } public record ConfigView(String resourceVersion, List mockIds, List mocks, - List options) { + List options, RoutingView routing) { } public record MockConfigView(String mockId, boolean active, ConfigData baseline, ConfigData user, @@ -553,4 +559,7 @@ private static Map toStringMap(Map quantities) public record OptionDefinition(String name, String label, String kind, String group, String description, List values) { } + + public record RoutingView(String mode, String host) { + } } diff --git a/fleet-api/src/main/resources/application.yaml b/fleet-api/src/main/resources/application.yaml index eb5a7b3..5de4819 100644 --- a/fleet-api/src/main/resources/application.yaml +++ b/fleet-api/src/main/resources/application.yaml @@ -23,6 +23,10 @@ mock-fleet: wiremock-user-config-map-name: ${MOCK_FLEET_WIREMOCK_USER_CONFIG_MAP_NAME:} wiremock-config-key: ${MOCK_FLEET_WIREMOCK_CONFIG_KEY:wiremock-options.yaml} proxy-deployment-name: ${MOCK_FLEET_PROXY_DEPLOYMENT_NAME:} + proxy: + routing: + mode: ${MOCK_FLEET_PROXY_ROUTING_MODE:HOST} + host: ${MOCK_FLEET_PROXY_ROUTING_HOST:mock-fleet.localhost} hazelcast: cluster-name: ${MOCK_FLEET_HAZELCAST_CLUSTER_NAME:mock-fleet} service-dns: ${MOCK_FLEET_HAZELCAST_SERVICE_DNS:} diff --git a/fleet-api/src/test/java/com/github/letsrokk/WireMockConfigResourceTest.java b/fleet-api/src/test/java/com/github/letsrokk/WireMockConfigResourceTest.java index 3589475..6cd45f6 100644 --- a/fleet-api/src/test/java/com/github/letsrokk/WireMockConfigResourceTest.java +++ b/fleet-api/src/test/java/com/github/letsrokk/WireMockConfigResourceTest.java @@ -143,6 +143,11 @@ private WireMockConfigService.ConfigView configView() { "Logging", "Log more detail to the console.", List.of()); - return new WireMockConfigService.ConfigView("42", List.of("demo"), List.of(mock), List.of(option)); + return new WireMockConfigService.ConfigView( + "42", + List.of("demo"), + List.of(mock), + List.of(option), + new WireMockConfigService.RoutingView("HOST", "mock-fleet.localhost")); } } diff --git a/fleet-api/src/test/java/com/github/letsrokk/WireMockConfigServiceTest.java b/fleet-api/src/test/java/com/github/letsrokk/WireMockConfigServiceTest.java index 78404d5..422f398 100644 --- a/fleet-api/src/test/java/com/github/letsrokk/WireMockConfigServiceTest.java +++ b/fleet-api/src/test/java/com/github/letsrokk/WireMockConfigServiceTest.java @@ -214,6 +214,12 @@ private MockFleetConfig config() { when(config.namespace()).thenReturn("mock-fleet"); when(config.wiremockUserConfigMapName()).thenReturn(Optional.of("user-config")); when(config.wiremockConfigKey()).thenReturn("wiremock-options.yaml"); + MockFleetConfig.ProxyConfig proxy = mock(MockFleetConfig.ProxyConfig.class); + MockFleetConfig.RoutingConfig routing = mock(MockFleetConfig.RoutingConfig.class); + when(config.proxy()).thenReturn(proxy); + when(proxy.routing()).thenReturn(routing); + when(routing.mode()).thenReturn(MockFleetConfig.RoutingMode.HOST); + when(routing.host()).thenReturn("mock-fleet.localhost"); return config; } diff --git a/fleet-dash/src/App.tsx b/fleet-dash/src/App.tsx index 2667132..bd8a8d5 100644 --- a/fleet-dash/src/App.tsx +++ b/fleet-dash/src/App.tsx @@ -33,6 +33,12 @@ type ConfigView = { mockIds: string[]; mocks: MockConfigView[]; options: OptionDefinition[]; + routing: RoutingView; +}; + +type RoutingView = { + mode: "HOST" | "PATH"; + host: string; }; type ConfirmDialogState = { @@ -126,7 +132,7 @@ export default function App() { if (!response.ok) { throw new Error(`Unable to load config (${response.status})`); } - const data = (await response.json()) as ConfigView; + const data = normalizeConfigView((await response.json()) as ConfigView & { routing?: RoutingView }); const preserveDraft = configDirty && selectedMockId !== null; const nextData = preserveDraft && selectedMockId && !data.mockIds.includes(selectedMockId) ? withLocalMock(data, selectedMockId) @@ -843,7 +849,10 @@ export default function App() { className={selectedMockId === mockId ? "mock-button active" : "mock-button"} onClick={() => selectMock(mockId)} > - {mockId} + + {mockId} + {mockBaseUrl(mockId, configView.routing)} + {configView.mocks.find((mock) => mock.mockId === mockId)?.active ? active : null} ))} @@ -854,7 +863,12 @@ export default function App() { {selectedMock ? ( <>
- {selectedMock.mockId} + + {selectedMock.mockId} + + {mockBaseUrl(selectedMock.mockId, configView.routing)} + + {selectedMock.active ? "Active pod running" : "Future pods"}
@@ -1248,6 +1262,32 @@ function withLocalMock(configView: ConfigView, mockId: string): ConfigView { }; } +function normalizeConfigView(configView: ConfigView & { routing?: RoutingView }): ConfigView { + return { + ...configView, + routing: configView.routing ?? { mode: "HOST", host: window.location.hostname } + }; +} + +function mockBaseUrl(mockId: string, routing: RoutingView) { + const origin = window.location.origin; + if (routing.mode === "PATH") { + return `${origin}/${encodeURIComponent(mockId)}`; + } + + const protocol = window.location.protocol; + const host = hostWithBrowserPort(routing.host); + return `${protocol}//${mockId}.${host}`; +} + +function hostWithBrowserPort(configuredHost: string) { + const host = configuredHost.trim() || window.location.hostname; + if (host.includes(":") || !window.location.port) { + return host; + } + return `${host}:${window.location.port}`; +} + function optionGroupNames(options: OptionDefinition[]) { return [...groupOptions(options).map(([group]) => group), RESOURCE_GROUP_NAME]; } diff --git a/fleet-dash/src/styles.css b/fleet-dash/src/styles.css index 91bfd6e..c094a43 100644 --- a/fleet-dash/src/styles.css +++ b/fleet-dash/src/styles.css @@ -444,10 +444,10 @@ tr:last-child td { .mock-button { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; gap: 10px; - min-height: 38px; + min-height: 46px; padding: 8px 10px; border: 0; border-radius: 6px; @@ -456,6 +456,31 @@ tr:last-child td { cursor: pointer; } +.mock-button-text, +.mock-heading { + display: grid; + min-width: 0; + gap: 4px; +} + +.mock-heading a, +.mock-base-url { + color: #90a0bb; + font-size: 0.78rem; + line-height: 1.3; + overflow-wrap: anywhere; +} + +.mock-heading a { + text-decoration: none; +} + +.mock-heading a:hover, +.mock-heading a:focus-visible { + color: #f4b663; + text-decoration: underline; +} + .mock-button:hover, .mock-button.active { color: #f3efe6;