diff --git a/packages/common/index.ts b/packages/common/index.ts index 1be4f7fd..77c47662 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -25,6 +25,7 @@ export { tunnelNameResolver, TunnelNameResolver } from './src/tunnel-name.js' export { editUrl } from './src/url.js' export { ScriptInjection, + ContainerScriptInjection, parseScriptInjectionLabels, scriptInjectionsToLabels, COMPOSE_TUNNEL_AGENT_PORT, diff --git a/packages/common/src/compose-tunnel-agent/index.ts b/packages/common/src/compose-tunnel-agent/index.ts index 96f039b8..5024090c 100644 --- a/packages/common/src/compose-tunnel-agent/index.ts +++ b/packages/common/src/compose-tunnel-agent/index.ts @@ -1,5 +1,5 @@ export { COMPOSE_TUNNEL_AGENT_SERVICE_LABELS } from './labels.js' -export { ScriptInjection, parseScriptInjectionLabels, scriptInjectionsToLabels } from './script-injection.js' +export { ScriptInjection, ContainerScriptInjection, parseScriptInjectionLabels, scriptInjectionsToLabels } from './script-injection.js' export const COMPOSE_TUNNEL_AGENT_SERVICE_NAME = 'preevy_proxy' export const COMPOSE_TUNNEL_AGENT_PORT = 3000 diff --git a/packages/common/src/compose-tunnel-agent/script-injection.test.ts b/packages/common/src/compose-tunnel-agent/script-injection.test.ts index 258c7e60..5f61510c 100644 --- a/packages/common/src/compose-tunnel-agent/script-injection.test.ts +++ b/packages/common/src/compose-tunnel-agent/script-injection.test.ts @@ -9,6 +9,7 @@ describe('script injection labels', () => { 'preevy.inject_script.widget.defer': 'true', 'preevy.inject_script.widget.async': 'false', 'preevy.inject_script.widget.path_regex': 't.*t', + 'preevy.inject_script.widget.port': '3000', } const [scripts, errors] = parseScriptInjectionLabels(labels) @@ -19,6 +20,7 @@ describe('script injection labels', () => { defer: true, async: false, pathRegex: expect.any(RegExp), + port: 3000, }) expect(scripts[0].pathRegex?.source).toBe('t.*t') }) @@ -42,6 +44,17 @@ describe('script injection labels', () => { expect(errors).toHaveLength(1) }) + test('should drop scripts with an invalid number as port', () => { + const labels = { + 'preevy.inject_script.widget.src': 'https://my-script', + 'preevy.inject_script.widget.defer': 'true', + 'preevy.inject_script.widget.port': 'a', + } + const [scripts, errors] = parseScriptInjectionLabels(labels) + expect(scripts).toHaveLength(0) + expect(errors).toHaveLength(1) + }) + test('should support multiple scripts', () => { const labels = { 'preevy.inject_script.widget.src': 'https://my-script', diff --git a/packages/common/src/compose-tunnel-agent/script-injection.ts b/packages/common/src/compose-tunnel-agent/script-injection.ts index 7e83dec1..15ee6be8 100644 --- a/packages/common/src/compose-tunnel-agent/script-injection.ts +++ b/packages/common/src/compose-tunnel-agent/script-injection.ts @@ -9,11 +9,23 @@ export type ScriptInjection = { async?: boolean } -const parseBooleanLabelValue = (s:string) => s === 'true' || s === '1' +export type ContainerScriptInjection = ScriptInjection & { + port?: number +} + +const parseBooleanLabelValue = (s: string) => s === 'true' || s === '1' + +const parseNumber = (s: string): number => { + const result = Number(s) + if (Number.isNaN(result)) { + throw new Error(`invalid number "${s}"`) + } + return result +} -const parseScriptInjection = (o: Record): ScriptInjection | Error => { +const parseScriptInjection = (o: Record): ContainerScriptInjection | Error => { // eslint-disable-next-line camelcase - const { src, defer, async, path_regex } = o + const { src, defer, async, path_regex, port } = o try { if (!src) { throw new Error('missing src') @@ -23,6 +35,7 @@ const parseScriptInjection = (o: Record): ScriptInjection | Erro ...path_regex && { pathRegex: new RegExp(path_regex) }, ...defer && { defer: parseBooleanLabelValue(defer) }, ...async && { async: parseBooleanLabelValue(async) }, + ...port && { port: parseNumber(port) }, src, } } catch (e) { @@ -32,12 +45,13 @@ const parseScriptInjection = (o: Record): ScriptInjection | Erro const scriptInjectionToLabels = ( id: string, - { src, async, defer, pathRegex }: ScriptInjection, + { src, async, defer, pathRegex, port }: ContainerScriptInjection, ): Record => mapKeys>({ src, ...async && { async: 'true' }, ...defer && { defer: 'true' }, ...pathRegex && { path_regex: pathRegex.source }, + ...port && { port: port.toString() }, }, (_value, key) => [COMPOSE_TUNNEL_AGENT_SERVICE_LABELS.INJECT_SCRIPT_PREFIX, id, key].join('.')) export const scriptInjectionsToLabels = ( @@ -66,11 +80,11 @@ const parseLabelsWithPrefixAndId = ( export const parseScriptInjectionLabels = ( labels: Record, -): [ScriptInjection[], Error[]] => { +): [ContainerScriptInjection[], Error[]] => { const stringifiedInjections = parseLabelsWithPrefixAndId( labels, COMPOSE_TUNNEL_AGENT_SERVICE_LABELS.INJECT_SCRIPT_PREFIX, ) const injectionOrErrors = stringifiedInjections.map(parseScriptInjection) - return partition(injectionOrErrors, x => !(x instanceof Error)) as [ScriptInjection[], Error[]] + return partition(injectionOrErrors, x => !(x instanceof Error)) as [ContainerScriptInjection[], Error[]] } diff --git a/packages/compose-tunnel-agent/index.ts b/packages/compose-tunnel-agent/index.ts index 7e737c29..cf0b800f 100644 --- a/packages/compose-tunnel-agent/index.ts +++ b/packages/compose-tunnel-agent/index.ts @@ -20,6 +20,7 @@ import { runMachineStatusCommand } from './src/machine-status.js' import { envMetadata } from './src/metadata.js' import { readAllFiles } from './src/files.js' import { eventsClient as dockerEventsClient, filteredClient as dockerFilteredClient } from './src/docker/index.js' +import { anyComposeProjectFilters, composeProjectFilters } from './src/docker/filters.js' const PinoPretty = pinoPrettyModule.default @@ -81,21 +82,22 @@ const main = async () => { clientPublicKey: formatPublicKey(connectionConfig.clientPrivateKey), }) + const dockerFilters = targetComposeProject + ? composeProjectFilters({ composeProject: targetComposeProject }) + : anyComposeProjectFilters + const docker = new Docker({ socketPath: dockerSocket }) const dockerClient = dockerEventsClient({ log: log.child({ name: 'docker' }), docker, debounceWait: 500, defaultAccess, - composeProject: targetComposeProject, + filters: dockerFilters, + tunnelNameResolver: tunnelNameResolver({ envId: requiredEnv('PREEVY_ENV_ID') }), }) const sshLog = log.child({ name: 'ssh' }) - const sshClient = await createSshClient({ - connectionConfig, - tunnelNameResolver: tunnelNameResolver({ envId: requiredEnv('PREEVY_ENV_ID') }), - log: sshLog, - }) + const sshClient = await createSshClient({ connectionConfig, log: sshLog }) sshClient.ssh.on('close', () => { if (!endRequested) { @@ -106,11 +108,11 @@ const main = async () => { }) sshLog.info('ssh client connected to %j', sshUrl) - let currentTunnels = dockerClient.getRunningServices().then(services => sshClient.updateTunnels(services)) + let currentTunnels = dockerClient.getForwards().then(services => sshClient.updateTunnels(services)) void dockerClient.startListening({ - onChange: async services => { - currentTunnels = sshClient.updateTunnels(services) + onChange: async forwards => { + currentTunnels = sshClient.updateTunnels(forwards) }, }) diff --git a/packages/compose-tunnel-agent/package.json b/packages/compose-tunnel-agent/package.json index 6fabaf2d..8af61ff1 100644 --- a/packages/compose-tunnel-agent/package.json +++ b/packages/compose-tunnel-agent/package.json @@ -30,6 +30,7 @@ "pino-pretty": "^10.2.3", "rimraf": "^5.0.5", "ssh2": "^1.12.0", + "tseep": "^1.1.3", "ws": "^8.13.0", "zod": "^3.21.4" }, diff --git a/packages/compose-tunnel-agent/src/docker/events-client.ts b/packages/compose-tunnel-agent/src/docker/events-client.ts index e4e290b7..bfd944de 100644 --- a/packages/compose-tunnel-agent/src/docker/events-client.ts +++ b/packages/compose-tunnel-agent/src/docker/events-client.ts @@ -1,48 +1,42 @@ import Docker from 'dockerode' -import { tryParseJson, Logger, ScriptInjection } from '@preevy/common' +import { tryParseJson, Logger, TunnelNameResolver } from '@preevy/common' import { throttle } from 'lodash-es' import { inspect } from 'util' -import { filters } from './filters.js' -import { containerToService } from './services.js' - -export type RunningService = { - project: string - name: string - networks: string[] - ports: number[] - access: 'private' | 'public' - inject: ScriptInjection[] -} +import { DockerFilters } from './filters.js' +import { composeContainerToForwards } from './services.js' +import { Forward } from '../ssh/tunnel-client.js' export const eventsClient = ({ log, docker, debounceWait, - composeProject, + filters: { apiFilter }, + tunnelNameResolver, defaultAccess, }: { log: Logger docker: Pick debounceWait: number - composeProject?: string + filters: Pick + tunnelNameResolver: TunnelNameResolver defaultAccess: 'private' | 'public' }) => { - const { listContainers, apiFilter } = filters({ docker, composeProject }) - - const getRunningServices = async (): Promise => (await listContainers()).map(container => { - const { errors, ...service } = containerToService({ container, defaultAccess }) + const getForwards = async (): Promise => ( + await docker.listContainers({ all: true, filters: { ...apiFilter } }) + ).flatMap(container => { + const { errors, forwards } = composeContainerToForwards({ container, defaultAccess, tunnelNameResolver }) if (errors.length) { log.warn('error parsing docker container "%s" info, some information may be missing: %j', container.Names?.[0], inspect(errors)) } - return service + return forwards }) - const startListening = async ({ onChange }: { onChange: (services: RunningService[]) => void }) => { + const startListening = async ({ onChange }: { onChange: (forwards: Forward[]) => void }) => { const handler = throttle(async (data?: Buffer) => { log.debug('event handler: %j', data && tryParseJson(data.toString())) - const services = await getRunningServices() - onChange(services) + const forwards = await getForwards() + onChange(forwards) }, debounceWait, { leading: true, trailing: true }) const stream = await docker.getEvents({ @@ -59,7 +53,7 @@ export const eventsClient = ({ return { close: () => stream.removeAllListeners() } } - return { getRunningServices, startListening } + return { getForwards, startListening } } export type DockerEventsClient = ReturnType diff --git a/packages/compose-tunnel-agent/src/docker/filtered-client.ts b/packages/compose-tunnel-agent/src/docker/filtered-client.ts index 64c0de30..d58b8148 100644 --- a/packages/compose-tunnel-agent/src/docker/filtered-client.ts +++ b/packages/compose-tunnel-agent/src/docker/filtered-client.ts @@ -1,5 +1,5 @@ import Docker from 'dockerode' -import { filters } from './filters.js' +import { anyComposeProjectFilters, composeProjectFilters, filters } from './filters.js' export const filteredClient = ({ docker, @@ -8,14 +8,17 @@ export const filteredClient = ({ docker: Pick composeProject?: string }) => { - const { listContainers, adhocFilter } = filters({ docker, composeProject }) + const { apiFilter, adhocFilter } = composeProject + ? composeProjectFilters({ composeProject }) + : anyComposeProjectFilters - const inspectContainer = async (id: string) => { - const result = await docker.getContainer(id).inspect() - return result && adhocFilter(result.Config) ? result : undefined + return { + listContainers: docker.listContainers({ all: true, filters: { ...apiFilter } }), + inspectContainer: async (id: string) => { + const result = await docker.getContainer(id).inspect() + return result && adhocFilter(result.Config) ? result : undefined + }, } - - return { listContainers, inspectContainer } } export type DockerFilterClient = ReturnType diff --git a/packages/compose-tunnel-agent/src/docker/filters.ts b/packages/compose-tunnel-agent/src/docker/filters.ts index 8959818f..d87d9abb 100644 --- a/packages/compose-tunnel-agent/src/docker/filters.ts +++ b/packages/compose-tunnel-agent/src/docker/filters.ts @@ -1,4 +1,4 @@ -import Docker from 'dockerode' +import Docker, { GetEventsOptions } from 'dockerode' import { COMPOSE_TUNNEL_AGENT_SERVICE_LABELS } from '@preevy/common' import { COMPOSE_PROJECT_LABEL } from './labels.js' @@ -22,27 +22,29 @@ export const portFilter = ( return (p: Docker.Port) => Boolean(p.PublicPort) } -export const filters = ({ - docker, - composeProject, -}: { - docker: Pick - composeProject?: string -}) => { - const apiFilter = { - label: composeProject ? [`${COMPOSE_PROJECT_LABEL}=${composeProject}`] : [COMPOSE_PROJECT_LABEL], - } +export type DockerApiFilter = { + label?: string[] +} - const listContainers = async () => await docker.listContainers({ - all: true, - filters: { ...apiFilter }, - }) +export type DockerFilters = { + apiFilter: DockerApiFilter + adhocFilter: (c: Docker.ContainerInfo) => boolean +} - const adhocFilter = (c: Pick): boolean => ( - composeProject - ? c.Labels[COMPOSE_PROJECT_LABEL] === composeProject - : COMPOSE_PROJECT_LABEL in c.Labels - ) +export const composeProjectFilters = ({ + composeProject, +}: { + composeProject: string +}): DockerFilters => ({ + apiFilter: { + label: [`${COMPOSE_PROJECT_LABEL}=${composeProject}`], + }, + adhocFilter: (c: Pick): boolean => c.Labels[COMPOSE_PROJECT_LABEL] === composeProject, +}) - return { listContainers, adhocFilter, apiFilter } +export const anyComposeProjectFilters: DockerFilters = { + apiFilter: { + label: [COMPOSE_PROJECT_LABEL], + }, + adhocFilter: (c: Pick): boolean => COMPOSE_PROJECT_LABEL in c.Labels, } diff --git a/packages/compose-tunnel-agent/src/docker/services.ts b/packages/compose-tunnel-agent/src/docker/services.ts index 25189464..b7ed5c9a 100644 --- a/packages/compose-tunnel-agent/src/docker/services.ts +++ b/packages/compose-tunnel-agent/src/docker/services.ts @@ -1,34 +1,79 @@ -import { ScriptInjection, COMPOSE_TUNNEL_AGENT_SERVICE_LABELS as PREEVY_LABELS, parseScriptInjectionLabels } from '@preevy/common' +import { ScriptInjection, COMPOSE_TUNNEL_AGENT_SERVICE_LABELS as PREEVY_LABELS, parseScriptInjectionLabels, TunnelNameResolver } from '@preevy/common' import Docker from 'dockerode' import { portFilter } from './filters.js' import { COMPOSE_PROJECT_LABEL, COMPOSE_SERVICE_LABEL } from './labels.js' - -export type RunningService = { - project: string - name: string - networks: string[] - ports: number[] - access: 'private' | 'public' - inject: ScriptInjection[] -} +import { Forward } from '../ssh/tunnel-client.js' const GLOBAL_INJECT_SCRIPTS = process.env.GLOBAL_INJECT_SCRIPTS ? JSON.parse(process.env.GLOBAL_INJECT_SCRIPTS) as ScriptInjection[] : [] -export const containerToService = ({ +type ContainerInfo = Pick + +const containerPorts = ( + container: ContainerInfo, +) => [ + // ports may have both IPv6 and IPv4 addresses, ignoring + ...new Set(container.Ports.filter(p => p.Type === 'tcp').filter(portFilter(container)).map(p => p.PrivatePort)), +] + +type ResolvedPort = Omit, 'injects' | 'access'> + +export type ComposeServiceMeta = { + service: string + project: string + port: number +} + +export const containerToForwards = ({ + resolvedPorts, container, defaultAccess, -}: { container: Docker.ContainerInfo; defaultAccess: 'private' | 'public' }): RunningService & { errors: Error[] } => { +}: { + resolvedPorts: ResolvedPort[] + container: ContainerInfo + defaultAccess: 'private' | 'public' +}): { errors: Error[]; forwards: Forward[] } => { const [inject, errors] = parseScriptInjectionLabels(container.Labels) - return ({ - project: container.Labels[COMPOSE_PROJECT_LABEL], - name: container.Labels[COMPOSE_SERVICE_LABEL], - access: (container.Labels[PREEVY_LABELS.ACCESS] || defaultAccess) as ('private' | 'public'), - networks: Object.keys(container.NetworkSettings.Networks), - // ports may have both IPv6 and IPv4 addresses, ignoring - ports: [...new Set(container.Ports.filter(p => p.Type === 'tcp').filter(portFilter(container)).map(p => p.PrivatePort))], - inject: [...inject, ...GLOBAL_INJECT_SCRIPTS], - errors, + const access = container.Labels[PREEVY_LABELS.ACCESS] as undefined | 'private' | 'public' + + const forwards = resolvedPorts.map(x => { + const { meta, externalName, host, port } = x + return { + meta, + host, + port, + externalName, + access: access ?? defaultAccess, + injects: [...inject.filter(i => i.port === undefined || i.port === port), ...GLOBAL_INJECT_SCRIPTS], + } + }) + return { errors, forwards } +} + +export const composeContainerToForwards = ({ + tunnelNameResolver, + defaultAccess, + container, +}: { + tunnelNameResolver: TunnelNameResolver + defaultAccess: 'private' | 'public' + container: ContainerInfo +}): { errors: Error[]; forwards: Forward[] } => { + const project = container.Labels[COMPOSE_PROJECT_LABEL] + const service = container.Labels[COMPOSE_SERVICE_LABEL] + const ports = containerPorts(container) + const portExternalNames = new Map( + tunnelNameResolver({ name: service, ports }).map(({ port, tunnel }) => [port, tunnel]), + ) + return containerToForwards({ + resolvedPorts: ports.map(port => ({ + meta: { project, service, port }, + externalName: portExternalNames.get(port) as string, + host: service, + port, + })), + container, + defaultAccess, }) } diff --git a/packages/compose-tunnel-agent/src/service-discovery.ts b/packages/compose-tunnel-agent/src/service-discovery.ts new file mode 100644 index 00000000..aa091e1d --- /dev/null +++ b/packages/compose-tunnel-agent/src/service-discovery.ts @@ -0,0 +1,14 @@ +import { IEventEmitter } from 'tseep' +import { ScriptInjection } from '@preevy/common' + +export type RunningService = { + project: string + name: string + ports: number[] + access: 'private' | 'public' + inject: ScriptInjection[] +} + +export type ServiceDiscovery = IEventEmitter<{ + servicesUpdated: (services: RunningService[]) => void +}> diff --git a/packages/compose-tunnel-agent/src/ssh/tunnel-client.ts b/packages/compose-tunnel-agent/src/ssh/tunnel-client.ts index 1bdee76c..f3dcef15 100644 --- a/packages/compose-tunnel-agent/src/ssh/tunnel-client.ts +++ b/packages/compose-tunnel-agent/src/ssh/tunnel-client.ts @@ -1,18 +1,25 @@ -import { baseSshClient, HelloResponse, ScriptInjection, SshClientOpts, TunnelNameResolver } from '@preevy/common' +import { baseSshClient, HelloResponse, ScriptInjection, SshClientOpts } from '@preevy/common' import net from 'net' import plimit from 'p-limit' import { inspect } from 'util' -import { RunningService } from '../docker/index.js' import { difference } from '../maps.js' +import { RunningService } from '../service-discovery.js' +import { ComposeServiceMeta } from '../docker/services.js' -type Forward = { - service: RunningService +export type Forward = { host: string port: number + externalName: string + meta: Meta + access: 'private' | 'public' + injects: ScriptInjection[] +} + +type InternalForward = Forward & { sockets: Set } -export type Tunnel = { +type SshStateTunnel = { project: string service: string ports: Record @@ -20,7 +27,8 @@ export type Tunnel = { export type SshState = { clientId: string - tunnels: Tunnel[] + tunnels: SshStateTunnel[] + forwards: { forward: Forward; url: string }[] } const stringifiableInject = (inject: ScriptInjection) => ({ @@ -33,10 +41,7 @@ const encodedJson = (o: unknown) => Buffer.from(JSON.stringify(o)).toString('bas export const sshClient = async ({ log, connectionConfig, - tunnelNameResolver, -}: Pick & { - tunnelNameResolver: TunnelNameResolver -}) => { +}: Pick) => { const { ssh, execHello, end } = await baseSshClient({ log, connectionConfig, @@ -47,7 +52,7 @@ export const sshClient = async ({ // baseSshClient calls end }) - const currentForwards = new Map() + const currentForwards = new Map() ssh.on('unix connection', ({ socketPath: forwardRequestId }, accept, reject) => { const forward = currentForwards.get(forwardRequestId) @@ -78,19 +83,17 @@ export const sshClient = async ({ }) const createForward = ( - service: RunningService, forwardRequest: string, - host: string, - port: number, + forward: Forward, ) => new Promise((resolve, reject) => { - log.debug('createForward: %j', { service, forwardRequestId: forwardRequest, host, port }) + log.debug('createForward: %j', { forwardRequest, forward }) ssh.openssh_forwardInStreamLocal(forwardRequest, err => { if (err) { log.error('error creating forward %s: %j', forwardRequest, inspect(err)) reject(err) } log.debug('created forward %j', forwardRequest) - currentForwards.set(forwardRequest, { service, host, port, sockets: new Set() }) + currentForwards.set(forwardRequest, { ...forward, sockets: new Set() }) resolve() }) }) @@ -115,8 +118,8 @@ export const sshClient = async ({ }) }) - const tunnelsFromHelloResponse = (helloTunnels: HelloResponse['tunnels']): Tunnel[] => { - const serviceKey = ({ name, project }: RunningService) => `${name}/${project}` + const tunnelsFromHelloResponse = (helloTunnels: HelloResponse['tunnels']): SshStateTunnel[] => { + const serviceKey = ({ name, project }: Pick) => `${name}/${project}` const r = Object.entries(helloTunnels) .reduce( @@ -125,53 +128,51 @@ export const sshClient = async ({ if (!forward) { throw new Error(`no such forward: ${forwardRequestId}`) } - const { service, port } = forward; - ((res[serviceKey(service)] ||= { - service: service.name, - project: service.project, + const { meta, port } = forward + const { service, project } = meta as ComposeServiceMeta + ((res[serviceKey({ name: service, project })] ||= { + service, + project, ports: {}, }).ports[port] = url) return res }, - {} as Record, + {} as Record, ) return Object.values(r) } - const stateFromHelloResponse = ({ clientId, tunnels }: HelloResponse): SshState => ({ - clientId, tunnels: tunnelsFromHelloResponse(tunnels), + const stateFromHelloResponse = ( + { clientId, tunnels }: Pick, + ): SshState => ({ + clientId, + tunnels: tunnelsFromHelloResponse(tunnels), + forwards: Object.entries(tunnels).map(([forwardRequestId, url]) => ({ + forward: currentForwards.get(forwardRequestId) as Forward, + url, + })), }) - const stringifyForwardRequests = (service: RunningService) => { - const tunnels = tunnelNameResolver({ ...service }) - return tunnels.map(({ port, tunnel }) => { - const args: Record = { - ...(service.access === 'private' ? { access: 'private' } : {}), - meta: encodedJson({ - service: service.name, - project: service.project, - port, - }), - ...(service.inject?.length - ? { inject: encodedJson(service.inject.map(stringifiableInject)) } - : {} - ), - } - const argsStr = Object.entries(args).map(([k, v]) => `${k}=${v}`).join(';') - return ({ port, requestId: `/${tunnel}#${argsStr}` }) - }) + const stringifyForwardRequest = ( + { access, meta, injects, externalName }: Pick, + ) => { + const args: Record = { + ...(access === 'private' ? { access: 'private' } : {}), + meta: encodedJson(meta), + ...injects?.length ? { inject: encodedJson(injects.map(stringifiableInject)) } : {}, + } + const argsStr = Object.entries(args).map(([k, v]) => `${k}=${v}`).join(';') + return `/${externalName}#${argsStr}` } let state: SshState const limit = plimit(1) return { - updateTunnels: async (services: RunningService[]): Promise => await limit(async () => { - const newForwardRequests = new Map( - services.flatMap( - service => stringifyForwardRequests(service).map(({ port, requestId }) => [requestId, { service, port }]) - ) + updateTunnels: async (forwards: Forward[]): Promise => await limit(async () => { + const newForwardRequests = new Map( + forwards.map(f => [stringifyForwardRequest(f), f]) ) const inserts = [...difference(newForwardRequests, currentForwards)] @@ -185,8 +186,8 @@ export const sshClient = async ({ await Promise.all( inserts.map(forwardRequest => { - const { service, port } = newForwardRequests.get(forwardRequest) as { service: RunningService; port: number } - return createForward(service, forwardRequest, service.name, port) + const forward = newForwardRequests.get(forwardRequest) as Forward + return createForward(forwardRequest, forward) }), ) diff --git a/packages/core/src/compose/script-injection.ts b/packages/core/src/compose/script-injection.ts index 874d1738..c67480c4 100644 --- a/packages/core/src/compose/script-injection.ts +++ b/packages/core/src/compose/script-injection.ts @@ -1,10 +1,10 @@ -import { ScriptInjection, scriptInjectionsToLabels } from '@preevy/common' +import { ContainerScriptInjection, scriptInjectionsToLabels } from '@preevy/common' import { mapValues } from 'lodash-es' import { ComposeModel, ComposeService } from './model.js' const addScriptInjectionsToService = ( service: ComposeService, - injections: Record, + injections: Record, ): ComposeService => ({ ...service, labels: { @@ -15,5 +15,5 @@ const addScriptInjectionsToService = ( export const addScriptInjectionsToServices = ( services: ComposeModel['services'], - factory: (serviceName: string, serviceDef: ComposeService) => Record | undefined, + factory: (serviceName: string, serviceDef: ComposeService) => Record | undefined, ): ComposeModel['services'] => mapValues(services, (def, name) => addScriptInjectionsToService(def, factory(name, def) ?? {})) diff --git a/tunnel-server/package.json b/tunnel-server/package.json index c4ea62fa..022856cf 100644 --- a/tunnel-server/package.json +++ b/tunnel-server/package.json @@ -23,7 +23,7 @@ "prom-client": "^14.2.0", "ssh2": "^1.12.0", "ts-pattern": "^5.0.5", - "tseep": "^1.1.1", + "tseep": "^1.1.3", "zod": "^3.22.4" }, "engines": { diff --git a/yarn.lock b/yarn.lock index fdedcd40..b4f13eb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13397,6 +13397,11 @@ tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0: minimist "^1.2.6" strip-bom "^3.0.0" +tseep@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/tseep/-/tseep-1.1.3.tgz#7cf81b83680403af9d10829be6a04bd134bee8c5" + integrity sha512-deBIcIlXUMlr3xaN0UEochqjU/zXGaZGPqHPd1rxo4w6DklBdRM6WQQtsk7bekIF+qY6QTeen3nE6OA7BxL9rg== + tslib@^1.11.1, tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"