From 075cc53300a6250656445a263d9c0269e36b364d Mon Sep 17 00:00:00 2001 From: Roy Razon Date: Thu, 9 Nov 2023 14:31:14 +0200 Subject: [PATCH] refactor --- packages/cli/src/commands/up.ts | 4 +- packages/common/index.ts | 10 +- packages/common/package.json | 3 +- .../common/src/compose-tunnel-agent.test.ts | 74 ------------- packages/common/src/compose-tunnel-agent.ts | 51 --------- .../common/src/compose-tunnel-agent/index.ts | 5 + .../common/src/compose-tunnel-agent/labels.ts | 8 ++ .../script-injection.test.ts | 100 ++++++++++++++++++ .../compose-tunnel-agent/script-injection.ts | 76 +++++++++++++ packages/common/src/compose-utils.ts | 23 ---- .../src/docker/events-client.ts | 10 +- .../src/docker/services.ts | 27 +++-- packages/core/src/commands/up/index.ts | 54 +++++----- .../core/src/compose-tunnel-agent-client.ts | 1 - packages/core/src/compose/index.ts | 1 + .../core/src/compose/script-injection.test.ts | 81 ++++++++------ packages/core/src/compose/script-injection.ts | 54 ++++------ packages/core/src/index.ts | 2 +- 18 files changed, 320 insertions(+), 264 deletions(-) delete mode 100644 packages/common/src/compose-tunnel-agent.test.ts delete mode 100644 packages/common/src/compose-tunnel-agent.ts create mode 100644 packages/common/src/compose-tunnel-agent/index.ts create mode 100644 packages/common/src/compose-tunnel-agent/labels.ts create mode 100644 packages/common/src/compose-tunnel-agent/script-injection.test.ts create mode 100644 packages/common/src/compose-tunnel-agent/script-injection.ts delete mode 100644 packages/common/src/compose-utils.ts diff --git a/packages/cli/src/commands/up.ts b/packages/cli/src/commands/up.ts index 268a3fed..b24fbe3b 100644 --- a/packages/cli/src/commands/up.ts +++ b/packages/cli/src/commands/up.ts @@ -120,7 +120,7 @@ export default class Up extends MachineCreationDriverCommand { this.logger.debug('expectedServiceUrls: %j', expectedServiceUrls) - const injectLivecycleScript = flags['enable-widget'] + const injectWidgetScript = flags['enable-widget'] ? editUrl(flags['livecycle-widget-url'], { queryParams: { profile: thumbprint, env: envId } }).toString() : undefined @@ -135,7 +135,7 @@ export default class Up extends MachineCreationDriverCommand { userSpecifiedProjectName: flags.project, composeFiles: this.config.composeFiles, envId, - injectLivecycleScript, + scriptInjections: injectWidgetScript ? { 'livecycle-widget': { src: injectWidgetScript } } : undefined, tunnelOpts, log: this.logger, dataDir: this.config.dataDir, diff --git a/packages/common/index.ts b/packages/common/index.ts index d7c9cdc0..8597902d 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -23,7 +23,13 @@ export { Logger } from './src/log' export { requiredEnv, numberFromEnv } from './src/env' export { tunnelNameResolver, TunnelNameResolver } from './src/tunnel-name' export { editUrl } from './src/url' -export * from './src/compose-tunnel-agent' -export * from './src/compose-utils' +export { + ScriptInjection, + parseScriptInjectionLabels, + scriptInjectionsToLabels, + COMPOSE_TUNNEL_AGENT_PORT, + COMPOSE_TUNNEL_AGENT_SERVICE_LABELS, + COMPOSE_TUNNEL_AGENT_SERVICE_NAME, +} from './src/compose-tunnel-agent' export { MachineStatusCommand, DockerMachineStatusCommandRecipe } from './src/machine-status-command' export { ProcessOutputBuffers, orderedOutput, OrderedOutput } from './src/process-output-buffers' diff --git a/packages/common/package.json b/packages/common/package.json index 3fc1ab10..2f3f4a77 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -38,10 +38,9 @@ }, "scripts": { "test": "yarn jest", - "start": "node dist/main.js", - "dev": "tsx watch ./main.ts", "lint": "eslint . --ext .ts,.tsx --cache", "build": "tsc -b", + "clean": "shx rm -rf dist tsconfig.tsbuildinfo", "prepack": "yarn build", "bump-to": "yarn version --no-commit-hooks --no-git-tag-version --new-version" } diff --git a/packages/common/src/compose-tunnel-agent.test.ts b/packages/common/src/compose-tunnel-agent.test.ts deleted file mode 100644 index 907511b2..00000000 --- a/packages/common/src/compose-tunnel-agent.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { describe, test, expect } from '@jest/globals' -import { scriptInjectionFromLabels } from './compose-tunnel-agent' - -describe('parse script injection labels', () => { - test('should parse correctly', () => { - const labels = { - 'preevy.inject_script.widget.src': 'https://my-script', - 'preevy.inject_script.widget.defer': 'true', - 'preevy.inject_script.widget.async': 'false', - 'preevy.inject_script.widget.path_regex': 't.*t', - } - const scriptInjections = scriptInjectionFromLabels(labels) - expect(scriptInjections).toHaveLength(1) - const [script] = scriptInjections - expect(script).toMatchObject({ - src: 'https://my-script', - defer: true, - async: false, - pathRegex: expect.any(RegExp), - }) - }) - test('should revive regex correctly', () => { - const labels = { - 'preevy.inject_script.widget.src': 'https://my-script', - 'preevy.inject_script.widget.path_regex': 't.*t', - } - const [script] = scriptInjectionFromLabels(labels) - expect('test').toMatch(script.pathRegex!) - expect('best').not.toMatch(script.pathRegex!) - }) - - test('should ignore scripts with invalid regex', () => { - const labels = { - 'preevy.inject_script.widget.src': 'https://my-script', - 'preevy.inject_script.widget.path_regex': '[', - } - expect(scriptInjectionFromLabels(labels)).toHaveLength(0) - }) - - test('should drop scripts without src', () => { - const labels = { - 'preevy.inject_script.widget.defer': 'true', - } - expect(scriptInjectionFromLabels(labels)).toHaveLength(0) - }) - - test('should support multiple scripts', () => { - const labels = { - 'preevy.inject_script.widget.src': 'https://my-script', - 'preevy.inject_script.widget.defer': '1', - 'preevy.inject_script.widget2.src': 'https://my-script2', - 'preevy.inject_script.widget2.defer': 'false', - 'preevy.inject_script.widget3.src': 'https://my-script3', - 'preevy.inject_script.widget3.defer': '0', - } - const scripts = scriptInjectionFromLabels(labels) - expect(scripts).toHaveLength(3) - expect(scripts).toMatchObject([ - { - src: 'https://my-script', - defer: true, - }, - { - src: 'https://my-script2', - defer: false, - }, - { - src: 'https://my-script3', - defer: false, - }, - ]) - }) -}) diff --git a/packages/common/src/compose-tunnel-agent.ts b/packages/common/src/compose-tunnel-agent.ts deleted file mode 100644 index e2306089..00000000 --- a/packages/common/src/compose-tunnel-agent.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { extractSectionsFromLabels, parseBooleanLabelValue } from './compose-utils' - -export const COMPOSE_TUNNEL_AGENT_SERVICE_LABELS = { - PROFILE_THUMBPRINT: 'preevy.profile_thumbprint', - PRIVATE_MODE: 'preevy.private_mode', - ENV_ID: 'preevy.env_id', - ACCESS: 'preevy.access', - EXPOSE: 'preevy.expose', - INJECT_SCRIPT_PREFIX: 'preevy.inject_script', -} - -export const COMPOSE_TUNNEL_AGENT_SERVICE_NAME = 'preevy_proxy' -export const COMPOSE_TUNNEL_AGENT_PORT = 3000 - -export type ScriptInjection = { - pathRegex?: RegExp - src: string - defer?: boolean - async?: boolean -} - -type Stringified = { - [k in keyof T]: string -} - -const parseScriptInjection = ({ pathRegex, defer, async, src }: Stringified): - ScriptInjection | Error => { - try { - if (!src) { - throw new Error('missing src') - } - return { - ...pathRegex && { pathRegex: new RegExp(pathRegex) }, - ...defer && { defer: parseBooleanLabelValue(defer) }, - ...async && { async: parseBooleanLabelValue(async) }, - src, - } - } catch (e) { - return e as Error - } -} - -export const scriptInjectionFromLabels = (labels : Record): ScriptInjection[] => { - const scripts = extractSectionsFromLabels>( - COMPOSE_TUNNEL_AGENT_SERVICE_LABELS.INJECT_SCRIPT_PREFIX, - labels - ) - return Object.values(scripts) - .map(parseScriptInjection) - .filter((x): x is ScriptInjection => !(x instanceof Error)) -} diff --git a/packages/common/src/compose-tunnel-agent/index.ts b/packages/common/src/compose-tunnel-agent/index.ts new file mode 100644 index 00000000..39e0d91a --- /dev/null +++ b/packages/common/src/compose-tunnel-agent/index.ts @@ -0,0 +1,5 @@ +export { COMPOSE_TUNNEL_AGENT_SERVICE_LABELS } from './labels' +export { ScriptInjection, parseScriptInjectionLabels, scriptInjectionsToLabels } from './script-injection' + +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/labels.ts b/packages/common/src/compose-tunnel-agent/labels.ts new file mode 100644 index 00000000..d7cbe96d --- /dev/null +++ b/packages/common/src/compose-tunnel-agent/labels.ts @@ -0,0 +1,8 @@ +export const COMPOSE_TUNNEL_AGENT_SERVICE_LABELS = { + PROFILE_THUMBPRINT: 'preevy.profile_thumbprint', + PRIVATE_MODE: 'preevy.private_mode', + ENV_ID: 'preevy.env_id', + ACCESS: 'preevy.access', + EXPOSE: 'preevy.expose', + INJECT_SCRIPT_PREFIX: 'preevy.inject_script', +} as const diff --git a/packages/common/src/compose-tunnel-agent/script-injection.test.ts b/packages/common/src/compose-tunnel-agent/script-injection.test.ts new file mode 100644 index 00000000..6386057b --- /dev/null +++ b/packages/common/src/compose-tunnel-agent/script-injection.test.ts @@ -0,0 +1,100 @@ +import { describe, test, expect, it } from '@jest/globals' +import { ScriptInjection, parseScriptInjectionLabels, scriptInjectionsToLabels } from './script-injection' + +describe('script injection labels', () => { + describe('parseScriptInjectionLabels', () => { + test('should parse correctly a single label group', () => { + const labels = { + 'preevy.inject_script.widget.src': 'https://my-script', + 'preevy.inject_script.widget.defer': 'true', + 'preevy.inject_script.widget.async': 'false', + 'preevy.inject_script.widget.path_regex': 't.*t', + } + + const [scripts, errors] = parseScriptInjectionLabels(labels) + expect(errors).toHaveLength(0) + expect(scripts).toHaveLength(1) + expect(scripts[0]).toMatchObject({ + src: 'https://my-script', + defer: true, + async: false, + pathRegex: expect.any(RegExp), + }) + expect(scripts[0].pathRegex?.source).toBe('t.*t') + }) + + test('should ignore scripts with invalid regex', () => { + const labels = { + 'preevy.inject_script.widget.src': 'https://my-script', + 'preevy.inject_script.widget.path_regex': '[', + } + const [scripts, errors] = parseScriptInjectionLabels(labels) + expect(scripts).toHaveLength(0) + expect(errors).toHaveLength(1) + }) + + test('should drop scripts without src', () => { + const labels = { + 'preevy.inject_script.widget.defer': 'true', + } + 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', + 'preevy.inject_script.widget.defer': '1', + 'preevy.inject_script.widget2.src': 'https://my-script2', + 'preevy.inject_script.widget2.defer': 'false', + 'preevy.inject_script.widget3.src': 'https://my-script3', + 'preevy.inject_script.widget3.defer': '0', + } + const [scripts, errors] = parseScriptInjectionLabels(labels) + expect(errors).toHaveLength(0) + expect(scripts).toHaveLength(3) + expect(scripts).toContainEqual( + { + src: 'https://my-script', + defer: true, + }, + ) + expect(scripts).toContainEqual( + { + src: 'https://my-script2', + defer: false, + }, + ) + expect(scripts).toContainEqual( + { + src: 'https://my-script3', + defer: false, + }, + ) + }) + }) + + describe('scriptInjectionsToLabels', () => { + const injections: Record = { + script1: { + src: 'https://my-script', + defer: true, + async: false, + pathRegex: /^aaa/, + }, + script2: { + src: 'https://my-script2', + }, + } + + it('should convert to labels', () => { + expect(scriptInjectionsToLabels(injections)).toMatchObject({ + 'preevy.inject_script.script1.src': 'https://my-script', + 'preevy.inject_script.script1.defer': 'true', + 'preevy.inject_script.script1.path_regex': '^aaa', + 'preevy.inject_script.script2.src': 'https://my-script2', + }) + }) + }) +}) diff --git a/packages/common/src/compose-tunnel-agent/script-injection.ts b/packages/common/src/compose-tunnel-agent/script-injection.ts new file mode 100644 index 00000000..33fd6258 --- /dev/null +++ b/packages/common/src/compose-tunnel-agent/script-injection.ts @@ -0,0 +1,76 @@ +import { groupBy, mapKeys, partition } from 'lodash' +import { inspect } from 'util' +import { COMPOSE_TUNNEL_AGENT_SERVICE_LABELS } from './labels' + +export type ScriptInjection = { + pathRegex?: RegExp + src: string + defer?: boolean + async?: boolean +} + +const parseBooleanLabelValue = (s:string) => s === 'true' || s === '1' + +const parseScriptInjection = (o: Record): ScriptInjection | Error => { + // eslint-disable-next-line camelcase + const { src, defer, async, path_regex } = o + try { + if (!src) { + throw new Error('missing src') + } + return { + // eslint-disable-next-line camelcase + ...path_regex && { pathRegex: new RegExp(path_regex) }, + ...defer && { defer: parseBooleanLabelValue(defer) }, + ...async && { async: parseBooleanLabelValue(async) }, + src, + } + } catch (e) { + return new Error(`error parsing script injection ${inspect(o)}: ${e}`, { cause: e }) + } +} + +const scriptInjectionToLabels = ( + id: string, + { src, async, defer, pathRegex }: ScriptInjection, +): Record => mapKeys>({ + src, + ...async && { async: 'true' }, + ...defer && { defer: 'true' }, + ...pathRegex && { path_regex: pathRegex.source }, +}, (_value, key) => [COMPOSE_TUNNEL_AGENT_SERVICE_LABELS.INJECT_SCRIPT_PREFIX, id, key].join('.')) + +export const scriptInjectionsToLabels = ( + injections: Record +) => Object.fromEntries( + Object.entries(injections).flatMap(([id, injection]) => Object.entries(scriptInjectionToLabels(id, injection))) +) + +const groupedLabelsRe = /^(?[^\s]+)\.(?[^.\s]+)\.(?[^.\s]+)$/ +type ParsedGroupedLabelKey = { prefix: string; id: string; key: string } +const parseGroupedLabelKey = (key: string) => { + const match = groupedLabelsRe.exec(key) + return match && match.groups as ParsedGroupedLabelKey +} + +const parseLabelsWithPrefixAndId = ( + labels: Record, + prefix: string, +): Record[] => { + const split: [ParsedGroupedLabelKey | null, string][] = Object.entries(labels) + .map(([k, v]) => [parseGroupedLabelKey(k), v]) + const filteredForPrefix = split.filter(([k]) => k?.prefix === prefix) as [ParsedGroupedLabelKey, string][] + const grouped = groupBy(filteredForPrefix, ([{ id }]) => id) + return Object.values(grouped).map(group => Object.fromEntries(group.map(([{ key }, value]) => [key, value]))) +} + +export const parseScriptInjectionLabels = ( + labels: Record, +): [ScriptInjection[], 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[]] +} diff --git a/packages/common/src/compose-utils.ts b/packages/common/src/compose-utils.ts deleted file mode 100644 index 854ef208..00000000 --- a/packages/common/src/compose-utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { set, camelCase, snakeCase } from 'lodash' - -export const extractSectionsFromLabels = (prefix: string, labels: Record) => { - const sections:{[id:string]: T } = {} - const normalizedPrefix = prefix.endsWith('.') ? prefix : `${prefix}.` - Object.entries(labels) - .filter(([key]) => key.startsWith(normalizedPrefix)) - .map(([key, value]) => [...key.substring(normalizedPrefix.length).split('.'), value]) - .forEach(([id, prop, value]) => set(sections, [id, camelCase(prop)], value)) - return sections -} - -export const parseBooleanLabelValue = (s:string) => s === 'true' || s === '1' - -const formatValueLabel = (x:unknown) => { - if (x instanceof RegExp) { - return x.source - } - return `${x}` -} - -export const sectionToLabels = (prefix: string, section: Record) => - Object.fromEntries(Object.entries(section).map(([key, value]) => ([`${prefix}.${snakeCase(key)}`, formatValueLabel(value)]))) diff --git a/packages/compose-tunnel-agent/src/docker/events-client.ts b/packages/compose-tunnel-agent/src/docker/events-client.ts index cdf41e58..db59524b 100644 --- a/packages/compose-tunnel-agent/src/docker/events-client.ts +++ b/packages/compose-tunnel-agent/src/docker/events-client.ts @@ -1,6 +1,7 @@ import Docker from 'dockerode' import { tryParseJson, Logger, ScriptInjection } from '@preevy/common' import { throttle } from 'lodash' +import { inspect } from 'util' import { filters } from './filters' import { containerToService } from './services' @@ -28,8 +29,13 @@ export const eventsClient = ({ }) => { const { listContainers, apiFilter } = filters({ docker, composeProject }) - const toService = (container: Docker.ContainerInfo) => containerToService({ container, defaultAccess }) - const getRunningServices = async (): Promise => (await listContainers()).map(toService) + const getRunningServices = async (): Promise => (await listContainers()).map(container => { + const { errors, ...service } = containerToService({ container, defaultAccess }) + if (errors.length) { + log.warn('error parsing docker container "%s" info, some information may be missing: %j', container.Names?.[0], inspect(errors)) + } + return service + }) const startListening = async ({ onChange }: { onChange: (services: RunningService[]) => void }) => { const handler = throttle(async (data?: Buffer) => { diff --git a/packages/compose-tunnel-agent/src/docker/services.ts b/packages/compose-tunnel-agent/src/docker/services.ts index ea50cafb..3b9ee502 100644 --- a/packages/compose-tunnel-agent/src/docker/services.ts +++ b/packages/compose-tunnel-agent/src/docker/services.ts @@ -1,4 +1,4 @@ -import { ScriptInjection, COMPOSE_TUNNEL_AGENT_SERVICE_LABELS as PREEVY_LABELS, scriptInjectionFromLabels } from '@preevy/common' +import { ScriptInjection, COMPOSE_TUNNEL_AGENT_SERVICE_LABELS as PREEVY_LABELS, parseScriptInjectionLabels } from '@preevy/common' import Docker from 'dockerode' import { portFilter } from './filters' import { COMPOSE_PROJECT_LABEL, COMPOSE_SERVICE_LABEL } from './labels' @@ -13,17 +13,22 @@ export type RunningService = { } const GLOBAL_INJECT_SCRIPTS = process.env.GLOBAL_INJECT_SCRIPTS - ? JSON.parse(process.env.GLOBAL_INJECT_SCRIPTS) as ScriptInjection[] : [] + ? JSON.parse(process.env.GLOBAL_INJECT_SCRIPTS) as ScriptInjection[] + : [] export const containerToService = ({ container, defaultAccess, -}: {container: Docker.ContainerInfo; defaultAccess: 'private' | 'public'}): RunningService => ({ - 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: [...scriptInjectionFromLabels(container.Labels), ...GLOBAL_INJECT_SCRIPTS], -}) +}: { container: Docker.ContainerInfo; defaultAccess: 'private' | 'public' }): RunningService & { errors: Error[] } => { + 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, + }) +} diff --git a/packages/core/src/commands/up/index.ts b/packages/core/src/commands/up/index.ts index f905b9c3..6742b973 100644 --- a/packages/core/src/commands/up/index.ts +++ b/packages/core/src/commands/up/index.ts @@ -1,11 +1,10 @@ -import { COMPOSE_TUNNEL_AGENT_SERVICE_NAME, formatPublicKey, readOrUndefined } from '@preevy/common' +import { COMPOSE_TUNNEL_AGENT_SERVICE_NAME, ScriptInjection, formatPublicKey, readOrUndefined } from '@preevy/common' import fs from 'fs' import path from 'path' import { rimraf } from 'rimraf' import yaml from 'yaml' -import { widgetScriptInjector } from '../../compose/script-injection' import { TunnelOpts } from '../../ssh' -import { composeModelFilename, fixModelForRemote, localComposeClient } from '../../compose' +import { composeModelFilename, fixModelForRemote, localComposeClient, addScriptInjectionsToModel } from '../../compose' import { ensureCustomizedMachine } from './machine' import { wrapWithDockerSocket } from '../../docker' import { addComposeTunnelAgentService } from '../../compose-tunnel-agent-client' @@ -36,7 +35,7 @@ const createCopiedFileInDataDir = ( return result } -const calcComposeArgs = ({ userSpecifiedServices, debug, cwd } : { +const calcComposeUpArgs = ({ userSpecifiedServices, debug, cwd } : { userSpecifiedServices: string[] debug: boolean cwd: string @@ -68,7 +67,7 @@ const up = async ({ tunnelOpts, userSpecifiedProjectName, userSpecifiedServices, - injectLivecycleScript, + scriptInjections, composeFiles, log, dataDir, @@ -91,7 +90,7 @@ const up = async ({ composeFiles: string[] log: Logger dataDir: string - injectLivecycleScript: string | undefined + scriptInjections?: Record sshTunnelPrivateKey: string | Buffer allowedSshHostKeys: Buffer cwd: string @@ -129,28 +128,31 @@ const up = async ({ machineDriver, machineCreationDriver, machineDriverName, envId, log, debug, }) + let remoteModel = addComposeTunnelAgentService({ + envId, + debug, + tunnelOpts, + sshPrivateKeyPath: path.posix.join(remoteDir, sshPrivateKeyFile.remote), + knownServerPublicKeyPath: path.posix.join(remoteDir, knownServerPublicKey.remote), + user: userAndGroup.join(':'), + machineStatusCommand: await machineDriver.machineStatusCommand(machine), + envMetadata: await envMetadata({ envId, version }), + composeModelPath: path.posix.join(remoteDir, composeModelFilename), + privateMode: false, + defaultAccess: 'public', + composeProject: projectName, + }, fixedModel) + + if (scriptInjections) { + remoteModel = addScriptInjectionsToModel( + remoteModel, + serviceName => (serviceName !== COMPOSE_TUNNEL_AGENT_SERVICE_NAME ? scriptInjections : undefined), + ) + } + try { const { exec } = connection - let remoteModel = addComposeTunnelAgentService({ - envId, - debug, - tunnelOpts, - sshPrivateKeyPath: path.posix.join(remoteDir, sshPrivateKeyFile.remote), - knownServerPublicKeyPath: path.posix.join(remoteDir, knownServerPublicKey.remote), - user: userAndGroup.join(':'), - machineStatusCommand: await machineDriver.machineStatusCommand(machine), - envMetadata: await envMetadata({ envId, version }), - composeModelPath: path.posix.join(remoteDir, composeModelFilename), - privateMode: false, - defaultAccess: 'public', - composeProject: projectName, - }, fixedModel) - - if (injectLivecycleScript) { - const scriptInjector = widgetScriptInjector(injectLivecycleScript) - remoteModel = scriptInjector.inject(remoteModel, x => x !== COMPOSE_TUNNEL_AGENT_SERVICE_NAME) - } const modelStr = yaml.stringify(remoteModel) log.debug('model', modelStr) const composeFilePath = await createCopiedFile(composeModelFilename, modelStr) @@ -165,7 +167,7 @@ const up = async ({ composeFiles: [composeFilePath.local], projectName: userSpecifiedProjectName, }) - const composeArgs = calcComposeArgs({ userSpecifiedServices, debug, cwd }) + const composeArgs = calcComposeUpArgs({ userSpecifiedServices, debug, cwd }) const withDockerSocket = wrapWithDockerSocket({ connection, log }) diff --git a/packages/core/src/compose-tunnel-agent-client.ts b/packages/core/src/compose-tunnel-agent-client.ts index a83e3ba4..47fb8054 100644 --- a/packages/core/src/compose-tunnel-agent-client.ts +++ b/packages/core/src/compose-tunnel-agent-client.ts @@ -124,7 +124,6 @@ export const addComposeTunnelAgentService = ( user, labels: { [COMPOSE_TUNNEL_AGENT_SERVICE_LABELS.ENV_ID]: envId, - [COMPOSE_TUNNEL_AGENT_SERVICE_LABELS.PRIVATE_MODE]: privateMode, ...profileThumbprint ? { [COMPOSE_TUNNEL_AGENT_SERVICE_LABELS.PROFILE_THUMBPRINT]: profileThumbprint } : {}, [COMPOSE_TUNNEL_AGENT_SERVICE_LABELS.PRIVATE_MODE]: privateMode.toString(), }, diff --git a/packages/core/src/compose/index.ts b/packages/core/src/compose/index.ts index 991e33ea..2758584a 100644 --- a/packages/core/src/compose/index.ts +++ b/packages/core/src/compose/index.ts @@ -2,3 +2,4 @@ export * from './client' export * from './model' export { resolveComposeFiles } from './files' export * from './remote' +export * from './script-injection' diff --git a/packages/core/src/compose/script-injection.test.ts b/packages/core/src/compose/script-injection.test.ts index 1cc31f5b..72bd09d4 100644 --- a/packages/core/src/compose/script-injection.test.ts +++ b/packages/core/src/compose/script-injection.test.ts @@ -1,44 +1,57 @@ -import { describe, test, expect } from '@jest/globals' +import { describe, expect, jest, beforeEach, it } from '@jest/globals' +import { ScriptInjection } from '@preevy/common' import { ComposeModel } from './model' -import { scriptInjector } from './script-injection' - -describe('script injection', () => { - test('inject script to all services', async () => { - const model:ComposeModel = { - name: 'my-app', - services: { - frontend1: {}, - frontend2: { - labels: { - other: 'value', - }, +import { addScriptInjectionsToModel } from './script-injection' + +describe('addScriptInjectionsToModel', () => { + const model: ComposeModel = Object.freeze({ + name: 'my-app', + services: { + frontend1: {}, + frontend2: { + labels: { + other: 'value', }, }, - } + frontend3: {}, + }, + }) - const injector = scriptInjector('test', { src: 'https://mydomain.com/myscript.ts', async: true, pathRegex: /.*/ }) - const newModel = injector.inject(model) - expect(newModel.services?.frontend1?.labels).toMatchObject({ 'preevy.inject_script.test.src': 'https://mydomain.com/myscript.ts', 'preevy.inject_script.test.async': 'true', 'preevy.inject_script.test.path_regex': '.*' }) - expect(newModel.services?.frontend2?.labels).toMatchObject({ other: 'value', 'preevy.inject_script.test.src': 'https://mydomain.com/myscript.ts', 'preevy.inject_script.test.async': 'true', 'preevy.inject_script.test.path_regex': '.*' }) + let callback: jest.MockedFunction<(name: string) => Record | undefined> + let newModel: ComposeModel + + const injection: ScriptInjection = { + src: 'https://mydomain.com/myscript.ts', + async: true, + pathRegex: /.*/, + } + + beforeEach(() => { + callback = jest.fn(name => (['frontend1', 'frontend2'].includes(name) ? ({ test: injection }) : undefined)) + newModel = addScriptInjectionsToModel(model, callback) }) - test('does not affect original model', async () => { - const model:ComposeModel = { - name: 'my-app', - services: { - frontend1: {}, - frontend2: { - labels: { - other: 'value', - }, - }, - }, + it('injects the script for the first two services', () => { + const expectedLabels = { + 'preevy.inject_script.test.src': 'https://mydomain.com/myscript.ts', + 'preevy.inject_script.test.async': 'true', + 'preevy.inject_script.test.path_regex': '.*', } + expect(newModel.services?.frontend1?.labels).toMatchObject(expectedLabels) + expect(newModel.services?.frontend2?.labels).toMatchObject({ other: 'value', ...expectedLabels }) + }) + + it('does not inject the script for the last service', () => { + expect(newModel.services?.frontend3?.labels).toMatchObject({}) + }) + + it('calls the factory correctly', () => { + expect(callback).toHaveBeenCalledTimes(3) + expect(callback).toHaveBeenCalledWith('frontend1', {}) + expect(callback).toHaveBeenCalledWith('frontend2', { labels: { other: 'value' } }) + }) - const injector = scriptInjector('test', { src: 'https://mydomain.com/myscript.ts' }) - const newModel = injector.inject(model) - expect(model.services?.frontend1?.labels).toBeUndefined() - expect(model.services?.frontend2?.labels).not - .toMatchObject(newModel.services?.frontend2?.labels as Record) + it('does not affect original model', () => { + expect(newModel).not.toBe(model) }) }) diff --git a/packages/core/src/compose/script-injection.ts b/packages/core/src/compose/script-injection.ts index cf38e445..0c92f1ee 100644 --- a/packages/core/src/compose/script-injection.ts +++ b/packages/core/src/compose/script-injection.ts @@ -1,38 +1,22 @@ -import { ScriptInjection, sectionToLabels } from '@preevy/common' +import { ScriptInjection, scriptInjectionsToLabels } from '@preevy/common' +import { mapValues } from 'lodash' import { ComposeModel, ComposeService } from './model' -export const addScript = (model: ComposeModel, service: string, { id, ...script } : - {id: string} & ScriptInjection):ComposeModel => { - const { services } = model - if (!services || !(service in services)) { - return model - } - const serviceDef = services[service] - return { - ...model, - services: { - ...model.services, - [service]: { - ...serviceDef, - labels: { - ...serviceDef.labels, - ...sectionToLabels(`preevy.inject_script.${id}`, script), - }, - }, - }, - } -} +const addScriptInjectionsToService = ( + service: ComposeService, + injections: Record, +): ComposeService => ({ + ...service, + labels: { + ...service.labels, + ...scriptInjectionsToLabels(injections), + }, +}) -export const scriptInjector = (id : string, script: ScriptInjection) => { - const injectScript = (model:ComposeModel, service:string) => addScript(model, service, { id, ...script }) - const injectAll = (_serviceName:string, _def: ComposeService) => true - return { - inject: (model: ComposeModel, serviceFilter = injectAll) => - Object.keys(model.services ?? {}) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .filter(s => serviceFilter(s, model.services![s])) - .reduce(injectScript, model), - } -} - -export const widgetScriptInjector = (url:string) => scriptInjector('livecycle-widget', { src: url }) +export const addScriptInjectionsToModel = ( + model: ComposeModel, + factory: (serviceName: string, serviceDef: ComposeService) => Record | undefined, +): ComposeModel => ({ + ...model, + services: mapValues(model.services ?? {}, (def, name) => addScriptInjectionsToService(def, factory(name, def) ?? {})), +}) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 76f8e2a4..e320b2b4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -22,7 +22,7 @@ export { export { profileStore, Profile, ProfileStore, link, Org } from './profile' export { telemetryEmitter, registerEmitter, wireProcessExit, createTelemetryEmitter, machineId } from './telemetry' export { fsTypeFromUrl, Store, VirtualFS, localFsFromUrl, localFs } from './store' -export { localComposeClient, ComposeModel, resolveComposeFiles, getExposedTcpServicePorts, remoteUserModel, NoComposeFilesError } from './compose' +export { localComposeClient, ComposeModel, resolveComposeFiles, getExposedTcpServicePorts, remoteUserModel, NoComposeFilesError, addScriptInjectionsToModel } from './compose' export { withSpinner } from './spinner' export { findEnvId, findProjectName, findEnvIdByProjectName, validateEnvId, normalize as normalizeEnvId, EnvId } from './env-id' export { sshKeysStore } from './state'