mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-02-21 09:51:58 +00:00
build/deploy: Add more test cases (--buildArg option)
Change-type: patch
This commit is contained in:
parent
099d755900
commit
6b208ec2ab
@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
import * as _ from 'lodash';
|
||||||
import mock = require('mock-require');
|
import mock = require('mock-require');
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@ -45,13 +46,16 @@ const commonResponseLines: { [key: string]: string[] } = {
|
|||||||
|
|
||||||
const commonQueryParams = {
|
const commonQueryParams = {
|
||||||
t: '${tag}',
|
t: '${tag}',
|
||||||
buildargs: '{}',
|
buildargs: {},
|
||||||
labels: '',
|
labels: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const commonComposeQueryParams = {
|
const commonComposeQueryParams = {
|
||||||
t: '${tag}',
|
t: '${tag}',
|
||||||
buildargs: '{"MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable"}',
|
buildargs: {
|
||||||
|
MY_VAR_1: 'This is a variable',
|
||||||
|
MY_VAR_2: 'Also a variable',
|
||||||
|
},
|
||||||
labels: '',
|
labels: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -375,19 +379,26 @@ describe('balena build', function () {
|
|||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
const expectedQueryParamsByService = {
|
const expectedQueryParamsByService = {
|
||||||
service1: Object.entries({
|
service1: Object.entries(
|
||||||
...commonComposeQueryParams,
|
_.merge({}, commonComposeQueryParams, {
|
||||||
buildargs:
|
buildargs: {
|
||||||
'{"BARG1":"b1","barg2":"B2","MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable","SERVICE1_VAR":"This is a service specific variable"}',
|
COMPOSE_ARG: 'A',
|
||||||
cachefrom: '["my/img1","my/img2"]',
|
barg: 'b',
|
||||||
}),
|
SERVICE1_VAR: 'This is a service specific variable',
|
||||||
service2: Object.entries({
|
},
|
||||||
...commonComposeQueryParams,
|
cachefrom: ['my/img1', 'my/img2'],
|
||||||
buildargs:
|
}),
|
||||||
'{"BARG1":"b1","barg2":"B2","MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable"}',
|
),
|
||||||
cachefrom: '["my/img1","my/img2"]',
|
service2: Object.entries(
|
||||||
dockerfile: 'Dockerfile-alt',
|
_.merge({}, commonComposeQueryParams, {
|
||||||
}),
|
buildargs: {
|
||||||
|
COMPOSE_ARG: 'A',
|
||||||
|
barg: 'b',
|
||||||
|
},
|
||||||
|
cachefrom: ['my/img1', 'my/img2'],
|
||||||
|
dockerfile: 'Dockerfile-alt',
|
||||||
|
}),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
const expectedResponseLines: string[] = [
|
const expectedResponseLines: string[] = [
|
||||||
...commonResponseLines[responseFilename],
|
...commonResponseLines[responseFilename],
|
||||||
@ -417,7 +428,7 @@ describe('balena build', function () {
|
|||||||
}
|
}
|
||||||
docker.expectGetInfo({});
|
docker.expectGetInfo({});
|
||||||
await testDockerBuildStream({
|
await testDockerBuildStream({
|
||||||
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -G -B BARG1=b1 -B barg2=B2 --cache-from my/img1,my/img2`,
|
commandLine: `build ${projectPath} --deviceType nuc --arch amd64 --convert-eol -G -B COMPOSE_ARG=A -B barg=b --cache-from my/img1,my/img2`,
|
||||||
dockerMock: docker,
|
dockerMock: docker,
|
||||||
expectedFilesByService,
|
expectedFilesByService,
|
||||||
expectedQueryParamsByService,
|
expectedQueryParamsByService,
|
||||||
@ -464,15 +475,19 @@ describe('balena build', function () {
|
|||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
const expectedQueryParamsByService = {
|
const expectedQueryParamsByService = {
|
||||||
service1: Object.entries({
|
service1: Object.entries(
|
||||||
...commonComposeQueryParams,
|
_.merge({}, commonComposeQueryParams, {
|
||||||
buildargs:
|
buildargs: { SERVICE1_VAR: 'This is a service specific variable' },
|
||||||
'{"MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable","SERVICE1_VAR":"This is a service specific variable"}',
|
}),
|
||||||
}),
|
),
|
||||||
service2: Object.entries({
|
service2: Object.entries(
|
||||||
...commonComposeQueryParams,
|
_.merge({}, commonComposeQueryParams, {
|
||||||
dockerfile: 'Dockerfile-alt',
|
buildargs: {
|
||||||
}),
|
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
|
||||||
|
},
|
||||||
|
dockerfile: 'Dockerfile-alt',
|
||||||
|
}),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
const expectedResponseLines: string[] = [
|
const expectedResponseLines: string[] = [
|
||||||
...commonResponseLines[responseFilename],
|
...commonResponseLines[responseFilename],
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
|
import * as _ from 'lodash';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
@ -53,14 +54,14 @@ const commonQueryParams = [
|
|||||||
['labels', ''],
|
['labels', ''],
|
||||||
];
|
];
|
||||||
|
|
||||||
const commonComposeQueryParams = [
|
const commonComposeQueryParams = {
|
||||||
['t', '${tag}'],
|
t: '${tag}',
|
||||||
[
|
buildargs: {
|
||||||
'buildargs',
|
MY_VAR_1: 'This is a variable',
|
||||||
'{"MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable"}',
|
MY_VAR_2: 'Also a variable',
|
||||||
],
|
},
|
||||||
['labels', ''],
|
labels: '',
|
||||||
];
|
};
|
||||||
|
|
||||||
const hr =
|
const hr =
|
||||||
'----------------------------------------------------------------------';
|
'----------------------------------------------------------------------';
|
||||||
@ -268,15 +269,19 @@ describe('balena deploy', function () {
|
|||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
const expectedQueryParamsByService = {
|
const expectedQueryParamsByService = {
|
||||||
service1: [
|
service1: Object.entries(
|
||||||
['t', '${tag}'],
|
_.merge({}, commonComposeQueryParams, {
|
||||||
[
|
buildargs: { SERVICE1_VAR: 'This is a service specific variable' },
|
||||||
'buildargs',
|
}),
|
||||||
'{"MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable","SERVICE1_VAR":"This is a service specific variable"}',
|
),
|
||||||
],
|
service2: Object.entries(
|
||||||
['labels', ''],
|
_.merge({}, commonComposeQueryParams, {
|
||||||
],
|
buildargs: {
|
||||||
service2: [...commonComposeQueryParams, ['dockerfile', 'Dockerfile-alt']],
|
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
|
||||||
|
},
|
||||||
|
dockerfile: 'Dockerfile-alt',
|
||||||
|
}),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
const expectedResponseLines: string[] = [
|
const expectedResponseLines: string[] = [
|
||||||
...commonResponseLines[responseFilename],
|
...commonResponseLines[responseFilename],
|
||||||
|
@ -465,7 +465,7 @@ describe('balena push', function () {
|
|||||||
const expectedFiles: ExpectedTarStreamFiles = {
|
const expectedFiles: ExpectedTarStreamFiles = {
|
||||||
'.balena/balena.yml': { fileSize: 197, type: 'file' },
|
'.balena/balena.yml': { fileSize: 197, type: 'file' },
|
||||||
'.dockerignore': { fileSize: 22, type: 'file' },
|
'.dockerignore': { fileSize: 22, type: 'file' },
|
||||||
'docker-compose.yml': { fileSize: 245, type: 'file' },
|
'docker-compose.yml': { fileSize: 332, type: 'file' },
|
||||||
'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
|
'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
|
||||||
'service1/file1.sh': { fileSize: 12, type: 'file' },
|
'service1/file1.sh': { fileSize: 12, type: 'file' },
|
||||||
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' },
|
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' },
|
||||||
@ -523,7 +523,7 @@ describe('balena push', function () {
|
|||||||
const expectedFiles: ExpectedTarStreamFiles = {
|
const expectedFiles: ExpectedTarStreamFiles = {
|
||||||
'.balena/balena.yml': { fileSize: 197, type: 'file' },
|
'.balena/balena.yml': { fileSize: 197, type: 'file' },
|
||||||
'.dockerignore': { fileSize: 22, type: 'file' },
|
'.dockerignore': { fileSize: 22, type: 'file' },
|
||||||
'docker-compose.yml': { fileSize: 245, type: 'file' },
|
'docker-compose.yml': { fileSize: 332, type: 'file' },
|
||||||
'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
|
'service1/Dockerfile.template': { fileSize: 144, type: 'file' },
|
||||||
'service1/file1.sh': { fileSize: 12, type: 'file' },
|
'service1/file1.sh': { fileSize: 12, type: 'file' },
|
||||||
'service1/test-ignore.txt': { fileSize: 12, type: 'file' },
|
'service1/test-ignore.txt': { fileSize: 12, type: 'file' },
|
||||||
|
@ -29,7 +29,12 @@ import { URL } from 'url';
|
|||||||
import { stripIndent } from '../lib/utils/lazy';
|
import { stripIndent } from '../lib/utils/lazy';
|
||||||
import { BuilderMock } from './builder-mock';
|
import { BuilderMock } from './builder-mock';
|
||||||
import { DockerMock } from './docker-mock';
|
import { DockerMock } from './docker-mock';
|
||||||
import { cleanOutput, fillTemplateArray, runCommand } from './helpers';
|
import {
|
||||||
|
cleanOutput,
|
||||||
|
deepJsonParse,
|
||||||
|
deepTemplateReplace,
|
||||||
|
runCommand,
|
||||||
|
} from './helpers';
|
||||||
import {
|
import {
|
||||||
ExpectedTarStreamFile,
|
ExpectedTarStreamFile,
|
||||||
ExpectedTarStreamFiles,
|
ExpectedTarStreamFiles,
|
||||||
@ -152,7 +157,7 @@ export async function testDockerBuildStream(o: {
|
|||||||
commandLine: string;
|
commandLine: string;
|
||||||
dockerMock: DockerMock;
|
dockerMock: DockerMock;
|
||||||
expectedFilesByService: ExpectedTarStreamFilesByService;
|
expectedFilesByService: ExpectedTarStreamFilesByService;
|
||||||
expectedQueryParamsByService: { [service: string]: string[][] };
|
expectedQueryParamsByService: { [service: string]: any[][] };
|
||||||
expectedErrorLines?: string[];
|
expectedErrorLines?: string[];
|
||||||
expectedExitCode?: number;
|
expectedExitCode?: number;
|
||||||
expectedResponseLines: string[];
|
expectedResponseLines: string[];
|
||||||
@ -161,15 +166,15 @@ export async function testDockerBuildStream(o: {
|
|||||||
responseBody: string;
|
responseBody: string;
|
||||||
services: string[]; // e.g. ['main'] or ['service1', 'service2']
|
services: string[]; // e.g. ['main'] or ['service1', 'service2']
|
||||||
}) {
|
}) {
|
||||||
const expectedErrorLines = fillTemplateArray(o.expectedErrorLines || [], o);
|
const expectedErrorLines = deepTemplateReplace(o.expectedErrorLines || [], o);
|
||||||
const expectedResponseLines = fillTemplateArray(o.expectedResponseLines, o);
|
const expectedResponseLines = deepTemplateReplace(o.expectedResponseLines, o);
|
||||||
|
|
||||||
for (const service of o.services) {
|
for (const service of o.services) {
|
||||||
// tagPrefix is, for example, 'myApp' if the path is 'path/to/myApp'
|
// tagPrefix is, for example, 'myApp' if the path is 'path/to/myApp'
|
||||||
const tagPrefix = o.projectPath.split(path.sep).pop();
|
const tagPrefix = o.projectPath.split(path.sep).pop();
|
||||||
const tag = `${tagPrefix}_${service}`;
|
const tag = `${tagPrefix}_${service}`;
|
||||||
const expectedFiles = o.expectedFilesByService[service];
|
const expectedFiles = o.expectedFilesByService[service];
|
||||||
const expectedQueryParams = fillTemplateArray(
|
const expectedQueryParams = deepTemplateReplace(
|
||||||
o.expectedQueryParamsByService[service],
|
o.expectedQueryParamsByService[service],
|
||||||
{ tag, ...o },
|
{ tag, ...o },
|
||||||
);
|
);
|
||||||
@ -181,7 +186,9 @@ export async function testDockerBuildStream(o: {
|
|||||||
checkURI: async (uri: string) => {
|
checkURI: async (uri: string) => {
|
||||||
const url = new URL(uri, 'http://test.net/');
|
const url = new URL(uri, 'http://test.net/');
|
||||||
const queryParams = Array.from(url.searchParams.entries());
|
const queryParams = Array.from(url.searchParams.entries());
|
||||||
expect(queryParams).to.have.deep.members(expectedQueryParams);
|
expect(deepJsonParse(queryParams)).to.have.deep.members(
|
||||||
|
deepJsonParse(expectedQueryParams),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
checkBuildRequestBody: (buildRequestBody: string) =>
|
checkBuildRequestBody: (buildRequestBody: string) =>
|
||||||
inspectTarStream(buildRequestBody, expectedFiles, projectPath),
|
inspectTarStream(buildRequestBody, expectedFiles, projectPath),
|
||||||
@ -226,15 +233,17 @@ export async function testPushBuildStream(o: {
|
|||||||
responseCode: number;
|
responseCode: number;
|
||||||
responseBody: string;
|
responseBody: string;
|
||||||
}) {
|
}) {
|
||||||
const expectedQueryParams = fillTemplateArray(o.expectedQueryParams, o);
|
const expectedQueryParams = deepTemplateReplace(o.expectedQueryParams, o);
|
||||||
const expectedResponseLines = fillTemplateArray(o.expectedResponseLines, o);
|
const expectedResponseLines = deepTemplateReplace(o.expectedResponseLines, o);
|
||||||
|
|
||||||
o.builderMock.expectPostBuild({
|
o.builderMock.expectPostBuild({
|
||||||
...o,
|
...o,
|
||||||
checkURI: async (uri: string) => {
|
checkURI: async (uri: string) => {
|
||||||
const url = new URL(uri, 'http://test.net/');
|
const url = new URL(uri, 'http://test.net/');
|
||||||
const queryParams = Array.from(url.searchParams.entries());
|
const queryParams = Array.from(url.searchParams.entries());
|
||||||
expect(queryParams).to.have.deep.members(expectedQueryParams);
|
expect(deepJsonParse(queryParams)).to.have.deep.members(
|
||||||
|
deepJsonParse(expectedQueryParams),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
checkBuildRequestBody: (buildRequestBody) =>
|
checkBuildRequestBody: (buildRequestBody) =>
|
||||||
inspectTarStream(buildRequestBody, o.expectedFiles, o.projectPath),
|
inspectTarStream(buildRequestBody, o.expectedFiles, o.projectPath),
|
||||||
|
@ -47,6 +47,7 @@ function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
|
|||||||
// TODO stop this warning message from appearing when running
|
// TODO stop this warning message from appearing when running
|
||||||
// sdk.setSharedOptions multiple times in the same process
|
// sdk.setSharedOptions multiple times in the same process
|
||||||
!line.startsWith('Shared SDK options') &&
|
!line.startsWith('Shared SDK options') &&
|
||||||
|
!line.startsWith('WARN: disabling Sentry.io error reporting') &&
|
||||||
// Node 12: '[DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated'
|
// Node 12: '[DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated'
|
||||||
!line.includes('[DEP0066]'),
|
!line.includes('[DEP0066]'),
|
||||||
),
|
),
|
||||||
@ -264,23 +265,55 @@ export function fillTemplate(
|
|||||||
return unescaped;
|
return unescaped;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fillTemplateArray(
|
/**
|
||||||
templateStringArray: string[],
|
* Recursively navigate the `data` argument (if it is an array or object),
|
||||||
templateVars: object,
|
* finding and replacing "template strings" such as 'hello ${name}!' with
|
||||||
): string[];
|
* the variable values given in `templateVars` such as { name: 'world' }.
|
||||||
export function fillTemplateArray(
|
*
|
||||||
templateStringArray: Array<string | string[]>,
|
* @param data Any data type (array, object, string) containing template
|
||||||
templateVars: object,
|
* strings to be replaced
|
||||||
): Array<string | string[]>;
|
* @param templateVars Map of template variable names to values
|
||||||
export function fillTemplateArray(
|
*/
|
||||||
templateStringArray: Array<string | string[]>,
|
export function deepTemplateReplace(
|
||||||
templateVars: object,
|
data: any,
|
||||||
): Array<string | string[]> {
|
templateVars: { [key: string]: any },
|
||||||
return templateStringArray.map((i) =>
|
): any {
|
||||||
Array.isArray(i)
|
switch (typeof data) {
|
||||||
? fillTemplateArray(i, templateVars)
|
case 'string':
|
||||||
: fillTemplate(i, templateVars),
|
return fillTemplate(data, templateVars);
|
||||||
);
|
case 'object':
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data.map((i) => deepTemplateReplace(i, templateVars));
|
||||||
|
}
|
||||||
|
return _.mapValues(data, (value) =>
|
||||||
|
deepTemplateReplace(value, templateVars),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
// number, undefined, null, or something else
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fillTemplateArray = deepTemplateReplace;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively navigate the `data` argument (if it is an array or object),
|
||||||
|
* looking for strings that start with `[` or `{` which are assumed to contain
|
||||||
|
* JSON arrays or objects that are then parsed with JSON.parse().
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function deepJsonParse(data: any): any {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
const maybeJson = data.trim();
|
||||||
|
if (maybeJson.startsWith('{') || maybeJson.startsWith('[')) {
|
||||||
|
return JSON.parse(maybeJson);
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
return data.map((i) => deepJsonParse(i));
|
||||||
|
} else if (typeof data === 'object') {
|
||||||
|
return _.mapValues(data, (value) => deepJsonParse(value));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function switchSentry(
|
export async function switchSentry(
|
||||||
|
@ -12,3 +12,5 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./service2
|
context: ./service2
|
||||||
dockerfile: Dockerfile-alt
|
dockerfile: Dockerfile-alt
|
||||||
|
args:
|
||||||
|
- 'COMPOSE_ARG=an argument defined in the docker-compose.yml file'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user