build/deploy: Add more test cases (--buildArg option)

Change-type: patch
This commit is contained in:
Paulo Castro 2020-10-20 00:00:53 +01:00
parent 099d755900
commit 6b208ec2ab
6 changed files with 134 additions and 70 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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