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 <quentin.guillemot@cyclair.fr>
This commit is contained in:
Quentin Guillemot 2024-04-25 21:47:31 +02:00
parent 10ca5b4f59
commit 258beb09a4
6 changed files with 71 additions and 14 deletions

View File

@ -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

View File

@ -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,

View File

@ -53,6 +53,7 @@ export interface TaggedImage {
export interface ComposeOpts {
convertEol: boolean;
dockerfilePath?: string;
composefile?: string;
inlineLogs?: boolean;
multiDockerignore: boolean;
noParentCheck: boolean;

View File

@ -38,6 +38,7 @@ export function generateOpts(options: {
nologs: boolean;
'noconvert-eol': boolean;
dockerfile?: string;
composefile?: string;
'multi-dockerignore': boolean;
'noparent-check': boolean;
}): Promise<ComposeOpts> {
@ -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'],
}));

View File

@ -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)),
),
),

View File

@ -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<void> {
const project = await loadProject(globalLogger, {
convertEol: opts.convertEol,
dockerfilePath: opts.dockerfilePath,
composefile: opts.composefile,
multiDockerignore: opts.multiDockerignore,
noParentCheck: opts.noParentCheck,
projectName: 'local',