diff --git a/packages/cli/src/commands/up.ts b/packages/cli/src/commands/up.ts index 14706994..11aaecf7 100644 --- a/packages/cli/src/commands/up.ts +++ b/packages/cli/src/commands/up.ts @@ -1,4 +1,6 @@ import { Args, Flags } from '@oclif/core' +import { buildFlags, parseBuildFlags, tableFlags, text, tunnelServerFlags } from '@preevy/cli-common' +import { editUrl, tunnelNameResolver } from '@preevy/common' import { ComposeModel, Logger, @@ -11,13 +13,11 @@ import { telemetryEmitter, withSpinner, } from '@preevy/core' -import { buildFlags, parseBuildFlags, tableFlags, text, tunnelServerFlags } from '@preevy/cli-common' import { inspect } from 'util' -import { editUrl, tunnelNameResolver } from '@preevy/common' -import MachineCreationDriverCommand from '../machine-creation-driver-command.js' import { envIdFlags, urlFlags } from '../common-flags.js' -import { filterUrls, printUrls, writeUrlsToFile } from './urls.js' +import MachineCreationDriverCommand from '../machine-creation-driver-command.js' import { connectToTunnelServerSsh } from '../tunnel-server-client.js' +import { filterUrls, printUrls, writeUrlsToFile } from './urls.js' const fetchTunnelServerDetails = async ({ log, @@ -179,7 +179,7 @@ export default class Up extends MachineCreationDriverCommand { const buildSpec = parseBuildFlags(flags) - await commands.up({ + const { composeModel } = await commands.up({ connection, machineStatusCommand, dockerPlatform, @@ -205,6 +205,7 @@ export default class Up extends MachineCreationDriverCommand { this.log(`Preview environment ${text.code(envId)} provisioned at: ${text.code(machine.locationDescription)}`) + const expectedServiceNames = Object.keys(composeModel.services ?? {}) const composeTunnelServiceUrl = findComposeTunnelAgentUrl(expectedServiceUrls) const flatTunnels = await withSpinner(() => commands.urls({ composeTunnelServiceUrl, diff --git a/packages/core/src/commands/urls.ts b/packages/core/src/commands/urls.ts index a7dff859..d5366005 100644 --- a/packages/core/src/commands/urls.ts +++ b/packages/core/src/commands/urls.ts @@ -1,7 +1,7 @@ -import retry from 'p-retry' import { COMPOSE_TUNNEL_AGENT_SERVICE_NAME } from '@preevy/common' -import { generateBasicAuthCredentials, jwtGenerator } from '../credentials/index.js' +import retry from 'p-retry' import { queryTunnels } from '../compose-tunnel-agent-client.js' +import { generateBasicAuthCredentials, jwtGenerator } from '../credentials/index.js' import { FlatTunnel, flattenTunnels } from '../tunneling/index.js' const tunnelFilter = ({ serviceAndPort, showPreevyService }: { @@ -26,6 +26,7 @@ export const urls = async ({ showPreevyService, composeTunnelServiceUrl, fetchTimeout, + expectedServiceNames, }: { serviceAndPort?: { service: string; port?: number } tunnelingKey: string | Buffer @@ -34,6 +35,7 @@ export const urls = async ({ showPreevyService: boolean composeTunnelServiceUrl: string fetchTimeout: number + expectedServiceNames?: string[] }) => { const credentials = await generateBasicAuthCredentials(jwtGenerator(tunnelingKey)) @@ -43,6 +45,7 @@ export const urls = async ({ credentials, includeAccessCredentials, fetchTimeout, + expectedServiceNames, }) return flattenTunnels(tunnels).filter(tunnelFilter({ serviceAndPort, showPreevyService })) diff --git a/packages/core/src/compose-tunnel-agent-client.ts b/packages/core/src/compose-tunnel-agent-client.ts index a39e3fd2..6dd61877 100644 --- a/packages/core/src/compose-tunnel-agent-client.ts +++ b/packages/core/src/compose-tunnel-agent-client.ts @@ -1,17 +1,17 @@ -import path from 'path' +import { COMPOSE_TUNNEL_AGENT_PORT, COMPOSE_TUNNEL_AGENT_SERVICE_LABELS, COMPOSE_TUNNEL_AGENT_SERVICE_NAME, MachineStatusCommand, ScriptInjection, dateReplacer } from '@preevy/common' +import { mapValues, merge } from 'lodash-es' +import { createRequire } from 'module' import retry from 'p-retry' +import path from 'path' import util from 'util' -import { createRequire } from 'module' -import { mapValues, merge } from 'lodash-es' -import { COMPOSE_TUNNEL_AGENT_PORT, COMPOSE_TUNNEL_AGENT_SERVICE_LABELS, COMPOSE_TUNNEL_AGENT_SERVICE_NAME, MachineStatusCommand, ScriptInjection, dateReplacer } from '@preevy/common' import { ComposeModel, ComposeService, composeModelFilename } from './compose/model.js' -import { TunnelOpts } from './ssh/url.js' -import { Tunnel } from './tunneling/index.js' +import { addScriptInjectionsToServices } from './compose/script-injection.js' import { withBasicAuthCredentials } from './credentials/index.js' +import { EnvId } from './env-id.js' import { EnvMetadata, driverMetadataFilename } from './env-metadata.js' import { REMOTE_DIR_BASE } from './remote-files.js' -import { EnvId } from './env-id.js' -import { addScriptInjectionsToServices } from './compose/script-injection.js' +import { TunnelOpts } from './ssh/url.js' +import { Tunnel } from './tunneling/index.js' const require = createRequire(import.meta.url) const COMPOSE_TUNNEL_AGENT_DIR = path.join(path.dirname(require.resolve('@preevy/compose-tunnel-agent')), '..') @@ -156,18 +156,31 @@ export const findComposeTunnelAgentUrl = ( return serviceUrl } +const ensureExpectedServices = ( + { tunnels }: { tunnels: Tunnel[] }, + expectedServiceNames: string[] +) => { + const actualServiceNames = new Set(tunnels.map(tunnel => tunnel.service)) + const missingServiceNames = expectedServiceNames.filter(name => !actualServiceNames.has(name)) + if (missingServiceNames.length) { + throw new Error(`Expected service names ${missingServiceNames.join(', ')} not found in tunnels: ${util.inspect(tunnels)}`) + } +} + export const queryTunnels = async ({ retryOpts = { retries: 0 }, composeTunnelServiceUrl, credentials, includeAccessCredentials, fetchTimeout, + expectedServiceNames, }: { composeTunnelServiceUrl: string credentials: { user: string; password: string } retryOpts?: retry.Options includeAccessCredentials: false | 'browser' | 'api' fetchTimeout: number + expectedServiceNames?: string[] }) => { const { tunnels } = await retry(async () => { const r = await fetch( @@ -177,7 +190,11 @@ export const queryTunnels = async ({ if (!r.ok) { throw new Error(`Failed to connect to docker proxy at ${composeTunnelServiceUrl}: ${r.status}: ${r.statusText}`) } - return await (r.json() as Promise<{ tunnels: Tunnel[] }>) + const tunnelsObj = await (r.json() as Promise<{ tunnels: Tunnel[] }>) + if (expectedServiceNames) { + ensureExpectedServices(tunnelsObj, expectedServiceNames) + } + return tunnelsObj }, retryOpts) return tunnels