diff --git a/docs/balena-cli.md b/docs/balena-cli.md index ffa46337..a8cb33fe 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 7693f7c4..aa8c75cd 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 1d8924a2..6b9f32e4 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 cfcfd4a4..cdbf00f2 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 3a34c7fd..4f31ef71 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,22 @@ async function resolveProject( logger: Logger, projectRoot: string, quiet = false, + specificComposeName?: string, ): Promise<[string, string]> { let composeFileName = ''; let composeFileContents = ''; - for (const fname of compositionFileNames) { + + let compositionFileNamesLocal: string[] = []; + if (specificComposeName) { + compositionFileNamesLocal = [specificComposeName]; + logger.logInfo( + `Using specified "${specificComposeName}" file in "${projectRoot}"`, + ); + } else { + compositionFileNamesLocal = 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 +1167,7 @@ export async function validateProjectDirectory( sdk: BalenaSDK, opts: { dockerfilePath?: string; + composefile?: string; noParentCheck: boolean; projectPath: string; registrySecretsPath?: string; @@ -1175,21 +1194,37 @@ 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) => { + const compositionFileNamesLocal = opts.composefile + ? [opts.composefile] + : compositionFileNames; return _.some( await Promise.all( - compositionFileNames.map((filename) => + compositionFileNamesLocal.map((filename) => exists(path.join(folder, filename)), ), ), diff --git a/lib/utils/device/deploy.ts b/lib/utils/device/deploy.ts index 018467e8..841c8ea6 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; @@ -183,6 +184,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise { const project = await loadProject(globalLogger, { convertEol: opts.convertEol, dockerfilePath: opts.dockerfilePath, + composefile: opts.composefile, multiDockerignore: opts.multiDockerignore, noParentCheck: opts.noParentCheck, projectName: 'local',