mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-19 05:37:51 +00:00
f9743b269a
Change-type: patch Signed-off-by: Paulo Castro <paulo@balena.io>
215 lines
6.2 KiB
TypeScript
215 lines
6.2 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2019-2020 Balena Ltd.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
// tslint:disable-next-line:no-var-requires
|
|
require('./config-tests'); // required for side effects
|
|
|
|
import { stripIndent } from 'common-tags';
|
|
import intercept = require('intercept-stdout');
|
|
import * as _ from 'lodash';
|
|
import { fs } from 'mz';
|
|
import * as nock from 'nock';
|
|
import * as path from 'path';
|
|
import { PathUtils } from 'resin-multibuild';
|
|
import { Readable } from 'stream';
|
|
import * as tar from 'tar-stream';
|
|
import { streamToBuffer } from 'tar-utils';
|
|
|
|
import * as balenaCLI from '../build/app';
|
|
|
|
export const runCommand = async (cmd: string) => {
|
|
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];
|
|
|
|
const err: string[] = [];
|
|
const out: string[] = [];
|
|
|
|
const stdoutHook = (log: string | Buffer) => {
|
|
// Skip over debug messages
|
|
if (typeof log === 'string' && !log.startsWith('[debug]')) {
|
|
out.push(log);
|
|
}
|
|
};
|
|
const stderrHook = (log: string | Buffer) => {
|
|
// Skip over debug messages
|
|
if (
|
|
typeof log === 'string' &&
|
|
!log.match(/\[debug\]/i) &&
|
|
// TODO stop this warning message from appearing when running
|
|
// sdk.setSharedOptions multiple times in the same process
|
|
!log.startsWith('Shared SDK options') &&
|
|
// Node 12: '[DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated'
|
|
!log.includes('[DEP0066]')
|
|
) {
|
|
err.push(log);
|
|
}
|
|
};
|
|
const unhookIntercept = intercept(stdoutHook, stderrHook);
|
|
|
|
try {
|
|
await balenaCLI.run(preArgs.concat(cmd.split(' ')), {
|
|
noFlush: true,
|
|
});
|
|
return {
|
|
err,
|
|
out,
|
|
};
|
|
} finally {
|
|
unhookIntercept();
|
|
}
|
|
};
|
|
|
|
export const balenaAPIMock = () => {
|
|
if (!nock.isActive()) {
|
|
nock.activate();
|
|
}
|
|
|
|
return nock(/./)
|
|
.get('/config/vars')
|
|
.reply(200, {
|
|
reservedNames: [],
|
|
reservedNamespaces: [],
|
|
invalidRegex: '/^d|W/',
|
|
whiteListedNames: [],
|
|
whiteListedNamespaces: [],
|
|
blackListedNames: [],
|
|
configVarSchema: [],
|
|
});
|
|
};
|
|
|
|
export function cleanOutput(output: string[] | string): string[] {
|
|
return _(_.castArray(output))
|
|
.map((log: string) => {
|
|
return log.split('\n').map(line => {
|
|
return monochrome(line.trim());
|
|
});
|
|
})
|
|
.flatten()
|
|
.compact()
|
|
.value();
|
|
}
|
|
|
|
/**
|
|
* Remove text colors (ASCII escape sequences). Example:
|
|
* Input: '\u001b[2K\r\u001b[34m[Build]\u001b[39m \u001b[1mmain\u001b[22m Image size: 1.14 MB'
|
|
* Output: '[Build] main Image size: 1.14 MB'
|
|
*
|
|
* TODO: check this function against a spec (ASCII escape sequences). It was
|
|
* coded from observation of a few samples only, and may not cover all cases.
|
|
*/
|
|
export function monochrome(text: string): string {
|
|
return text.replace(/\u001b\[\??\d+?[a-zA-Z]\r?/g, '');
|
|
}
|
|
|
|
export interface TarStreamFiles {
|
|
[filePath: string]: {
|
|
fileSize: number;
|
|
type: tar.Headers['type'];
|
|
testStream?: (header: tar.Headers, stream: Readable) => Promise<void>;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Run a few chai.expect() test assertions on a tar stream/buffer produced by
|
|
* the balena push, build and deploy commands, intercepted at HTTP level on
|
|
* their way from the CLI to the Docker daemon or balenaCloud builders.
|
|
*
|
|
* @param tarRequestBody Intercepted buffer of tar stream to be sent to builders/Docker
|
|
* @param expectedFiles Details of files expected to be found in the buffer
|
|
* @param projectPath Path of test project that was tarred, to compare file contents
|
|
* @param expect chai.expect function
|
|
*/
|
|
export async function inspectTarStream(
|
|
tarRequestBody: string | Buffer,
|
|
expectedFiles: TarStreamFiles,
|
|
projectPath: string,
|
|
expect: Chai.ExpectStatic,
|
|
): Promise<void> {
|
|
// string to stream: https://stackoverflow.com/a/22085851
|
|
const sourceTarStream = new Readable();
|
|
sourceTarStream._read = () => undefined;
|
|
sourceTarStream.push(tarRequestBody);
|
|
sourceTarStream.push(null);
|
|
|
|
const found: TarStreamFiles = await new Promise((resolve, reject) => {
|
|
const foundFiles: TarStreamFiles = {};
|
|
const extract = tar.extract();
|
|
extract.on('error', reject);
|
|
extract.on(
|
|
'entry',
|
|
async (header: tar.Headers, stream: Readable, next: tar.Callback) => {
|
|
try {
|
|
// TODO: test the .balena folder instead of ignoring it
|
|
if (header.name.startsWith('.balena/')) {
|
|
stream.resume();
|
|
} else {
|
|
expect(foundFiles).to.not.have.property(header.name);
|
|
foundFiles[header.name] = {
|
|
fileSize: header.size || 0,
|
|
type: header.type,
|
|
};
|
|
const expected = expectedFiles[header.name];
|
|
if (expected && expected.testStream) {
|
|
await expected.testStream(header, stream);
|
|
} else {
|
|
await defaultTestStream(header, stream, projectPath, expect);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
next();
|
|
},
|
|
);
|
|
extract.once('finish', () => {
|
|
resolve(foundFiles);
|
|
});
|
|
sourceTarStream.on('error', reject);
|
|
sourceTarStream.pipe(extract);
|
|
});
|
|
|
|
expect(found).to.deep.equal(
|
|
_.mapValues(expectedFiles, v => _.omit(v, 'testStream')),
|
|
);
|
|
}
|
|
|
|
/** Check that a tar stream entry matches the project contents in the filesystem */
|
|
async function defaultTestStream(
|
|
header: tar.Headers,
|
|
stream: Readable,
|
|
projectPath: string,
|
|
expect: Chai.ExpectStatic,
|
|
): Promise<void> {
|
|
const [buf, buf2] = await Promise.all([
|
|
streamToBuffer(stream),
|
|
fs.readFile(path.join(projectPath, PathUtils.toNativePath(header.name))),
|
|
]);
|
|
const msg = stripIndent`
|
|
contents mismatch for tar stream entry "${header.name}"
|
|
stream length=${buf.length}, filesystem length=${buf2.length}`;
|
|
expect(buf.equals(buf2), msg).to.be.true;
|
|
}
|
|
|
|
/** Test a tar stream entry for the absence of Windows CRLF line breaks */
|
|
export async function expectStreamNoCRLF(
|
|
_header: tar.Headers,
|
|
stream: Readable,
|
|
): Promise<void> {
|
|
const chai = await import('chai');
|
|
const buf = await streamToBuffer(stream);
|
|
await chai.expect(buf.includes('\r\n')).to.be.false;
|
|
}
|