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 * as _ from 'lodash';
import mock = require('mock-require');
import { promises as fs } from 'fs';
import * as path from 'path';
@ -45,13 +46,16 @@ const commonResponseLines: { [key: string]: string[] } = {
const commonQueryParams = {
t: '${tag}',
buildargs: '{}',
buildargs: {},
labels: '',
};
const commonComposeQueryParams = {
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: '',
};
@ -375,19 +379,26 @@ describe('balena build', function () {
'utf8',
);
const expectedQueryParamsByService = {
service1: Object.entries({
...commonComposeQueryParams,
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"}',
cachefrom: '["my/img1","my/img2"]',
}),
service2: Object.entries({
...commonComposeQueryParams,
buildargs:
'{"BARG1":"b1","barg2":"B2","MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable"}',
cachefrom: '["my/img1","my/img2"]',
dockerfile: 'Dockerfile-alt',
}),
service1: Object.entries(
_.merge({}, commonComposeQueryParams, {
buildargs: {
COMPOSE_ARG: 'A',
barg: 'b',
SERVICE1_VAR: 'This is a service specific variable',
},
cachefrom: ['my/img1', 'my/img2'],
}),
),
service2: Object.entries(
_.merge({}, commonComposeQueryParams, {
buildargs: {
COMPOSE_ARG: 'A',
barg: 'b',
},
cachefrom: ['my/img1', 'my/img2'],
dockerfile: 'Dockerfile-alt',
}),
),
};
const expectedResponseLines: string[] = [
...commonResponseLines[responseFilename],
@ -417,7 +428,7 @@ describe('balena build', function () {
}
docker.expectGetInfo({});
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,
expectedFilesByService,
expectedQueryParamsByService,
@ -464,15 +475,19 @@ describe('balena build', function () {
'utf8',
);
const expectedQueryParamsByService = {
service1: Object.entries({
...commonComposeQueryParams,
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({
...commonComposeQueryParams,
dockerfile: 'Dockerfile-alt',
}),
service1: Object.entries(
_.merge({}, commonComposeQueryParams, {
buildargs: { SERVICE1_VAR: 'This is a service specific variable' },
}),
),
service2: Object.entries(
_.merge({}, commonComposeQueryParams, {
buildargs: {
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
},
dockerfile: 'Dockerfile-alt',
}),
),
};
const expectedResponseLines: string[] = [
...commonResponseLines[responseFilename],

View File

@ -17,6 +17,7 @@
import { expect } from 'chai';
import { promises as fs } from 'fs';
import * as _ from 'lodash';
import * as path from 'path';
import * as sinon from 'sinon';
@ -53,14 +54,14 @@ const commonQueryParams = [
['labels', ''],
];
const commonComposeQueryParams = [
['t', '${tag}'],
[
'buildargs',
'{"MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable"}',
],
['labels', ''],
];
const commonComposeQueryParams = {
t: '${tag}',
buildargs: {
MY_VAR_1: 'This is a variable',
MY_VAR_2: 'Also a variable',
},
labels: '',
};
const hr =
'----------------------------------------------------------------------';
@ -268,15 +269,19 @@ describe('balena deploy', function () {
'utf8',
);
const expectedQueryParamsByService = {
service1: [
['t', '${tag}'],
[
'buildargs',
'{"MY_VAR_1":"This is a variable","MY_VAR_2":"Also a variable","SERVICE1_VAR":"This is a service specific variable"}',
],
['labels', ''],
],
service2: [...commonComposeQueryParams, ['dockerfile', 'Dockerfile-alt']],
service1: Object.entries(
_.merge({}, commonComposeQueryParams, {
buildargs: { SERVICE1_VAR: 'This is a service specific variable' },
}),
),
service2: Object.entries(
_.merge({}, commonComposeQueryParams, {
buildargs: {
COMPOSE_ARG: 'an argument defined in the docker-compose.yml file',
},
dockerfile: 'Dockerfile-alt',
}),
),
};
const expectedResponseLines: string[] = [
...commonResponseLines[responseFilename],

View File

@ -465,7 +465,7 @@ describe('balena push', function () {
const expectedFiles: ExpectedTarStreamFiles = {
'.balena/balena.yml': { fileSize: 197, 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/file1.sh': { fileSize: 12, type: 'file' },
'service2/Dockerfile-alt': { fileSize: 40, type: 'file' },
@ -523,7 +523,7 @@ describe('balena push', function () {
const expectedFiles: ExpectedTarStreamFiles = {
'.balena/balena.yml': { fileSize: 197, 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/file1.sh': { 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 { BuilderMock } from './builder-mock';
import { DockerMock } from './docker-mock';
import { cleanOutput, fillTemplateArray, runCommand } from './helpers';
import {
cleanOutput,
deepJsonParse,
deepTemplateReplace,
runCommand,
} from './helpers';
import {
ExpectedTarStreamFile,
ExpectedTarStreamFiles,
@ -152,7 +157,7 @@ export async function testDockerBuildStream(o: {
commandLine: string;
dockerMock: DockerMock;
expectedFilesByService: ExpectedTarStreamFilesByService;
expectedQueryParamsByService: { [service: string]: string[][] };
expectedQueryParamsByService: { [service: string]: any[][] };
expectedErrorLines?: string[];
expectedExitCode?: number;
expectedResponseLines: string[];
@ -161,15 +166,15 @@ export async function testDockerBuildStream(o: {
responseBody: string;
services: string[]; // e.g. ['main'] or ['service1', 'service2']
}) {
const expectedErrorLines = fillTemplateArray(o.expectedErrorLines || [], o);
const expectedResponseLines = fillTemplateArray(o.expectedResponseLines, o);
const expectedErrorLines = deepTemplateReplace(o.expectedErrorLines || [], o);
const expectedResponseLines = deepTemplateReplace(o.expectedResponseLines, o);
for (const service of o.services) {
// tagPrefix is, for example, 'myApp' if the path is 'path/to/myApp'
const tagPrefix = o.projectPath.split(path.sep).pop();
const tag = `${tagPrefix}_${service}`;
const expectedFiles = o.expectedFilesByService[service];
const expectedQueryParams = fillTemplateArray(
const expectedQueryParams = deepTemplateReplace(
o.expectedQueryParamsByService[service],
{ tag, ...o },
);
@ -181,7 +186,9 @@ export async function testDockerBuildStream(o: {
checkURI: async (uri: string) => {
const url = new URL(uri, 'http://test.net/');
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) =>
inspectTarStream(buildRequestBody, expectedFiles, projectPath),
@ -226,15 +233,17 @@ export async function testPushBuildStream(o: {
responseCode: number;
responseBody: string;
}) {
const expectedQueryParams = fillTemplateArray(o.expectedQueryParams, o);
const expectedResponseLines = fillTemplateArray(o.expectedResponseLines, o);
const expectedQueryParams = deepTemplateReplace(o.expectedQueryParams, o);
const expectedResponseLines = deepTemplateReplace(o.expectedResponseLines, o);
o.builderMock.expectPostBuild({
...o,
checkURI: async (uri: string) => {
const url = new URL(uri, 'http://test.net/');
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) =>
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
// sdk.setSharedOptions multiple times in the same process
!line.startsWith('Shared SDK options') &&
!line.startsWith('WARN: disabling Sentry.io error reporting') &&
// Node 12: '[DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated'
!line.includes('[DEP0066]'),
),
@ -264,23 +265,55 @@ export function fillTemplate(
return unescaped;
}
export function fillTemplateArray(
templateStringArray: string[],
templateVars: object,
): string[];
export function fillTemplateArray(
templateStringArray: Array<string | string[]>,
templateVars: object,
): Array<string | string[]>;
export function fillTemplateArray(
templateStringArray: Array<string | string[]>,
templateVars: object,
): Array<string | string[]> {
return templateStringArray.map((i) =>
Array.isArray(i)
? fillTemplateArray(i, templateVars)
: fillTemplate(i, templateVars),
);
/**
* Recursively navigate the `data` argument (if it is an array or object),
* finding and replacing "template strings" such as 'hello ${name}!' with
* the variable values given in `templateVars` such as { name: 'world' }.
*
* @param data Any data type (array, object, string) containing template
* strings to be replaced
* @param templateVars Map of template variable names to values
*/
export function deepTemplateReplace(
data: any,
templateVars: { [key: string]: any },
): any {
switch (typeof data) {
case 'string':
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(

View File

@ -12,3 +12,5 @@ services:
build:
context: ./service2
dockerfile: Dockerfile-alt
args:
- 'COMPOSE_ARG=an argument defined in the docker-compose.yml file'