From 6b208ec2abde887ffd11cfdfb624382ea7bfc049 Mon Sep 17 00:00:00 2001 From: Paulo Castro Date: Tue, 20 Oct 2020 00:00:53 +0100 Subject: [PATCH] build/deploy: Add more test cases (--buildArg option) Change-type: patch --- tests/commands/build.spec.ts | 65 +++++++++++------- tests/commands/deploy.spec.ts | 39 ++++++----- tests/commands/push.spec.ts | 4 +- tests/docker-build.ts | 27 +++++--- tests/helpers.ts | 67 ++++++++++++++----- .../docker-compose/basic/docker-compose.yml | 2 + 6 files changed, 134 insertions(+), 70 deletions(-) diff --git a/tests/commands/build.spec.ts b/tests/commands/build.spec.ts index bfe5b413..05c74361 100644 --- a/tests/commands/build.spec.ts +++ b/tests/commands/build.spec.ts @@ -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], diff --git a/tests/commands/deploy.spec.ts b/tests/commands/deploy.spec.ts index 4da394c3..bbea817b 100644 --- a/tests/commands/deploy.spec.ts +++ b/tests/commands/deploy.spec.ts @@ -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], diff --git a/tests/commands/push.spec.ts b/tests/commands/push.spec.ts index 6b5a678d..148ae0ec 100644 --- a/tests/commands/push.spec.ts +++ b/tests/commands/push.spec.ts @@ -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' }, diff --git a/tests/docker-build.ts b/tests/docker-build.ts index 16fccb9a..c1a83dc2 100644 --- a/tests/docker-build.ts +++ b/tests/docker-build.ts @@ -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), diff --git a/tests/helpers.ts b/tests/helpers.ts index 26830a39..42ffd8f4 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -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, - templateVars: object, -): Array; -export function fillTemplateArray( - templateStringArray: Array, - templateVars: object, -): Array { - 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( diff --git a/tests/test-data/projects/docker-compose/basic/docker-compose.yml b/tests/test-data/projects/docker-compose/basic/docker-compose.yml index 6bb6d970..00e1f075 100644 --- a/tests/test-data/projects/docker-compose/basic/docker-compose.yml +++ b/tests/test-data/projects/docker-compose/basic/docker-compose.yml @@ -12,3 +12,5 @@ services: build: context: ./service2 dockerfile: Dockerfile-alt + args: + - 'COMPOSE_ARG=an argument defined in the docker-compose.yml file'