From fc5d24d7c8389498e39da130a630ecaaf4729f3d Mon Sep 17 00:00:00 2001 From: Quentin Guillemot Date: Thu, 25 Apr 2024 21:47:31 +0200 Subject: [PATCH] Feat: Add ability to select a docker-compose file Allow using the --composefile argument with the push command to specify the compose file Change-type: minor Signed-off-by: Quentin Guillemot --- docs/balena-cli.md | 5 ++++ lib/commands/push/index.ts | 12 +++++++++ lib/utils/compose-types.d.ts | 1 + lib/utils/compose.ts | 2 ++ lib/utils/compose_ts.ts | 51 +++++++++++++++++++++++++++--------- lib/utils/device/deploy.ts | 1 + 6 files changed, 59 insertions(+), 13 deletions(-) diff --git a/docs/balena-cli.md b/docs/balena-cli.md index ffa463379d..a8cb33fe19 100644 --- a/docs/balena-cli.md +++ b/docs/balena-cli.md @@ -3316,6 +3316,11 @@ suspected issues with the balenaCloud backend. Alternative Dockerfile name/path, relative to the source folder +#### --composefile COMPOSEFILE + +Alternative compose file name/path, relative to the source folder. +Only works for local devices + #### -c, --nocache Don't use cached layers of previously built images for this project. This diff --git a/lib/commands/push/index.ts b/lib/commands/push/index.ts index 7693f7c430..aa8c75cdc8 100644 --- a/lib/commands/push/index.ts +++ b/lib/commands/push/index.ts @@ -121,6 +121,11 @@ export default class PushCmd extends Command { description: 'Alternative Dockerfile name/path, relative to the source folder', }), + composefile: Flags.string({ + description: stripIndent` + Alternative compose file name/path, relative to the source folder. + Only works for local devices`, + }), nocache: Flags.boolean({ description: stripIndent` Don't use cached layers of previously built images for this project. This @@ -238,6 +243,7 @@ export default class PushCmd extends Command { sdk, { dockerfilePath: options.dockerfile, + composefile: options.composefile, noParentCheck: options['noparent-check'], projectPath: options.source, registrySecretsPath: options['registry-secrets'], @@ -248,6 +254,11 @@ export default class PushCmd extends Command { case BuildTarget.Cloud: logger.logDebug(`Pushing to cloud for fleet: ${params.fleetOrDevice}`); + if (options.composefile) { + throw new Error(stripIndent` + The use of a compose file is not permitted for cloud builds.`); + } + await this.pushToCloud( params.fleetOrDevice, options, @@ -363,6 +374,7 @@ export default class PushCmd extends Command { source: options.source, deviceHost: localDeviceAddress, dockerfilePath, + composefile: options.composefile, registrySecrets, multiDockerignore: options['multi-dockerignore'], nocache: options.nocache, diff --git a/lib/utils/compose-types.d.ts b/lib/utils/compose-types.d.ts index 1d8924a255..6b9f32e4e5 100644 --- a/lib/utils/compose-types.d.ts +++ b/lib/utils/compose-types.d.ts @@ -53,6 +53,7 @@ export interface TaggedImage { export interface ComposeOpts { convertEol: boolean; dockerfilePath?: string; + composefile?: string; inlineLogs?: boolean; multiDockerignore: boolean; noParentCheck: boolean; diff --git a/lib/utils/compose.ts b/lib/utils/compose.ts index cfcfd4a404..cdbf00f233 100644 --- a/lib/utils/compose.ts +++ b/lib/utils/compose.ts @@ -38,6 +38,7 @@ export function generateOpts(options: { nologs: boolean; 'noconvert-eol': boolean; dockerfile?: string; + composefile?: string; 'multi-dockerignore': boolean; 'noparent-check': boolean; }): Promise { @@ -48,6 +49,7 @@ export function generateOpts(options: { inlineLogs: !options.nologs, convertEol: !options['noconvert-eol'], dockerfilePath: options.dockerfile, + composefile: options.composefile, multiDockerignore: !!options['multi-dockerignore'], noParentCheck: options['noparent-check'], })); diff --git a/lib/utils/compose_ts.ts b/lib/utils/compose_ts.ts index 3a34c7fd98..5c19810a80 100644 --- a/lib/utils/compose_ts.ts +++ b/lib/utils/compose_ts.ts @@ -128,7 +128,12 @@ export async function loadProject( composeStr = compose.defaultComposition(image); } else { logger.logDebug('Resolving project...'); - [composeName, composeStr] = await resolveProject(logger, opts.projectPath); + [composeName, composeStr] = await resolveProject( + logger, + opts.projectPath, + false, + opts.composefile, + ); if (composeName) { if (opts.dockerfilePath) { @@ -143,8 +148,9 @@ export async function loadProject( composeStr = compose.defaultComposition(undefined, opts.dockerfilePath); } - // If local push, merge dev compose overlay - if (opts.isLocal) { + // If local push and no specific compose file has been provided, + // merge dev compose overlay + if (opts.isLocal && !opts.composefile) { composeStr = await mergeDevComposeOverlay( logger, composeStr, @@ -206,10 +212,14 @@ async function resolveProject( logger: Logger, projectRoot: string, quiet = false, + specificComposeName?: string, ): Promise<[string, string]> { let composeFileName = ''; let composeFileContents = ''; - for (const fname of compositionFileNames) { + const compositionFileNamesLocal = specificComposeName + ? [specificComposeName] + : compositionFileNames; + for (const fname of compositionFileNamesLocal) { const fpath = path.join(projectRoot, fname); if (await exists(fpath)) { logger.logDebug(`${fname} file found at "${projectRoot}"`); @@ -1149,6 +1159,7 @@ export async function validateProjectDirectory( sdk: BalenaSDK, opts: { dockerfilePath?: string; + composefile?: string; noParentCheck: boolean; projectPath: string; registrySecretsPath?: string; @@ -1175,15 +1186,29 @@ export async function validateProjectDirectory( ); } else { const files = await fs.readdir(opts.projectPath); - const projectMatch = (file: string) => - /^(Dockerfile|Dockerfile\.\S+|docker-compose.ya?ml|package.json)$/.test( - file, - ); - if (!_.some(files, projectMatch)) { - throw new ExpectedError(stripIndent` - Error: no "Dockerfile[.*]", "docker-compose.yml" or "package.json" file - found in source folder "${opts.projectPath}" - `); + + const projectMatch = (file: string, composefile?: string) => { + let regexPattern = + /^(Dockerfile|Dockerfile\.\S+|docker-compose.ya?ml|package(\.json)?)$/; + if (composefile) { + regexPattern = new RegExp( + `^(Dockerfile|Dockerfile\\.\\S+|docker-compose.ya?ml|${composefile}|package(\\.json)?)$`, + ); + } + return regexPattern.test(file); + }; + if (!_.some(files, (file) => projectMatch(file, opts.composefile))) { + if (opts.composefile) { + throw new ExpectedError(stripIndent` + Error: no "${opts.composefile}" file + found in source folder "${opts.projectPath}" + `); + } else { + throw new ExpectedError(stripIndent` + Error: no "Dockerfile[.*]", "docker-compose.yml" or "package.json" file + found in source folder "${opts.projectPath}" + `); + } } if (!opts.noParentCheck) { const checkCompose = async (folder: string) => { diff --git a/lib/utils/device/deploy.ts b/lib/utils/device/deploy.ts index 018467e825..2a0a06f985 100644 --- a/lib/utils/device/deploy.ts +++ b/lib/utils/device/deploy.ts @@ -57,6 +57,7 @@ export interface DeviceDeployOptions { deviceHost: string; devicePort?: number; dockerfilePath?: string; + composefile?: string; registrySecrets: RegistrySecrets; multiDockerignore: boolean; nocache: boolean;