From a4e6da111a935b456181f9de8d023cd1731f33b1 Mon Sep 17 00:00:00 2001 From: Roy Razon Date: Sun, 28 Jan 2024 10:17:05 +0200 Subject: [PATCH] add `--project-directory` flag - add flag to allow setting the project directory like in the `docker compose` CLI - remove usages of cwd where applicable fixes #382 --- .../cli-common/src/hooks/init/load-plugins.ts | 9 ++++--- .../cli-common/src/lib/common-flags/index.ts | 4 ++++ packages/cli-common/src/lib/plugins/model.ts | 4 ++-- packages/cli/src/commands/logs.ts | 2 +- packages/cli/src/commands/up.ts | 3 +-- packages/core/src/commands/build.ts | 4 +--- packages/core/src/commands/model.ts | 7 ++---- packages/core/src/commands/up.ts | 9 ++----- packages/core/src/compose/files.ts | 19 +++++++++++---- packages/core/src/compose/model.ts | 5 ++++ packages/core/src/compose/remote.ts | 24 +++++++++---------- packages/core/src/index.ts | 1 + 12 files changed, 50 insertions(+), 41 deletions(-) diff --git a/packages/cli-common/src/hooks/init/load-plugins.ts b/packages/cli-common/src/hooks/init/load-plugins.ts index 632585ae..598b2d18 100644 --- a/packages/cli-common/src/hooks/init/load-plugins.ts +++ b/packages/cli-common/src/hooks/init/load-plugins.ts @@ -1,3 +1,4 @@ +import path from 'path' import { Hook as OclifHook, Command, Flags } from '@oclif/core' import { Parser } from '@oclif/core/lib/parser/parse.js' import { BooleanFlag, Config, Topic } from '@oclif/core/lib/interfaces' @@ -41,9 +42,11 @@ export const initHook: OclifHook<'init'> = async function hook(args) { argv, } as const).parse() - const composeFiles = await resolveComposeFiles({ + const { files: composeFiles, projectDirectory } = await resolveComposeFiles({ userSpecifiedFiles: flags.file, userSpecifiedSystemFiles: flags['system-compose-file'], + userSpecifiedProjectDirectory: flags['project-directory'], + cwd: process.cwd(), }) const userModelOrError = composeFiles.length @@ -53,7 +56,7 @@ export const initHook: OclifHook<'init'> = async function hook(args) { async () => await localComposeClient({ composeFiles, projectName: flags.project, - projectDirectory: process.cwd(), + projectDirectory, }).getModelOrError(), { text: `Loading compose file${composeFiles.length > 1 ? 's' : ''}: ${composeFiles.join(', ')}`, @@ -76,7 +79,7 @@ export const initHook: OclifHook<'init'> = async function hook(args) { (config as InternalConfig).loadTopics({ commands, topics }) Object.assign(config, { - composeFiles, + composeFiles: { files: composeFiles, projectDirectory }, initialUserModel: userModelOrError, preevyConfig, preevyHooks: hooksFromPlugins(loadedPlugins.map(p => p.initResults)), diff --git a/packages/cli-common/src/lib/common-flags/index.ts b/packages/cli-common/src/lib/common-flags/index.ts index c917e204..875e3dc8 100644 --- a/packages/cli-common/src/lib/common-flags/index.ts +++ b/packages/cli-common/src/lib/common-flags/index.ts @@ -35,6 +35,10 @@ export const composeFlags = { default: [], helpGroup: 'GLOBAL', }), + 'project-directory': Flags.string({ + required: false, + summary: 'Alternate working directory (default: the path of the first specified Compose file)', + }), ...projectFlag, } as const diff --git a/packages/cli-common/src/lib/plugins/model.ts b/packages/cli-common/src/lib/plugins/model.ts index 89253e72..68e88dd0 100644 --- a/packages/cli-common/src/lib/plugins/model.ts +++ b/packages/cli-common/src/lib/plugins/model.ts @@ -1,7 +1,7 @@ import { FlagProps } from '@oclif/core/lib/interfaces/parser.js' import { Topic } from '@oclif/core/lib/interfaces' import { Command } from '@oclif/core' -import { ComposeModel, config as coreConfig } from '@preevy/core' +import { ComposeFiles, ComposeModel, config as coreConfig } from '@preevy/core' import { PluginInitContext } from './context.js' import { HookFuncs, HooksListeners } from '../hooks.js' import PreevyConfig = coreConfig.PreevyConfig @@ -25,7 +25,7 @@ export type PluginModule = { declare module '@oclif/core/lib/config/config.js' { export interface Config { - composeFiles: string[] + composeFiles: ComposeFiles initialUserModel: ComposeModel | Error preevyHooks: HooksListeners preevyConfig: PreevyConfig diff --git a/packages/cli/src/commands/logs.ts b/packages/cli/src/commands/logs.ts index 2f96314d..d38fcb0b 100644 --- a/packages/cli/src/commands/logs.ts +++ b/packages/cli/src/commands/logs.ts @@ -100,7 +100,7 @@ export default class Logs extends DriverCommand { const compose = localComposeClient({ composeFiles: Buffer.from(yaml.stringify(addBaseComposeTunnelAgentService(userModel))), projectName: flags.project, - projectDirectory: process.cwd(), + projectDirectory: this.config.composeFiles.projectDirectory, }) await using dockerContext = await dockerEnvContext({ connection, log }) diff --git a/packages/cli/src/commands/up.ts b/packages/cli/src/commands/up.ts index e79e8036..7582c8ea 100644 --- a/packages/cli/src/commands/up.ts +++ b/packages/cli/src/commands/up.ts @@ -83,7 +83,7 @@ export default class Up extends MachineCreationDriverCommand { ...tunnelServerFlags, ...buildFlags, 'skip-volume': Flags.string({ - description: 'Additional volume glob patterns to skip copying', + description: 'Additional volume glob patterns to skip copying (relative to project directory)', multiple: true, multipleNonGreedy: true, default: [], @@ -199,7 +199,6 @@ export default class Up extends MachineCreationDriverCommand { dataDir: this.config.dataDir, sshTunnelPrivateKey: tunnelingKey, allowedSshHostKeys: hostKey, - cwd: process.cwd(), skipUnchangedFiles: flags['skip-unchanged-files'], version: this.config.version, buildSpec, diff --git a/packages/core/src/commands/build.ts b/packages/core/src/commands/build.ts index 9acd847b..0534eb8d 100644 --- a/packages/core/src/commands/build.ts +++ b/packages/core/src/commands/build.ts @@ -14,7 +14,6 @@ const buildCommand = async ({ log, composeModel, projectLocalDataDir, - cwd, buildSpec, machineDockerPlatform, env, @@ -23,7 +22,6 @@ const buildCommand = async ({ log: Logger composeModel: ComposeModel projectLocalDataDir: string - cwd: string buildSpec: BuildSpec machineDockerPlatform: string env?: Record @@ -47,7 +45,7 @@ const buildCommand = async ({ ] log.info(`Running: docker ${dockerArgs.join(' ')}`) - const { elapsedTimeSec } = await measureTime(() => childProcessPromise(spawn('docker', dockerArgs, { stdio: 'inherit', cwd, env }))) + const { elapsedTimeSec } = await measureTime(() => childProcessPromise(spawn('docker', dockerArgs, { stdio: 'inherit', env }))) telemetryEmitter().capture('build success', { elapsed_sec: elapsedTimeSec, has_registry: Boolean(buildSpec.registry), diff --git a/packages/core/src/commands/model.ts b/packages/core/src/commands/model.ts index 85a0865d..8174250e 100644 --- a/packages/core/src/commands/model.ts +++ b/packages/core/src/commands/model.ts @@ -2,7 +2,7 @@ import { MachineStatusCommand, ScriptInjection } from '@preevy/common' import path from 'path' import { rimraf } from 'rimraf' import { TunnelOpts } from '../ssh/index.js' -import { ComposeModel, remoteComposeModel } from '../compose/index.js' +import { ComposeFiles, ComposeModel, remoteComposeModel } from '../compose/index.js' import { createCopiedFileInDataDir } from '../remote-files.js' import { Logger } from '../log.js' import { EnvId } from '../env-id.js' @@ -21,7 +21,6 @@ const composeModel = async ({ dataDir, allowedSshHostKeys: hostKey, sshTunnelPrivateKey, - cwd, version, envId, expectedServiceUrls, @@ -35,13 +34,12 @@ const composeModel = async ({ userSpecifiedProjectName: string | undefined userSpecifiedServices: string[] volumeSkipList: string[] - composeFiles: string[] + composeFiles: ComposeFiles log: Logger dataDir: string scriptInjections?: Record sshTunnelPrivateKey: string | Buffer allowedSshHostKeys: Buffer - cwd: string version: string envId: EnvId expectedServiceUrls: { name: string; port: number; url: string }[] @@ -60,7 +58,6 @@ const composeModel = async ({ volumeSkipList, composeFiles, log, - cwd, expectedServiceUrls, projectName, modelFilter, diff --git a/packages/core/src/commands/up.ts b/packages/core/src/commands/up.ts index e9cbf190..76ffb7a6 100644 --- a/packages/core/src/commands/up.ts +++ b/packages/core/src/commands/up.ts @@ -1,7 +1,7 @@ import { MachineStatusCommand, ScriptInjection } from '@preevy/common' import yaml from 'yaml' import { TunnelOpts } from '../ssh/index.js' -import { ComposeModel, composeModelFilename, localComposeClient } from '../compose/index.js' +import { ComposeFiles, ComposeModel, composeModelFilename, localComposeClient } from '../compose/index.js' import { dockerEnvContext } from '../docker.js' import { MachineConnection } from '../driver/index.js' import { remoteProjectDir } from '../remote-files.js' @@ -54,7 +54,6 @@ const up = async ({ dataDir, allowedSshHostKeys, sshTunnelPrivateKey, - cwd, skipUnchangedFiles, version, envId, @@ -72,13 +71,12 @@ const up = async ({ userSpecifiedProjectName: string | undefined userSpecifiedServices: string[] volumeSkipList: string[] - composeFiles: string[] + composeFiles: ComposeFiles log: Logger dataDir: string scriptInjections?: Record sshTunnelPrivateKey: string | Buffer allowedSshHostKeys: Buffer - cwd: string skipUnchangedFiles: boolean version: string envId: EnvId @@ -100,7 +98,6 @@ const up = async ({ log, machineStatusCommand, userAndGroup, - cwd, tunnelOpts, userSpecifiedProjectName, userSpecifiedServices, @@ -126,7 +123,6 @@ const up = async ({ composeModel = (await buildCommand({ log, buildSpec, - cwd, composeModel, projectLocalDataDir, machineDockerPlatform: dockerPlatform, @@ -151,7 +147,6 @@ const up = async ({ const compose = localComposeClient({ composeFiles: [composeFilePath.local], - projectDirectory: cwd, }) const composeArgs = [ diff --git a/packages/core/src/compose/files.ts b/packages/core/src/compose/files.ts index 59a1ee62..aba1f324 100644 --- a/packages/core/src/compose/files.ts +++ b/packages/core/src/compose/files.ts @@ -1,4 +1,6 @@ import fs from 'fs' +import path from 'path' +import { ComposeFiles } from './model.js' const DEFAULT_BASE_FILES = ['compose', 'docker-compose'] const DEFAULT_OVERRIDE_FILES = DEFAULT_BASE_FILES.map(f => `${f}.override`) @@ -46,11 +48,18 @@ const findDefaultFiles = async () => (await oneYamlFileArray(DEFAULT_BASE_FILES, const findDefaultSystemFiles = async () => (await oneYamlFileArray(DEFAULT_SYSTEM_FILES, 'default system Compose')) ?? [] export const resolveComposeFiles = async ( - { userSpecifiedFiles, userSpecifiedSystemFiles }: { + { userSpecifiedFiles, userSpecifiedSystemFiles, userSpecifiedProjectDirectory, cwd }: { userSpecifiedFiles: string[] userSpecifiedSystemFiles: string[] + userSpecifiedProjectDirectory: string + cwd: string }, -): Promise => [ - ...(userSpecifiedSystemFiles.length ? userSpecifiedSystemFiles : await findDefaultSystemFiles()), - ...(userSpecifiedFiles.length ? userSpecifiedFiles : await findDefaultFiles()), -] +): Promise => { + const files = (userSpecifiedFiles.length ? userSpecifiedFiles : await findDefaultFiles()) + const systemFiles = (userSpecifiedSystemFiles.length ? userSpecifiedSystemFiles : await findDefaultSystemFiles()) + + return { + files: [...systemFiles, ...files], + projectDirectory: path.resolve(userSpecifiedProjectDirectory ?? files.length ? path.dirname(files[0]) : cwd), + } +} diff --git a/packages/core/src/compose/model.ts b/packages/core/src/compose/model.ts index 0e052412..1f223e8d 100644 --- a/packages/core/src/compose/model.ts +++ b/packages/core/src/compose/model.ts @@ -66,3 +66,8 @@ export type ComposeModel = { } export const composeModelFilename = 'docker-compose.yaml' + +export type ComposeFiles = { + files: string[] + projectDirectory: string +} diff --git a/packages/core/src/compose/remote.ts b/packages/core/src/compose/remote.ts index f9f4bb23..a49bb496 100644 --- a/packages/core/src/compose/remote.ts +++ b/packages/core/src/compose/remote.ts @@ -2,10 +2,10 @@ import yaml from 'yaml' import path from 'path' import { mapValues } from 'lodash-es' import { MMRegExp, makeRe } from 'minimatch' -import { asyncMap, asyncToArray } from 'iter-tools-es' +import { asyncMap, asyncToArray, compose } from 'iter-tools-es' import { COMPOSE_TUNNEL_AGENT_SERVICE_NAME, MachineStatusCommand, ScriptInjection, formatPublicKey } from '@preevy/common' import { MachineConnection } from '../driver/index.js' -import { ComposeModel, ComposeSecretOrConfig, composeModelFilename } from './model.js' +import { ComposeFiles, ComposeModel, ComposeSecretOrConfig, composeModelFilename } from './model.js' import { REMOTE_DIR_BASE, remoteProjectDir } from '../remote-files.js' import { TunnelOpts } from '../ssh/index.js' import { addComposeTunnelAgentService } from '../compose-tunnel-agent-client.js' @@ -42,9 +42,9 @@ const toPosix = (x:string) => x.split(path.sep).join(path.posix.sep) export type SkippedVolume = { service: string; source: string; matchingRule: string } const fixModelForRemote = async ( - { skipServices = [], cwd, remoteBaseDir, volumeSkipList = defaultVolumeSkipList }: { + { skipServices = [], projectDirectory, remoteBaseDir, volumeSkipList = defaultVolumeSkipList }: { skipServices?: string[] - cwd: string + projectDirectory: string remoteBaseDir: string volumeSkipList: string[] }, @@ -55,7 +55,7 @@ const fixModelForRemote = async ( skippedVolumes: SkippedVolume[] }> => { const volumeSkipRes = volumeSkipList - .map(s => makeRe(path.resolve(cwd, s))) + .map(s => makeRe(path.resolve(projectDirectory, s))) .map((r, i) => { if (!r) { throw new Error(`Invalid glob pattern in volumeSkipList: "${volumeSkipList[i]}"`) @@ -70,7 +70,7 @@ const fixModelForRemote = async ( if (!path.isAbsolute(absolutePath)) { throw new Error(`expected absolute path: "${absolutePath}"`) } - const relativePath = toPosix(path.relative(cwd, absolutePath)) + const relativePath = toPosix(path.relative(projectDirectory, absolutePath)) return relativePath.startsWith('..') ? path.posix.join('absolute', absolutePath) @@ -166,7 +166,6 @@ export const remoteComposeModel = async ({ volumeSkipList, composeFiles, log, - cwd, expectedServiceUrls, projectName, agentSettings, @@ -176,9 +175,8 @@ export const remoteComposeModel = async ({ userSpecifiedProjectName: string | undefined userSpecifiedServices: string[] volumeSkipList: string[] - composeFiles: string[] + composeFiles: ComposeFiles log: Logger - cwd: string expectedServiceUrls: { name: string; port: number; url: string }[] projectName: string agentSettings?: AgentSettings @@ -186,15 +184,15 @@ export const remoteComposeModel = async ({ }) => { const remoteDir = remoteProjectDir(projectName) - log.debug(`Using compose files: ${composeFiles.join(', ')}`) + log.debug(`Using compose files: ${composeFiles.files.join(', ')} and project directory "${composeFiles.projectDirectory}"`) const linkEnvVars = serviceLinkEnvVars(expectedServiceUrls) const composeClientWithInjectedArgs = localComposeClient({ - composeFiles, + composeFiles: composeFiles.files, env: linkEnvVars, projectName: userSpecifiedProjectName, - projectDirectory: cwd, + projectDirectory: composeFiles.projectDirectory, }) const services = userSpecifiedServices.length @@ -202,7 +200,7 @@ export const remoteComposeModel = async ({ : [] const { model: fixedModel, filesToCopy, skippedVolumes } = await fixModelForRemote( - { cwd, remoteBaseDir: remoteDir, volumeSkipList }, + { projectDirectory: composeFiles.projectDirectory, remoteBaseDir: remoteDir, volumeSkipList }, await modelFilter(await composeClientWithInjectedArgs.getModel(services)), ) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6f8c6d05..430d67e4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -31,6 +31,7 @@ export { localComposeClient, ComposeModel, resolveComposeFiles, getExposedTcpServicePorts, fetchRemoteUserModel as remoteUserModel, NoComposeFilesError, addScriptInjectionsToServices as addScriptInjectionsToModel, + ComposeFiles, defaultVolumeSkipList, } from './compose/index.js' export { withSpinner } from './spinner.js'