Add tests for push, deploy and build commands

Change-type: patch
Signed-off-by: Paulo Castro <paulo@balena.io>
This commit is contained in:
Paulo Castro 2020-01-15 19:41:47 +00:00
parent bbea58a9c8
commit cc5fe60a15
31 changed files with 1493 additions and 219 deletions

View File

@ -1,6 +1,6 @@
/** /**
* @license * @license
* Copyright 2019 Balena Ltd. * Copyright 2019-2020 Balena Ltd.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,59 +16,111 @@
*/ */
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as nock from 'nock'; import * as path from 'path';
export class BalenaAPIMock { import { NockMock, ScopeOpts } from './nock-mock';
public static basePathPattern = /api\.balena-cloud\.com/;
public readonly scope: nock.Scope;
// Expose `scope` as `expect` to allow for better semantics in tests
public readonly expect = this.scope;
// For debugging tests const apiResponsePath = path.normalize(
get unfulfilledCallCount(): number { path.join(__dirname, 'test-data', 'api-response'),
return this.scope.pendingMocks().length; );
}
const jHeader = { 'Content-Type': 'application/json' };
export class BalenaAPIMock extends NockMock {
constructor() { constructor() {
nock.cleanAll(); super('https://api.balena-cloud.com');
if (!nock.isActive()) {
nock.activate();
}
this.scope = nock(BalenaAPIMock.basePathPattern);
nock.emitter.on('no match', this.handleUnexpectedRequest);
} }
public done() { public expectGetApplication(opts: ScopeOpts = {}) {
// scope.done() will throw an error if there are expected api calls that have not happened. this.optGet(/^\/v5\/application($|[(?])/, opts).replyWithFile(
// So ensures that all expected calls have been made. 200,
this.scope.done(); path.join(apiResponsePath, 'application-GET-v5-expanded-app-type.json'),
// Remove 'no match' handler, for tests using nock without this module jHeader,
nock.emitter.removeListener('no match', this.handleUnexpectedRequest); );
// Restore unmocked behavior
nock.cleanAll();
nock.restore();
} }
public expectTestApp() { public expectGetMyApplication(opts: ScopeOpts = {}) {
this.scope this.optGet(/^\/v5\/my_application($|[(?])/, opts).reply(
.get(/^\/v\d+\/application($|\?)/) 200,
.reply(200, { d: [{ id: 1234567 }] }); JSON.parse(`{"d": [{
"user": [{ "username": "bob", "__metadata": {} }],
"id": 1301645,
"__metadata": { "uri": "/resin/my_application(@id)?@id=1301645" }}]}
`),
);
} }
public expectTestDevice( public expectGetAuth(opts: ScopeOpts = {}) {
fullUUID = 'f63fd7d7812c34c4c14ae023fdff05f5', this.optGet(/^\/auth\/v1\//, opts).reply(200, {
inaccessibleApp = false, // "token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlJZVFk6TlE3WDpKSDVCOlFFWFk6RkU2TjpLTlVVOklWNTI6TFFRQTo3UjRWOjJVUFI6Qk9ISjpDNklPIn0.eyJqdGkiOiI3ZTNlN2RmMS1iYjljLTQxZTMtOTlkMi00NjVlMjE4YzFmOWQiLCJuYmYiOjE1NzkxOTQ1MjgsImFjY2VzcyI6W3sibmFtZSI6InYyL2MwODljNDIxZmIyMzM2ZDA0NzUxNjZmYmYzZDBmOWZhIiwidHlwZSI6InJlcG9zaXRvcnkiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19LHsibmFtZSI6InYyLzljMDBjOTQxMzk0MmNkMTVjZmM5MTg5YzVkYWMzNTlkIiwidHlwZSI6InJlcG9zaXRvcnkiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19XSwiaWF0IjoxNTc5MTk0NTM4LCJleHAiOjE1NzkyMDg5MzgsImF1ZCI6InJlZ2lzdHJ5Mi5iYWxlbmEtY2xvdWQuY29tIiwiaXNzIjoiYXBpLmJhbGVuYS1jbG91ZC5jb20iLCJzdWIiOiJnaF9wYXVsb19jYXN0cm8ifQ.bRw5_lg-nT-c1V4RxIJjujfPuVewZTs0BRNENEw2-sk_6zepLs-sLl9DOSEHYBdi87EtyCiUB3Wqee6fvz2HyQ"
) { token: 'test',
});
}
public expectGetRelease(opts: ScopeOpts = {}) {
this.optGet(/^\/v5\/release($|[(?])/, opts).replyWithFile(
200,
path.join(apiResponsePath, 'release-GET-v5.json'),
jHeader,
);
}
public expectPatchRelease(opts: ScopeOpts = {}) {
this.optPatch(/^\/v5\/release($|[(?])/, opts).reply(200, 'OK');
}
public expectPostRelease(opts: ScopeOpts = {}) {
this.optPost(/^\/v5\/release($|[(?])/, opts).replyWithFile(
200,
path.join(apiResponsePath, 'release-POST-v5.json'),
jHeader,
);
}
public expectPatchImage(opts: ScopeOpts = {}) {
this.optPatch(/^\/v5\/image($|[(?])/, opts).reply(200, 'OK');
}
public expectPostImage(opts: ScopeOpts = {}) {
this.optPost(/^\/v5\/image($|[(?])/, opts).replyWithFile(
201,
path.join(apiResponsePath, 'image-POST-v5.json'),
jHeader,
);
}
public expectPostImageLabel(opts: ScopeOpts = {}) {
this.optPost(/^\/v5\/image_label($|[(?])/, opts).replyWithFile(
201,
path.join(apiResponsePath, 'image-label-POST-v5.json'),
jHeader,
);
}
public expectPostImageIsPartOfRelease(opts: ScopeOpts = {}) {
this.optPost(
/^\/v5\/image__is_part_of__release($|[(?])/,
opts,
).replyWithFile(
200,
path.join(apiResponsePath, 'image-is-part-of-release-POST-v5.json'),
jHeader,
);
}
public expectGetDevice(opts: {
fullUUID: string;
inaccessibleApp?: boolean;
optional?: boolean;
persist?: boolean;
}) {
const id = 7654321; const id = 7654321;
this.scope.get(/^\/v\d+\/device($|\?)/).reply(200, { this.optGet(/^\/v\d+\/device($|\?)/, opts).reply(200, {
d: [ d: [
{ {
id, id,
uuid: fullUUID, uuid: opts.fullUUID,
belongs_to__application: inaccessibleApp belongs_to__application: opts.inaccessibleApp
? [] ? []
: [{ app_name: 'test' }], : [{ app_name: 'test' }],
}, },
@ -76,10 +128,10 @@ export class BalenaAPIMock {
}); });
} }
public expectAppEnvVars() { public expectGetAppEnvVars(opts: ScopeOpts = {}) {
this.scope this.optGet(/^\/v\d+\/application_environment_variable($|\?)/, opts).reply(
.get(/^\/v\d+\/application_environment_variable($|\?)/) 200,
.reply(200, { {
d: [ d: [
{ {
id: 120101, id: 120101,
@ -92,11 +144,12 @@ export class BalenaAPIMock {
value: '22', value: '22',
}, },
], ],
}); },
);
} }
public expectAppConfigVars() { public expectGetAppConfigVars(opts: ScopeOpts = {}) {
this.scope.get(/^\/v\d+\/application_config_variable($|\?)/).reply(200, { this.optGet(/^\/v\d+\/application_config_variable($|\?)/, opts).reply(200, {
d: [ d: [
{ {
id: 120300, id: 120300,
@ -107,10 +160,9 @@ export class BalenaAPIMock {
}); });
} }
public expectAppServiceVars() { public expectGetAppServiceVars(opts: ScopeOpts = {}) {
this.scope this.optGet(/^\/v\d+\/service_environment_variable($|\?)/, opts).reply(
.get(/^\/v\d+\/service_environment_variable($|\?)/) function(uri, _requestBody) {
.reply(function(uri, _requestBody) {
const match = uri.match(/service_name%20eq%20%27(.+?)%27/); const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
const serviceName = (match && match[1]) || undefined; const serviceName = (match && match[1]) || undefined;
let varArray: any[]; let varArray: any[];
@ -121,11 +173,12 @@ export class BalenaAPIMock {
varArray = _.map(appServiceVarsByService, value => value); varArray = _.map(appServiceVarsByService, value => value);
} }
return [200, { d: varArray }]; return [200, { d: varArray }];
}); },
);
} }
public expectDeviceEnvVars() { public expectGetDeviceEnvVars(opts: ScopeOpts = {}) {
this.scope.get(/^\/v\d+\/device_environment_variable($|\?)/).reply(200, { this.optGet(/^\/v\d+\/device_environment_variable($|\?)/, opts).reply(200, {
d: [ d: [
{ {
id: 120203, id: 120203,
@ -141,8 +194,8 @@ export class BalenaAPIMock {
}); });
} }
public expectDeviceConfigVars() { public expectGetDeviceConfigVars(opts: ScopeOpts = {}) {
this.scope.get(/^\/v\d+\/device_config_variable($|\?)/).reply(200, { this.optGet(/^\/v\d+\/device_config_variable($|\?)/, opts).reply(200, {
d: [ d: [
{ {
id: 120400, id: 120400,
@ -153,25 +206,34 @@ export class BalenaAPIMock {
}); });
} }
public expectDeviceServiceVars() { public expectGetDeviceServiceVars(opts: ScopeOpts = {}) {
this.scope this.optGet(
.get(/^\/v\d+\/device_service_environment_variable($|\?)/) /^\/v\d+\/device_service_environment_variable($|\?)/,
.reply(function(uri, _requestBody) { opts,
const match = uri.match(/service_name%20eq%20%27(.+?)%27/); ).reply(function(uri, _requestBody) {
const serviceName = (match && match[1]) || undefined; const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
let varArray: any[]; const serviceName = (match && match[1]) || undefined;
if (serviceName) { let varArray: any[];
const varObj = deviceServiceVarsByService[serviceName]; if (serviceName) {
varArray = varObj ? [varObj] : []; const varObj = deviceServiceVarsByService[serviceName];
} else { varArray = varObj ? [varObj] : [];
varArray = _.map(deviceServiceVarsByService, value => value); } else {
} varArray = _.map(deviceServiceVarsByService, value => value);
return [200, { d: varArray }]; }
}); return [200, { d: varArray }];
});
} }
public expectConfigVars() { public expectGetDeviceTypes(opts: ScopeOpts = {}) {
this.scope.get('/config/vars').reply(200, { this.optGet('/device-types/v1', opts).replyWithFile(
200,
path.join(apiResponsePath, 'device-types-GET-v1.json'),
jHeader,
);
}
public expectGetConfigVars(opts: ScopeOpts = {}) {
this.optGet('/config/vars', opts).reply(200, {
reservedNames: [], reservedNames: [],
reservedNamespaces: [], reservedNamespaces: [],
invalidRegex: '/^d|W/', invalidRegex: '/^d|W/',
@ -182,52 +244,53 @@ export class BalenaAPIMock {
}); });
} }
public expectService(serviceName: string, serviceId = 243768) { public expectGetService(opts: {
this.scope.get(/^\/v\d+\/service($|\?)/).reply(200, { optional?: boolean;
d: [{ id: serviceId, service_name: serviceName }], persist?: boolean;
serviceId?: number;
serviceName: string;
}) {
const serviceId = opts.serviceId || 243768;
this.optGet(/^\/v\d+\/service($|\?)/, opts).reply(200, {
d: [{ id: serviceId, service_name: opts.serviceName }],
});
}
public expectPostService404(opts: ScopeOpts = {}) {
this.optPost(/^\/v\d+\/service$/, opts).reply(
404,
'Unique key constraint violated',
);
}
public expectGetUser(opts: ScopeOpts = {}) {
this.optGet(/^\/v5\/user/, opts).reply(200, {
d: [
{
id: 99999,
actor: 1234567,
username: 'gh_user',
created_at: '2018-08-19T13:55:04.485Z',
__metadata: {
uri: '/resin/user(@id)?@id=43699',
},
},
],
}); });
} }
// User details are cached in the SDK // User details are cached in the SDK
// so often we don't know if we can expect the whoami request // so often we don't know if we can expect the whoami request
public expectWhoAmI(persist = false, optional = true) { public expectGetWhoAmI(opts: ScopeOpts = {}) {
const get = (persist ? this.scope.persist() : this.scope).get( this.optGet('/user/v1/whoami', opts).reply(200, {
'/user/v1/whoami',
);
(optional ? get.optionally() : get).reply(200, {
id: 99999, id: 99999,
username: 'testuser', username: 'gh_user',
email: 'testuser@test.com', email: 'testuser@test.com',
}); });
} }
public expectMixpanel(optional = true) { public expectGetMixpanel(opts: ScopeOpts = {}) {
const get = this.scope.get(/^\/mixpanel\/track/); this.optGet(/^\/mixpanel\/track/, opts).reply(200, {});
(optional ? get.optionally() : get).reply(200, {});
}
protected handleUnexpectedRequest(req: any) {
console.error(`Unexpected http request!: ${req.path}`);
// Errors thrown here are not causing the tests to fail for some reason.
// Possibly due to CLI global error handlers? (error.js)
// (Also, nock should automatically throw an error, but also not happening)
// For now, the console.error is sufficient (will fail the test)
}
public debug() {
const scope = this.scope;
let mocks = scope.pendingMocks();
console.error(`pending mocks ${mocks.length}: ${mocks}`);
this.scope.on('request', function(_req, _interceptor, _body) {
console.log(`>> REQUEST:` + _req.path);
mocks = scope.pendingMocks();
console.error(`pending mocks ${mocks.length}: ${mocks}`);
});
this.scope.on('replied', function(_req) {
console.log(`<< REPLIED:` + _req.path);
});
} }
} }

60
tests/builder-mock.ts Normal file
View File

@ -0,0 +1,60 @@
/**
* @license
* Copyright 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.
*/
import Bluebird = require('bluebird');
import * as _ from 'lodash';
import * as zlib from 'zlib';
import { NockMock } from './nock-mock';
export class BuilderMock extends NockMock {
constructor() {
super('https://builder.balena-cloud.com');
}
public expectPostBuild(opts: {
optional?: boolean;
persist?: boolean;
responseBody: any;
responseCode: number;
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
}) {
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(async function(
_uri,
requestBody,
callback,
) {
let error: Error | null = null;
try {
if (typeof requestBody === 'string') {
const gzipped = Buffer.from(requestBody, 'hex');
const gunzipped = await Bluebird.fromCallback<Buffer>(cb => {
zlib.gunzip(gzipped, cb);
});
await opts.checkBuildRequestBody(gunzipped);
} else {
throw new Error(
`unexpected requestBody type "${typeof requestBody}"`,
);
}
} catch (err) {
error = err;
}
callback(error, [opts.responseCode, opts.responseBody]);
});
}
}

View File

@ -1,3 +1,20 @@
/**
* @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.
*/
import { expect } from 'chai'; import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
@ -37,8 +54,8 @@ describe('balena app create', function() {
}); });
it('should print help text with the -h flag', async () => { it('should print help text with the -h flag', async () => {
api.expectWhoAmI(); api.expectGetWhoAmI({ optional: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('app create -h'); const { out, err } = await runCommand('app create -h');

View File

@ -0,0 +1,110 @@
/**
* @license
* Copyright 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.
*/
import { configureBluebird } from '../../build/app-common';
configureBluebird();
import { expect } from 'chai';
import { stripIndent } from 'common-tags';
import * as path from 'path';
import { BalenaAPIMock } from '../balena-api-mock';
import { DockerMock } from '../docker-mock';
import {
cleanOutput,
inspectTarStream,
runCommand,
TarStreamFiles,
} from '../helpers';
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
describe('balena build', function() {
let api: BalenaAPIMock;
let docker: DockerMock;
this.beforeEach(() => {
api = new BalenaAPIMock();
docker = new DockerMock();
api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true });
docker.expectGetPing();
docker.expectGetInfo();
docker.expectGetVersion();
docker.expectGetImages();
});
this.afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
docker.done();
});
it('should create the expected tar stream', async () => {
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
const expectedFiles: TarStreamFiles = {
'src/start.sh': { fileSize: 89, type: 'file' },
Dockerfile: { fileSize: 85, type: 'file' },
};
const responseBody = stripIndent`
{"stream":"Step 1/4 : FROM busybox"}
{"stream":"\\n"}
{"stream":" ---\\u003e 64f5d945efcc\\n"}
{"stream":"Step 2/4 : COPY ./src/start.sh /start.sh"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 97098fc9d757\\n"}
{"stream":"Step 3/4 : RUN chmod a+x /start.sh"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 33728e2e3f7e\\n"}
{"stream":"Step 4/4 : CMD [\\"/start.sh\\"]"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 2590e3b11eaf\\n"}
{"aux":{"ID":"sha256:2590e3b11eaf739491235016b53fec5d209c81837160abdd267c8fe5005ff1bd"}}
{"stream":"Successfully built 2590e3b11eaf\\n"}
{"stream":"Successfully tagged basic_main:latest\\n"}`;
docker.expectPostBuild({
tag: 'basic_main',
responseCode: 200,
responseBody,
checkBuildRequestBody: (buildRequestBody: string) =>
inspectTarStream(buildRequestBody, expectedFiles, projectPath, expect),
});
const { out, err } = await runCommand(
`build ${projectPath} --deviceType nuc --arch amd64`,
);
expect(err).to.have.members([]);
expect(
cleanOutput(out).map(line => line.replace(/\s{2,}/g, ' ')),
).to.include.members([
`[Info] Creating default composition with source: ${projectPath}`,
'[Info] Building for amd64/nuc',
'[Info] Docker Desktop detected (daemon architecture: "x86_64")',
'[Info] Docker itself will determine and enable architecture emulation if required,',
'[Info] without balena-cli intervention and regardless of the --emulated option.',
'[Build] main Image size: 1.14 MB',
'[Success] Build succeeded!',
]);
});
});

View File

@ -0,0 +1,131 @@
/**
* @license
* Copyright 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.
*/
import { configureBluebird } from '../../build/app-common';
configureBluebird();
import { expect } from 'chai';
import { stripIndent } from 'common-tags';
import * as path from 'path';
import { BalenaAPIMock } from '../balena-api-mock';
import { DockerMock } from '../docker-mock';
import {
cleanOutput,
inspectTarStream,
runCommand,
TarStreamFiles,
} from '../helpers';
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
describe('balena deploy', function() {
let api: BalenaAPIMock;
let docker: DockerMock;
this.beforeEach(() => {
api = new BalenaAPIMock();
docker = new DockerMock();
api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true });
api.expectGetDeviceTypes();
api.expectGetApplication();
api.expectPatchRelease();
api.expectPostRelease();
api.expectGetRelease();
api.expectGetUser();
api.expectGetService({ serviceName: 'main' });
api.expectPostService404();
api.expectGetAuth();
api.expectPostImage();
api.expectPostImageIsPartOfRelease();
api.expectPostImageLabel();
api.expectPatchImage();
docker.expectGetPing();
docker.expectGetInfo();
docker.expectGetVersion();
docker.expectGetImages({ persist: true });
docker.expectPostImagesTag();
docker.expectPostImagesPush();
docker.expectDeleteImages();
});
this.afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
docker.done();
});
it('should create the expected --build tar stream', async () => {
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
const expectedFiles: TarStreamFiles = {
'src/start.sh': { fileSize: 89, type: 'file' },
Dockerfile: { fileSize: 85, type: 'file' },
};
const responseBody = stripIndent`
{"stream":"Step 1/4 : FROM busybox"}
{"stream":"\\n"}
{"stream":" ---\\u003e 64f5d945efcc\\n"}
{"stream":"Step 2/4 : COPY ./src/start.sh /start.sh"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 97098fc9d757\\n"}
{"stream":"Step 3/4 : RUN chmod a+x /start.sh"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 33728e2e3f7e\\n"}
{"stream":"Step 4/4 : CMD [\\"/start.sh\\"]"}
{"stream":"\\n"}
{"stream":" ---\\u003e Using cache\\n"}
{"stream":" ---\\u003e 2590e3b11eaf\\n"}
{"aux":{"ID":"sha256:2590e3b11eaf739491235016b53fec5d209c81837160abdd267c8fe5005ff1bd"}}
{"stream":"Successfully built 2590e3b11eaf\\n"}
{"stream":"Successfully tagged basic_main:latest\\n"}`;
docker.expectPostBuild({
tag: 'basic_main',
responseCode: 200,
responseBody,
checkBuildRequestBody: (buildRequestBody: string) =>
inspectTarStream(buildRequestBody, expectedFiles, projectPath, expect),
});
const { out, err } = await runCommand(
`deploy testApp --build --source ${projectPath}`,
);
expect(err).to.have.members([]);
expect(
cleanOutput(out).map(line => line.replace(/\s{2,}/g, ' ')),
).to.include.members([
`[Info] Creating default composition with source: ${projectPath}`,
'[Info] Building for armv7hf/raspberrypi3',
'[Info] Docker Desktop detected (daemon architecture: "x86_64")',
'[Info] Docker itself will determine and enable architecture emulation if required,',
'[Info] without balena-cli intervention and regardless of the --emulated option.',
'[Build] main Image size: 1.14 MB',
'[Info] Creating release...',
'[Info] Pushing images to registry...',
'[Info] Saving release...',
'[Success] Deploy succeeded!',
'[Success] Release: 09f7c3e1fdec609be818002299edfc2a',
]);
});
});

View File

@ -1,3 +1,20 @@
/**
* @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.
*/
import { expect } from 'chai'; import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
@ -32,8 +49,8 @@ describe('balena device move', function() {
}); });
it('should print help text with the -h flag', async () => { it('should print help text with the -h flag', async () => {
api.expectWhoAmI(); api.expectGetWhoAmI({ optional: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('device move -h'); const { out, err } = await runCommand('device move -h');
@ -45,8 +62,8 @@ describe('balena device move', function() {
it.skip('should error if uuid not provided', async () => { it.skip('should error if uuid not provided', async () => {
// TODO: Figure out how to test for expected errors with current setup // TODO: Figure out how to test for expected errors with current setup
// including exit codes if possible. // including exit codes if possible.
api.expectWhoAmI(); api.expectGetWhoAmI({ optional: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('device move'); const { out, err } = await runCommand('device move');
const errLines = cleanOutput(err); const errLines = cleanOutput(err);

View File

@ -1,4 +1,23 @@
/**
* @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.
*/
import { expect } from 'chai'; import { expect } from 'chai';
import * as path from 'path';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
@ -12,6 +31,10 @@ Examples:
\t$ balena device 7cf02a6 \t$ balena device 7cf02a6
`; `;
const apiResponsePath = path.normalize(
path.join(__dirname, '..', '..', 'test-data', 'api-response'),
);
describe('balena device', function() { describe('balena device', function() {
let api: BalenaAPIMock; let api: BalenaAPIMock;
@ -25,8 +48,8 @@ describe('balena device', function() {
}); });
it('should print help text with the -h flag', async () => { it('should print help text with the -h flag', async () => {
api.expectWhoAmI(); api.expectGetWhoAmI({ optional: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('device -h'); const { out, err } = await runCommand('device -h');
@ -38,8 +61,8 @@ describe('balena device', function() {
it.skip('should error if uuid not provided', async () => { it.skip('should error if uuid not provided', async () => {
// TODO: Figure out how to test for expected errors with current setup // TODO: Figure out how to test for expected errors with current setup
// including exit codes if possible. // including exit codes if possible.
api.expectWhoAmI(); api.expectGetWhoAmI({ optional: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('device'); const { out, err } = await runCommand('device');
const errLines = cleanOutput(err); const errLines = cleanOutput(err);
@ -49,12 +72,12 @@ describe('balena device', function() {
}); });
it('should list device details for provided uuid', async () => { it('should list device details for provided uuid', async () => {
api.expectWhoAmI(); api.expectGetWhoAmI({ optional: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
api.scope api.scope
.get(/^\/v5\/device/) .get(/^\/v5\/device/)
.replyWithFile(200, __dirname + '/device.api-response.json', { .replyWithFile(200, path.join(apiResponsePath, 'device.json'), {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}); });
@ -72,14 +95,18 @@ describe('balena device', function() {
it('correctly handles devices with missing application', async () => { it('correctly handles devices with missing application', async () => {
// Devices with missing applications will have application name set to `N/a`. // Devices with missing applications will have application name set to `N/a`.
// e.g. When user has a device associated with app that user is no longer a collaborator of. // e.g. When user has a device associated with app that user is no longer a collaborator of.
api.expectWhoAmI(); api.expectGetWhoAmI({ optional: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
api.scope api.scope
.get(/^\/v5\/device/) .get(/^\/v5\/device/)
.replyWithFile(200, __dirname + '/device.api-response.missing-app.json', { .replyWithFile(
'Content-Type': 'application/json', 200,
}); path.join(apiResponsePath, 'device-missing-app.json'),
{
'Content-Type': 'application/json',
},
);
const { out, err } = await runCommand('device 27fda508c'); const { out, err } = await runCommand('device 27fda508c');

View File

@ -1,4 +1,23 @@
/**
* @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.
*/
import { expect } from 'chai'; import { expect } from 'chai';
import * as path from 'path';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
@ -21,6 +40,10 @@ Options:
--application, -a, --app <application> application name --application, -a, --app <application> application name
`; `;
const apiResponsePath = path.normalize(
path.join(__dirname, '..', '..', 'test-data', 'api-response'),
);
describe('balena devices', function() { describe('balena devices', function() {
let api: BalenaAPIMock; let api: BalenaAPIMock;
@ -34,8 +57,8 @@ describe('balena devices', function() {
}); });
it('should print help text with the -h flag', async () => { it('should print help text with the -h flag', async () => {
api.expectWhoAmI(); api.expectGetWhoAmI({ optional: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('devices -h'); const { out, err } = await runCommand('devices -h');
@ -45,14 +68,14 @@ describe('balena devices', function() {
}); });
it('should list devices from own and collaborator apps', async () => { it('should list devices from own and collaborator apps', async () => {
api.expectWhoAmI(); api.expectGetWhoAmI({ optional: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
api.scope api.scope
.get( .get(
'/v5/device?$orderby=device_name%20asc&$expand=belongs_to__application($select=app_name)', '/v5/device?$orderby=device_name%20asc&$expand=belongs_to__application($select=app_name)',
) )
.replyWithFile(200, __dirname + '/devices.api-response.json', { .replyWithFile(200, path.join(apiResponsePath, 'devices.json'), {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}); });

View File

@ -1,4 +1,22 @@
/**
* @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.
*/
import { expect } from 'chai'; import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
@ -15,8 +33,8 @@ describe('balena devices supported', function() {
}); });
it('should print help text with the -h flag', async () => { it('should print help text with the -h flag', async () => {
api.expectWhoAmI(); api.expectGetWhoAmI({ optional: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('devices supported -h'); const { out, err } = await runCommand('devices supported -h');
@ -26,15 +44,9 @@ describe('balena devices supported', function() {
}); });
it('should list currently supported devices, with correct filtering', async () => { it('should list currently supported devices, with correct filtering', async () => {
api.expectWhoAmI(); api.expectGetWhoAmI({ optional: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
api.expectGetDeviceTypes();
// TODO: Using the alias api.expect here causes route /config/vars to be called unexpectedly - why?
api.scope
.get('/device-types/v1')
.replyWithFile(200, __dirname + '/device-types.api-response.json', {
'Content-Type': 'application/json',
});
const { out, err } = await runCommand('devices supported'); const { out, err } = await runCommand('devices supported');

View File

@ -1,6 +1,6 @@
/** /**
* @license * @license
* Copyright 2019 Balena Ltd. * Copyright 2019-2020 Balena Ltd.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,8 +25,8 @@ describe('balena env add', function() {
beforeEach(() => { beforeEach(() => {
api = new BalenaAPIMock(); api = new BalenaAPIMock();
api.expectWhoAmI(true); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
}); });
afterEach(() => { afterEach(() => {
@ -35,14 +35,14 @@ describe('balena env add', function() {
}); });
it('should successfully add an environment variable', async () => { it('should successfully add an environment variable', async () => {
const deviceId = 'f63fd7d7812c34c4c14ae023fdff05f5'; const fullUUID = 'f63fd7d7812c34c4c14ae023fdff05f5';
api.expectTestDevice(); api.expectGetDevice({ fullUUID });
api.expectConfigVars(); api.expectGetConfigVars();
api.scope api.scope
.post(/^\/v\d+\/device_environment_variable($|\?)/) .post(/^\/v\d+\/device_environment_variable($|\?)/)
.reply(200, 'OK'); .reply(200, 'OK');
const { out, err } = await runCommand(`env add TEST 1 -d ${deviceId}`); const { out, err } = await runCommand(`env add TEST 1 -d ${fullUUID}`);
expect(out.join('')).to.equal(''); expect(out.join('')).to.equal('');
expect(err.join('')).to.equal(''); expect(err.join('')).to.equal('');

View File

@ -1,6 +1,6 @@
/** /**
* @license * @license
* Copyright 2019 Balena Ltd. * Copyright 2019-2020 Balena Ltd.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -29,8 +29,8 @@ describe('balena envs', function() {
beforeEach(() => { beforeEach(() => {
api = new BalenaAPIMock(); api = new BalenaAPIMock();
api.expectWhoAmI(true); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
// Random device UUID used to frustrate _.memoize() in utils/cloud.ts // Random device UUID used to frustrate _.memoize() in utils/cloud.ts
fullUUID = require('crypto') fullUUID = require('crypto')
.randomBytes(16) .randomBytes(16)
@ -44,8 +44,8 @@ describe('balena envs', function() {
}); });
it('should successfully list env vars for a test app', async () => { it('should successfully list env vars for a test app', async () => {
api.expectTestApp(); api.expectGetApplication();
api.expectAppEnvVars(); api.expectGetAppEnvVars();
const { out, err } = await runCommand(`envs -a ${appName}`); const { out, err } = await runCommand(`envs -a ${appName}`);
@ -60,8 +60,8 @@ describe('balena envs', function() {
}); });
it('should successfully list config vars for a test app', async () => { it('should successfully list config vars for a test app', async () => {
api.expectTestApp(); api.expectGetApplication();
api.expectAppConfigVars(); api.expectGetAppConfigVars();
const { out, err } = await runCommand(`envs -a ${appName} --config`); const { out, err } = await runCommand(`envs -a ${appName} --config`);
@ -75,8 +75,8 @@ describe('balena envs', function() {
}); });
it('should successfully list config vars for a test app (JSON output)', async () => { it('should successfully list config vars for a test app (JSON output)', async () => {
api.expectTestApp(); api.expectGetApplication();
api.expectAppConfigVars(); api.expectGetAppConfigVars();
const { out, err } = await runCommand(`envs -cja ${appName}`); const { out, err } = await runCommand(`envs -cja ${appName}`);
@ -92,9 +92,9 @@ describe('balena envs', function() {
it('should successfully list service variables for a test app (-s flag)', async () => { it('should successfully list service variables for a test app (-s flag)', async () => {
const serviceName = 'service2'; const serviceName = 'service2';
api.expectService(serviceName); api.expectGetService({ serviceName });
api.expectTestApp(); api.expectGetApplication();
api.expectAppServiceVars(); api.expectGetAppServiceVars();
const { out, err } = await runCommand( const { out, err } = await runCommand(
`envs -a ${appName} -s ${serviceName}`, `envs -a ${appName} -s ${serviceName}`,
@ -111,9 +111,9 @@ describe('balena envs', function() {
it('should produce an empty JSON array when no app service variables exist', async () => { it('should produce an empty JSON array when no app service variables exist', async () => {
const serviceName = 'nono'; const serviceName = 'nono';
api.expectService(serviceName); api.expectGetService({ serviceName });
api.expectTestApp(); api.expectGetApplication();
api.expectAppServiceVars(); api.expectGetAppServiceVars();
const { out, err } = await runCommand( const { out, err } = await runCommand(
`envs -a ${appName} -s ${serviceName} -j`, `envs -a ${appName} -s ${serviceName} -j`,
@ -124,9 +124,9 @@ describe('balena envs', function() {
}); });
it('should successfully list env and service vars for a test app (--all flag)', async () => { it('should successfully list env and service vars for a test app (--all flag)', async () => {
api.expectTestApp(); api.expectGetApplication();
api.expectAppEnvVars(); api.expectGetAppEnvVars();
api.expectAppServiceVars(); api.expectGetAppServiceVars();
const { out, err } = await runCommand(`envs -a ${appName} --all`); const { out, err } = await runCommand(`envs -a ${appName} --all`);
@ -144,10 +144,10 @@ describe('balena envs', function() {
it('should successfully list env and service vars for a test app (--all -s flags)', async () => { it('should successfully list env and service vars for a test app (--all -s flags)', async () => {
const serviceName = 'service1'; const serviceName = 'service1';
api.expectService(serviceName); api.expectGetService({ serviceName });
api.expectTestApp(); api.expectGetApplication();
api.expectAppEnvVars(); api.expectGetAppEnvVars();
api.expectAppServiceVars(); api.expectGetAppServiceVars();
const { out, err } = await runCommand( const { out, err } = await runCommand(
`envs -a ${appName} --all -s ${serviceName}`, `envs -a ${appName} --all -s ${serviceName}`,
@ -165,8 +165,8 @@ describe('balena envs', function() {
}); });
it('should successfully list env variables for a test device', async () => { it('should successfully list env variables for a test device', async () => {
api.expectTestDevice(fullUUID); api.expectGetDevice({ fullUUID });
api.expectDeviceEnvVars(); api.expectGetDeviceEnvVars();
const { out, err } = await runCommand(`envs -d ${shortUUID}`); const { out, err } = await runCommand(`envs -d ${shortUUID}`);
@ -181,8 +181,8 @@ describe('balena envs', function() {
}); });
it('should successfully list env variables for a test device (JSON output)', async () => { it('should successfully list env variables for a test device (JSON output)', async () => {
api.expectTestDevice(fullUUID); api.expectGetDevice({ fullUUID });
api.expectDeviceEnvVars(); api.expectGetDeviceEnvVars();
const { out, err } = await runCommand(`envs -jd ${shortUUID}`); const { out, err } = await runCommand(`envs -jd ${shortUUID}`);
@ -202,8 +202,8 @@ describe('balena envs', function() {
}); });
it('should successfully list config variables for a test device', async () => { it('should successfully list config variables for a test device', async () => {
api.expectTestDevice(fullUUID); api.expectGetDevice({ fullUUID });
api.expectDeviceConfigVars(); api.expectGetDeviceConfigVars();
const { out, err } = await runCommand(`envs -d ${shortUUID} --config`); const { out, err } = await runCommand(`envs -d ${shortUUID} --config`);
@ -218,10 +218,10 @@ describe('balena envs', function() {
it('should successfully list service variables for a test device (-s flag)', async () => { it('should successfully list service variables for a test device (-s flag)', async () => {
const serviceName = 'service2'; const serviceName = 'service2';
api.expectService(serviceName); api.expectGetService({ serviceName });
api.expectTestApp(); api.expectGetApplication();
api.expectTestDevice(fullUUID); api.expectGetDevice({ fullUUID });
api.expectDeviceServiceVars(); api.expectGetDeviceServiceVars();
const { out, err } = await runCommand( const { out, err } = await runCommand(
`envs -d ${shortUUID} -s ${serviceName}`, `envs -d ${shortUUID} -s ${serviceName}`,
@ -238,10 +238,10 @@ describe('balena envs', function() {
it('should produce an empty JSON array when no device service variables exist', async () => { it('should produce an empty JSON array when no device service variables exist', async () => {
const serviceName = 'nono'; const serviceName = 'nono';
api.expectService(serviceName); api.expectGetService({ serviceName });
api.expectTestApp(); api.expectGetApplication();
api.expectTestDevice(fullUUID); api.expectGetDevice({ fullUUID });
api.expectDeviceServiceVars(); api.expectGetDeviceServiceVars();
const { out, err } = await runCommand( const { out, err } = await runCommand(
`envs -d ${shortUUID} -s ${serviceName} -j`, `envs -d ${shortUUID} -s ${serviceName} -j`,
@ -252,12 +252,12 @@ describe('balena envs', function() {
}); });
it('should successfully list env and service variables for a test device (--all flag)', async () => { it('should successfully list env and service variables for a test device (--all flag)', async () => {
api.expectTestApp(); api.expectGetApplication();
api.expectAppEnvVars(); api.expectGetAppEnvVars();
api.expectAppServiceVars(); api.expectGetAppServiceVars();
api.expectTestDevice(fullUUID); api.expectGetDevice({ fullUUID });
api.expectDeviceEnvVars(); api.expectGetDeviceEnvVars();
api.expectDeviceServiceVars(); api.expectGetDeviceServiceVars();
const uuid = shortUUID; const uuid = shortUUID;
const { out, err } = await runCommand(`envs -d ${uuid} --all`); const { out, err } = await runCommand(`envs -d ${uuid} --all`);
@ -279,9 +279,9 @@ describe('balena envs', function() {
}); });
it('should successfully list env and service variables for a test device (unknown app)', async () => { it('should successfully list env and service variables for a test device (unknown app)', async () => {
api.expectTestDevice(fullUUID, true); api.expectGetDevice({ fullUUID, inaccessibleApp: true });
api.expectDeviceEnvVars(); api.expectGetDeviceEnvVars();
api.expectDeviceServiceVars(); api.expectGetDeviceServiceVars();
const uuid = shortUUID; const uuid = shortUUID;
const { out, err } = await runCommand(`envs -d ${uuid} --all`); const { out, err } = await runCommand(`envs -d ${uuid} --all`);
@ -300,13 +300,13 @@ describe('balena envs', function() {
it('should successfully list env and service vars for a test device (--all -s flags)', async () => { it('should successfully list env and service vars for a test device (--all -s flags)', async () => {
const serviceName = 'service1'; const serviceName = 'service1';
api.expectService(serviceName); api.expectGetService({ serviceName });
api.expectTestApp(); api.expectGetApplication();
api.expectAppEnvVars(); api.expectGetAppEnvVars();
api.expectAppServiceVars(); api.expectGetAppServiceVars();
api.expectTestDevice(fullUUID); api.expectGetDevice({ fullUUID });
api.expectDeviceEnvVars(); api.expectGetDeviceEnvVars();
api.expectDeviceServiceVars(); api.expectGetDeviceServiceVars();
const uuid = shortUUID; const uuid = shortUUID;
const { out, err } = await runCommand( const { out, err } = await runCommand(
@ -329,13 +329,13 @@ describe('balena envs', function() {
it('should successfully list env and service vars for a test device (--all -js flags)', async () => { it('should successfully list env and service vars for a test device (--all -js flags)', async () => {
const serviceName = 'service1'; const serviceName = 'service1';
api.expectService(serviceName); api.expectGetService({ serviceName });
api.expectTestApp(); api.expectGetApplication();
api.expectAppEnvVars(); api.expectGetAppEnvVars();
api.expectAppServiceVars(); api.expectGetAppServiceVars();
api.expectTestDevice(fullUUID); api.expectGetDevice({ fullUUID });
api.expectDeviceEnvVars(); api.expectGetDeviceEnvVars();
api.expectDeviceServiceVars(); api.expectGetDeviceServiceVars();
const { out, err } = await runCommand( const { out, err } = await runCommand(
`envs -d ${shortUUID} --all -js ${serviceName}`, `envs -d ${shortUUID} --all -js ${serviceName}`,

View File

@ -1,6 +1,6 @@
/** /**
* @license * @license
* Copyright 2016-2019 Balena Ltd. * Copyright 2019-2020 Balena Ltd.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,8 +25,8 @@ describe('balena env rename', function() {
beforeEach(() => { beforeEach(() => {
api = new BalenaAPIMock(); api = new BalenaAPIMock();
api.expectWhoAmI(true); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
}); });
afterEach(() => { afterEach(() => {

View File

@ -1,6 +1,6 @@
/** /**
* @license * @license
* Copyright 2016-2019 Balena Ltd. * Copyright 2019-2020 Balena Ltd.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,8 +25,8 @@ describe('balena env rm', function() {
beforeEach(() => { beforeEach(() => {
api = new BalenaAPIMock(); api = new BalenaAPIMock();
api.expectWhoAmI(true); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectMixpanel(); api.expectGetMixpanel({ optional: true });
}); });
afterEach(() => { afterEach(() => {

121
tests/commands/push.spec.ts Normal file
View File

@ -0,0 +1,121 @@
/**
* @license
* Copyright 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.
*/
import { configureBluebird } from '../../build/app-common';
configureBluebird();
import { expect } from 'chai';
import { fs } from 'mz';
import * as path from 'path';
import { BalenaAPIMock } from '../balena-api-mock';
import { BuilderMock } from '../builder-mock';
// import { DockerMock } from '../docker-mock';
import {
cleanOutput,
inspectTarStream,
runCommand,
TarStreamFiles,
} from '../helpers';
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
const builderResponsePath = path.normalize(
path.join(__dirname, '..', 'test-data', 'builder-response'),
);
describe('balena push', function() {
let api: BalenaAPIMock;
let builder: BuilderMock;
this.beforeEach(() => {
api = new BalenaAPIMock();
builder = new BuilderMock();
api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true });
api.expectGetMyApplication();
});
this.afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
builder.done();
});
it('should create the expected tar stream', async () => {
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
const expectedFiles: TarStreamFiles = {
'src/start.sh': { fileSize: 89, type: 'file' },
Dockerfile: { fileSize: 85, type: 'file' },
};
const responseBody = await fs.readFile(
path.join(builderResponsePath, 'build-POST-v3.json'),
'utf8',
);
builder.expectPostBuild({
responseCode: 200,
responseBody,
checkBuildRequestBody: (buildRequestBody: string | Buffer) =>
inspectTarStream(buildRequestBody, expectedFiles, projectPath, expect),
});
const { out, err } = await runCommand(
`push testApp --source ${projectPath}`,
);
expect(err).to.have.members([]);
expect(
cleanOutput(out).map(line =>
line
.replace(/\s{2,}/g, ' ')
.replace(/in \d+? seconds/, 'in 20 seconds'),
),
).to.include.members([
'[Info] Starting build for testApp, user gh_user',
'[Info] Dashboard link: https://dashboard.balena-cloud.com/apps/1301645/devices',
'[Info] Building on arm01',
'[Info] Pulling previous images for caching purposes...',
'[Success] Successfully pulled cache images',
'[main] Step 1/4 : FROM busybox',
'[main] ---> 76aea0766768',
'[main] Step 2/4 : COPY ./src/start.sh /start.sh',
'[main] ---> b563ad6a0801',
'[main] Step 3/4 : RUN chmod a+x /start.sh',
'[main] ---> Running in 10d4ddc40bfc',
'[main] Removing intermediate container 10d4ddc40bfc',
'[main] ---> 82e98871a32c',
'[main] Step 4/4 : CMD ["/start.sh"]',
'[main] ---> Running in 0682894e13eb',
'[main] Removing intermediate container 0682894e13eb',
'[main] ---> 889ccb6afc7c',
'[main] Successfully built 889ccb6afc7c',
'[Info] Uploading images',
'[Success] Successfully uploaded images',
'[Info] Built on arm01',
'[Success] Release successfully created!',
'[Info] Release: 05a24b5b034c9f95f25d4d74f0593bea (id: 1220245)',
'[Info] ┌─────────┬────────────┬────────────┐',
'[Info] │ Service │ Image Size │ Build Time │',
'[Info] ├─────────┼────────────┼────────────┤',
'[Info] │ main │ 1.32 MB │ 11 seconds │',
'[Info] └─────────┴────────────┴────────────┘',
'[Info] Build finished in 20 seconds',
]);
});
});

130
tests/docker-mock.ts Normal file
View File

@ -0,0 +1,130 @@
/**
* @license
* Copyright 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.
*/
import * as _ from 'lodash';
import * as path from 'path';
import { NockMock, ScopeOpts } from './nock-mock';
const dockerResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'docker-response'),
);
export class DockerMock extends NockMock {
constructor() {
super('http://localhost');
}
public expectGetPing(opts: ScopeOpts = {}) {
this.optGet('/_ping', opts).reply(200, 'OK');
}
public expectGetInfo(opts: ScopeOpts = {}) {
// this body is a partial copy from Docker for Mac v18.06.1-ce-mac73
const body = {
KernelVersion: '4.9.93-linuxkit-aufs',
OperatingSystem: 'Docker for Mac',
OSType: 'linux',
Architecture: 'x86_64',
};
this.optGet('/info', opts).reply(200, body);
}
public expectGetVersion(opts: ScopeOpts = {}) {
// this body is partial copy from Docker for Mac v18.06.1-ce-mac73
const body = {
Platform: {
Name: '',
},
Version: '18.06.1-ce',
ApiVersion: '1.38',
MinAPIVersion: '1.12',
GitCommit: 'e68fc7a',
GoVersion: 'go1.10.3',
Os: 'linux',
Arch: 'amd64',
KernelVersion: '4.9.93-linuxkit-aufs',
Experimental: true,
BuildTime: '2018-08-21T17:29:02.000000000+00:00',
};
this.optGet('/version', opts).reply(200, body);
}
public expectPostBuild(opts: {
optional?: boolean;
persist?: boolean;
responseBody: any;
responseCode: number;
tag: string;
checkBuildRequestBody: (requestBody: string) => Promise<void>;
}) {
this.optPost(
new RegExp(`^/build\\?t=${_.escapeRegExp(opts.tag)}&`),
opts,
).reply(async function(_uri, requestBody, cb) {
let error: Error | null = null;
try {
if (typeof requestBody === 'string') {
await opts.checkBuildRequestBody(requestBody);
} else {
throw new Error(
`unexpected requestBody type "${typeof requestBody}"`,
);
}
} catch (err) {
error = err;
}
cb(error, [opts.responseCode, opts.responseBody]);
});
}
public expectGetImages(opts: ScopeOpts = {}) {
// this body is partial copy from Docker for Mac v18.06.1-ce-mac73
const body = {
Size: 1199596,
};
this.optGet(/^\/images\//, opts).reply(200, body);
}
public expectDeleteImages(opts: ScopeOpts = {}) {
this.optDelete(/^\/images\//, opts).reply(200, [
{
Untagged:
'registry2.balena-cloud.com/v2/c089c421fb2336d0475166fbf3d0f9fa:latest',
},
{
Untagged:
'registry2.balena-cloud.com/v2/c089c421fb2336d0475166fbf3d0f9fa@sha256:444a5e0c57eed51f5e752b908cb95188c25a0476fc6e5f43e5113edfc4d07199',
},
]);
}
public expectPostImagesTag(opts: ScopeOpts = {}) {
this.optPost(/^\/images\/.+?\/tag\?/, opts).reply(201);
}
public expectPostImagesPush(opts: ScopeOpts = {}) {
this.optPost(/^\/images\/.+?\/push/, opts).replyWithFile(
200,
path.join(dockerResponsePath, 'images-push-POST.json'),
{
'api-version': '1.38',
'Content-Type': 'application/json',
},
);
}
}

View File

@ -17,8 +17,13 @@
import intercept = require('intercept-stdout'); import intercept = require('intercept-stdout');
import * as _ from 'lodash'; import * as _ from 'lodash';
import { fs } from 'mz';
import * as nock from 'nock'; import * as nock from 'nock';
import * as path from 'path'; 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'; import * as balenaCLI from '../build/app';
import { configureBluebird, setMaxListeners } from '../build/app-common'; import { configureBluebird, setMaxListeners } from '../build/app-common';
@ -44,7 +49,7 @@ export const runCommand = async (cmd: string) => {
// Skip over debug messages // Skip over debug messages
if ( if (
typeof log === 'string' && typeof log === 'string' &&
!log.startsWith('[debug]') && !log.match(/\[debug\]/i) &&
// 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
!log.startsWith('Shared SDK options') && !log.startsWith('Shared SDK options') &&
@ -87,14 +92,96 @@ export const balenaAPIMock = () => {
}); });
}; };
export function cleanOutput(output: string[] | string) { export function cleanOutput(output: string[] | string): string[] {
return _(_.castArray(output)) return _(_.castArray(output))
.map(log => { .map((log: string) => {
return log.split('\n').map(line => { return log.split('\n').map(line => {
return line.trim(); return monochrome(line.trim());
}); });
}) })
.flatten() .flatten()
.compact() .compact()
.value(); .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'];
};
}
/**
* 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 [buf, buf2] = await Promise.all([
streamToBuffer(stream),
fs.readFile(
path.join(projectPath, PathUtils.toNativePath(header.name)),
),
]);
expect(buf.equals(buf2)).to.be.true;
}
} catch (err) {
reject(err);
}
next();
},
);
extract.once('finish', () => {
resolve(foundFiles);
});
sourceTarStream.on('error', reject);
sourceTarStream.pipe(extract);
});
expect(found).to.deep.equal(expectedFiles);
}

152
tests/nock-mock.ts Normal file
View File

@ -0,0 +1,152 @@
/**
* @license
* Copyright 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.
*/
import { configureBluebird } from '../build/app-common';
configureBluebird();
import * as _ from 'lodash';
import * as nock from 'nock';
export interface ScopeOpts {
optional?: boolean;
persist?: boolean;
}
/**
* Base class for tests using nock to intercept HTTP requests.
* Subclasses include BalenaAPIMock, DockerMock and BuilderMock.
*/
export class NockMock {
public readonly scope: nock.Scope;
// Expose `scope` as `expect` to allow for better semantics in tests
public readonly expect = this.scope;
protected static instanceCount = 0;
constructor(public basePathPattern: string | RegExp) {
if (NockMock.instanceCount === 0) {
if (!nock.isActive()) {
nock.activate();
}
nock.emitter.on('no match', this.handleUnexpectedRequest);
} else if (process.env.DEBUG) {
console.error(
`[debug] NockMock.constructor() instance count is ${
NockMock.instanceCount
}`,
);
}
NockMock.instanceCount += 1;
this.scope = nock(this.basePathPattern);
}
public optGet(
uri: string | RegExp | ((uri: string) => boolean),
opts: ScopeOpts,
): nock.Interceptor {
opts = _.assign({ optional: false, persist: false }, opts);
const get = (opts.persist ? this.scope.persist() : this.scope).get(uri);
return opts.optional ? get.optionally() : get;
}
public optDelete(
uri: string | RegExp | ((uri: string) => boolean),
opts: ScopeOpts,
) {
opts = _.assign({ optional: false, persist: false }, opts);
const del = (opts.persist ? this.scope.persist() : this.scope).delete(uri);
return opts.optional ? del.optionally() : del;
}
public optPatch(
uri: string | RegExp | ((uri: string) => boolean),
opts: ScopeOpts,
) {
opts = _.assign({ optional: false, persist: false }, opts);
const patch = (opts.persist ? this.scope.persist() : this.scope).patch(uri);
return opts.optional ? patch.optionally() : patch;
}
public optPost(
uri: string | RegExp | ((uri: string) => boolean),
opts: ScopeOpts,
) {
opts = _.assign({ optional: false, persist: false }, opts);
const post = (opts.persist ? this.scope.persist() : this.scope).post(uri);
return opts.optional ? post.optionally() : post;
}
public done() {
try {
// scope.done() will throw an error if there are expected api calls that have not happened.
// So ensure that all expected calls have been made.
this.scope.done();
} finally {
const count = NockMock.instanceCount - 1;
if (count < 0 && process.env.DEBUG) {
console.error(
`[debug] Warning: NockMock.instanceCount is negative (${count})`,
);
}
NockMock.instanceCount = Math.max(0, count);
if (NockMock.instanceCount === 0) {
// Remove 'no match' handler, for tests using nock without this module
nock.emitter.removeAllListeners('no match');
nock.cleanAll();
nock.restore();
} else if (process.env.DEBUG) {
console.error(
`[debug] NockMock.done() instance count is ${NockMock.instanceCount}`,
);
}
}
}
protected handleUnexpectedRequest(req: any) {
const o = req.options || {};
const u = o.uri || {};
console.error(
`Unexpected http request!: ${req.method} ${o.proto ||
u.protocol}//${o.host || u.host}${req.path || o.path || u.path}`,
);
// Errors thrown here are not causing the tests to fail for some reason.
// Possibly due to CLI global error handlers? (error.js)
// (Also, nock should automatically throw an error, but also not happening)
// For now, the console.error is sufficient (will fail the test)
}
// For debugging tests
get unfulfilledCallCount(): number {
return this.scope.pendingMocks().length;
}
public debug() {
const scope = this.scope;
let mocks = scope.pendingMocks();
console.error(`pending mocks ${mocks.length}: ${mocks}`);
this.scope.on('request', function(_req, _interceptor, _body) {
console.log(`>> REQUEST:` + _req.path);
mocks = scope.pendingMocks();
console.error(`pending mocks ${mocks.length}: ${mocks}`);
});
this.scope.on('replied', function(_req) {
console.log(`<< REPLIED:` + _req.path);
});
}
}

View File

@ -0,0 +1,35 @@
{
"d": [
{
"application_type": [
{
"name": "Starter",
"slug": "microservices-starter",
"supports_multicontainer": true,
"is_legacy": false,
"__metadata": {}
}
],
"id": 1301645,
"user": {
"__deferred": {
"uri": "/resin/user(43699)"
},
"__id": 43699
},
"depends_on__application": null,
"actor": 3423895,
"app_name": "testApp",
"slug": "gh_user/testApp",
"commit": "96eec431d57e6976d3a756df33fde7e2",
"device_type": "raspberrypi3",
"should_track_latest_release": true,
"is_accessible_by_support_until__date": null,
"is_public": false,
"is_host": false,
"__metadata": {
"uri": "/resin/application(@id)?@id=1301645"
}
}
]
}

View File

@ -0,0 +1,25 @@
{
"created_at": "2020-01-16T17:08:56.652Z",
"id": 1859016,
"start_timestamp": "2020-01-16T17:08:56.219Z",
"end_timestamp": null,
"dockerfile": null,
"is_a_build_of__service": {
"__deferred": {
"uri": "/resin/service(233455)"
},
"__id": 233455
},
"image_size": null,
"is_stored_at__image_location": "registry2.balena-cloud.com/v2/c089c421fb2336d0475166fbf3d0f9fa",
"project_type": null,
"error_message": null,
"build_log": null,
"push_timestamp": null,
"status": "running",
"content_hash": null,
"contract": null,
"__metadata": {
"uri": "/resin/image(@id)?@id=1859016"
}
}

View File

@ -0,0 +1,19 @@
{
"id": 1774668,
"created_at": "2020-01-16T17:08:57.043Z",
"image": {
"__deferred": {
"uri": "/resin/image(1859016)"
},
"__id": 1859016
},
"is_part_of__release": {
"__deferred": {
"uri": "/resin/release(1218643)"
},
"__id": 1218643
},
"__metadata": {
"uri": "/resin/image__is_part_of__release(@id)?@id=1774668"
}
}

View File

@ -0,0 +1,15 @@
{
"id": 99699617,
"created_at": "2020-01-16T17:08:57.443Z",
"release_image": {
"__deferred": {
"uri": "/resin/image__is_part_of__release(1774668)"
},
"__id": 1774668
},
"label_name": "io.resin.features.firmware",
"value": "1",
"__metadata": {
"uri": "/resin/image_label(@id)?@id=99699617"
}
}

View File

@ -0,0 +1,52 @@
{
"d": [
{
"contains__image": [
{
"image": [
{
"id": 1820810,
"created_at": "2020-01-04T01:13:08.805Z",
"start_timestamp": "2020-01-04T01:13:08.583Z",
"end_timestamp": "2020-01-04T01:13:11.920Z",
"dockerfile": "# FROM busybox\n# FROM arm32v7/busybox\n# FROM arm32v7/alpine\n# FROM eu.gcr.io/buoyant-idea-226013/arm32v7/busybox\n# FROM eu.gcr.io/buoyant-idea-226013/amd64/busybox\n# FROM balenalib/raspberrypi3-debian:jessie-build\nFROM balenalib/raspberrypi3:stretch\nENV UDEV=1\n\n# FROM sander85/rpi-busybox # armv6\n# FROM balenalib/raspberrypi3-alpine\n\n# COPY start.sh /\n# COPY /src/start.sh /src/start.sh\n# COPY /src/hello.txt /\n# COPY src/hi.txt /\n\n# RUN cat /hello.txt\n# RUN cat /hi.txt\n# RUN cat /run/secrets/my-secret.txt\n# EXPOSE 80\nRUN uname -a\n\n# FROM alpine\n# RUN apk update && apk add bash\n# SHELL [\"/bin/bash\", \"-c\"]\n# CMD for ((i=1; i > 0; i++)); do echo \"(Plain Dockerfile 34-$i) $(uname -a)\"; sleep ${INTERVAL=5}; done\n\n# CMD i=1; while :; do echo \"Plain Dockerfile 36 ($i) $(uname -a)\"; sleep 10; i=$((i+1)); done\n# ENTRYPOINT [\"/usr/bin/entry.sh\"]\nCMD [\"/bin/bash\"]\n",
"is_a_build_of__service": {
"__deferred": {
"uri": "/resin/service(233455)"
},
"__id": 233455
},
"image_size": 134320410,
"is_stored_at__image_location": "registry2.balena-cloud.com/v2/9c00c9413942cd15cfc9189c5dac359d",
"project_type": "Standard Dockerfile",
"error_message": null,
"build_log": "Step 1/4 : FROM balenalib/raspberrypi3:stretch\n ---> 8a75ea61d9c0\nStep 2/4 : ENV UDEV=1\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 159206067c8a\nStep 3/4 : RUN uname -a\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> dd1b3d9c334b\nStep 4/4 : CMD [\"/bin/bash\"]\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 5211b6f4bb72\nSuccessfully built 5211b6f4bb72\n",
"push_timestamp": "2020-01-04T01:13:14.415Z",
"status": "success",
"content_hash": "sha256:6b5471aae43ae81e8f69e10d1a516cb412569a6d5020a57eae311f8fa16d688a",
"contract": null,
"__metadata": {
"uri": "/resin/image(@id)?@id=1820810"
}
}
],
"id": 1738663,
"created_at": "2020-01-04T01:13:14.646Z",
"is_part_of__release": {
"__deferred": {
"uri": "/resin/release(1203844)"
},
"__id": 1203844
},
"__metadata": {
"uri": "/resin/image__is_part_of__release(@id)?@id=1738663"
}
}
],
"id": 1203844,
"__metadata": {
"uri": "/resin/release(@id)?@id=1203844"
}
}
]
}

View File

@ -0,0 +1,54 @@
{
"id": 1218643,
"created_at": "2020-01-16T17:08:53.016Z",
"belongs_to__application": {
"__deferred": {
"uri": "/resin/application(1301645)"
},
"__id": 1301645
},
"is_created_by__user": {
"__deferred": {
"uri": "/resin/user(43699)"
},
"__id": 43699
},
"commit": "09f7c3e1fdec609be818002299edfc2a",
"composition": {
"version": "2.1",
"networks": {},
"volumes": {
"resin-data": {}
},
"services": {
"main": {
"build": {
"context": "."
},
"privileged": true,
"tty": true,
"restart": "always",
"network_mode": "host",
"volumes": [
"resin-data:/data"
],
"labels": {
"io.resin.features.kernel-modules": "1",
"io.resin.features.firmware": "1",
"io.resin.features.dbus": "1",
"io.resin.features.supervisor-api": "1",
"io.resin.features.resin-api": "1"
}
}
}
},
"status": "running",
"source": "local",
"build_log": null,
"start_timestamp": "2020-01-16T17:08:52.710Z",
"end_timestamp": null,
"update_timestamp": "2020-01-16T17:08:53.017Z",
"__metadata": {
"uri": "/resin/release(@id)?@id=1218643"
}
}

View File

@ -0,0 +1,99 @@
[
{"type":"metadata","resource":"buildLogId","value":1220245}
,
{"message":"\u001b[36m[Info]\u001b[39m Starting build for testApp, user gh_user"}
,
{"message":"\u001b[36m[Info]\u001b[39m Dashboard link: https://dashboard.balena-cloud.com/apps/1301645/devices"}
,
{"message":"\u001b[36m[Info]\u001b[39m Building on arm01"}
,
{"message":"\u001b[36m[Info]\u001b[39m Pulling previous images for caching purposes..."}
,
{"message":"[=> ] 2%","replace":true}
,
{"message":"[===> ] 6%","replace":true}
,
{"message":"[======> ] 13%","replace":true}
,
{"message":"[=================================================> ] 98%","replace":true}
,
{"message":"[==================================================>] 100%","replace":true}
,
{"type":"metadata","resource":"cursor","value":"erase"}
,
{"message":"\u001b[32m[Success]\u001b[39m Successfully pulled cache images"}
,
{"message":"\u001b[34m[main]\u001b[39m Step 1/4 : FROM busybox"}
,
{"message":"\u001b[34m[main]\u001b[39m ---> 76aea0766768"}
,
{"message":"\u001b[34m[main]\u001b[39m Step 2/4 : COPY ./src/start.sh /start.sh"}
,
{"message":"\u001b[34m[main]\u001b[39m ---> b563ad6a0801"}
,
{"message":"\u001b[34m[main]\u001b[39m Step 3/4 : RUN chmod a+x /start.sh"}
,
{"message":"\u001b[34m[main]\u001b[39m ---> Running in 10d4ddc40bfc"}
,
{"message":"\u001b[34m[main]\u001b[39m Removing intermediate container 10d4ddc40bfc"}
,
{"message":"\u001b[34m[main]\u001b[39m ---> 82e98871a32c"}
,
{"message":"\u001b[34m[main]\u001b[39m Step 4/4 : CMD [\"/start.sh\"]"}
,
{"message":"\u001b[34m[main]\u001b[39m ---> Running in 0682894e13eb"}
,
{"message":"\u001b[34m[main]\u001b[39m Removing intermediate container 0682894e13eb"}
,
{"message":"\u001b[34m[main]\u001b[39m ---> 889ccb6afc7c"}
,
{"message":"\u001b[34m[main]\u001b[39m Successfully built 889ccb6afc7c"}
,
{"message":"\u001b[36m[Info]\u001b[39m Uploading images"}
,
{"message":"[================> ] 33%","replace":true}
,
{"message":"[=========================> ] 50%","replace":true}
,
{"message":"[=================================> ] 67%","replace":true}
,
{"message":"[=================================> ] 67%","replace":true}
,
{"message":"[==========================================> ] 84%","replace":true}
,
{"message":"[==================================================>] 100%","replace":true}
,
{"message":"[==================================================>] 100%","replace":true}
,
{"message":"[==================================================>] 100%","replace":true}
,
{"message":"[==================================================>] 100%","replace":true}
,
{"message":"[==================================================>] 100%","replace":true}
,
{"message":"[==================================================>] 100%","replace":true}
,
{"type":"metadata","resource":"cursor","value":"erase"}
,
{"message":"\u001b[32m[Success]\u001b[39m Successfully uploaded images"}
,
{"message":"\u001b[36m[Info]\u001b[39m Built on arm01"}
,
{"message":"\u001b[32m[Success]\u001b[39m Release successfully created!"}
,
{"message":"\u001b[36m[Info]\u001b[39m Release: \u001b[34m05a24b5b034c9f95f25d4d74f0593bea\u001b[39m (id: \u001b[32m1220245\u001b[39m)"}
,
{"message":"\u001b[36m[Info]\u001b[39m \u001b[90m┌─────────\u001b[39m\u001b[90m┬────────────\u001b[39m\u001b[90m┬────────────┐\u001b[39m"}
,
{"message":"\u001b[36m[Info]\u001b[39m \u001b[90m│\u001b[39m \u001b[1mService\u001b[22m \u001b[90m│\u001b[39m \u001b[1mImage Size\u001b[22m \u001b[90m│\u001b[39m \u001b[1mBuild Time\u001b[22m \u001b[90m│\u001b[39m"}
,
{"message":"\u001b[36m[Info]\u001b[39m \u001b[90m├─────────\u001b[39m\u001b[90m┼────────────\u001b[39m\u001b[90m┼────────────┤\u001b[39m"}
,
{"message":"\u001b[36m[Info]\u001b[39m \u001b[90m│\u001b[39m main \u001b[90m│\u001b[39m 1.32 MB \u001b[90m│\u001b[39m 11 seconds \u001b[90m│\u001b[39m"}
,
{"message":"\u001b[36m[Info]\u001b[39m \u001b[90m└─────────\u001b[39m\u001b[90m┴────────────\u001b[39m\u001b[90m┴────────────┘\u001b[39m"}
,
{"message":"\u001b[36m[Info]\u001b[39m Build finished in 26 seconds"}
,
{"message":"\u001b[1m\u001b[34m\t\t\t \\\n\t\t\t \\\n\t\t\t \\\\\n\t\t\t \\\\\n\t\t\t >\\/7\n\t\t\t _.-(6' \\\n\t\t\t (=___._/` \\\n\t\t\t ) \\ |\n\t\t\t / / |\n\t\t\t / > /\n\t\t\t j < _\\\n\t\t\t _.-' : ``.\n\t\t\t \\ r=._\\ `.\n\t\t\t<`\\\\_ \\ .`-.\n\t\t\t \\ r-7 `-. ._ ' . `\\\n\t\t\t \\`, `-.`7 7) )\n\t\t\t \\/ \\| \\' / `-._\n\t\t\t || .'\n\t\t\t \\\\ (\n\t\t\t >\\ >\n\t\t\t ,.-' >.'\n\t\t\t <.'_.''\n\t\t\t <'\u001b[39m\u001b[22m","isSuccess":true}
]

View File

@ -0,0 +1,19 @@
{"status":"The push refers to repository [registry2.balena-cloud.com/v2/c089c421fb2336d0475166fbf3d0f9fa]"}
{"status":"Preparing","progressDetail":{},"id":"a5b1f6c006f8"}
{"status":"Preparing","progressDetail":{},"id":"2b74be40c29e"}
{"status":"Preparing","progressDetail":{},"id":"d1156b98822d"}
{"status":"Pushing","progressDetail":{"current":512,"total":89},"progress":"[==================================================\u003e] 512B","id":"a5b1f6c006f8"}
{"status":"Pushing","progressDetail":{"current":2048,"total":89},"progress":"[==================================================\u003e] 2.048kB","id":"a5b1f6c006f8"}
{"status":"Pushing","progressDetail":{"current":512,"total":89},"progress":"[==================================================\u003e] 512B","id":"2b74be40c29e"}
{"status":"Pushing","progressDetail":{"current":33792,"total":1199418},"progress":"[=\u003e ] 33.79kB/1.199MB","id":"d1156b98822d"}
{"status":"Pushing","progressDetail":{"current":2048,"total":89},"progress":"[==================================================\u003e] 2.048kB","id":"2b74be40c29e"}
{"status":"Pushing","progressDetail":{"current":99328,"total":1199418},"progress":"[====\u003e ] 99.33kB/1.199MB","id":"d1156b98822d"}
{"status":"Pushing","progressDetail":{"current":787456,"total":1199418},"progress":"[================================\u003e ] 787.5kB/1.199MB","id":"d1156b98822d"}
{"status":"Pushing","progressDetail":{"current":852992,"total":1199418},"progress":"[===================================\u003e ] 853kB/1.199MB","id":"d1156b98822d"}
{"status":"Pushing","progressDetail":{"current":951296,"total":1199418},"progress":"[=======================================\u003e ] 951.3kB/1.199MB","id":"d1156b98822d"}
{"status":"Pushing","progressDetail":{"current":1415680,"total":1199418},"progress":"[==================================================\u003e] 1.416MB","id":"d1156b98822d"}
{"status":"Pushed","progressDetail":{},"id":"a5b1f6c006f8"}
{"status":"Pushed","progressDetail":{},"id":"2b74be40c29e"}
{"status":"Pushed","progressDetail":{},"id":"d1156b98822d"}
{"status":"latest: digest: sha256:444a5e0c57eed51f5e752b908cb95188c25a0476fc6e5f43e5113edfc4d07199 size: 941"}
{"progressDetail":{},"aux":{"Tag":"latest","Digest":"sha256:444a5e0c57eed51f5e752b908cb95188c25a0476fc6e5f43e5113edfc4d07199","Size":941}}

View File

@ -0,0 +1,4 @@
FROM busybox
COPY ./src/start.sh /start.sh
RUN chmod a+x /start.sh
CMD ["/start.sh"]

View File

@ -0,0 +1,2 @@
#!/bin/sh
i=1; while :; do echo "basic test ($i) $(uname -a)"; sleep 5; i=$((i+1)); done