Convert selected functions to Typescript and async/await (compose.js)

Connects-to: #1045
Change-type: patch
This commit is contained in:
Paulo Castro 2020-05-21 14:52:15 +01:00
parent 480228d8f4
commit 8522363cd3
4 changed files with 268 additions and 132 deletions

View File

@ -40,7 +40,10 @@ const deployProject = function(docker, logger, composeOpts, opts) {
const _ = require('lodash');
const doodles = require('resin-doodles');
const sdk = getBalenaSdk();
const { loadProject } = require('../utils/compose_ts');
const {
deployProject: $deployProject,
loadProject,
} = require('../utils/compose_ts');
return Promise.resolve(loadProject(logger, composeOpts, opts.image))
.then(function(project) {
@ -147,7 +150,7 @@ const deployProject = function(docker, logger, composeOpts, opts) {
sdk.auth.getToken(),
sdk.settings.get('apiUrl'),
(userId, auth, apiEndpoint) =>
compose.deployProject(
$deployProject(
docker,
logger,
project.composition,

View File

@ -25,6 +25,27 @@ interface Image {
tag: string;
}
export interface BuiltImage {
logs: string;
name: string;
props: {
dockerfile?: string;
projectType?: string;
size?: number;
};
serviceName: string;
}
export interface TaggedImage {
localImage: import('dockerode').Image;
serviceImage: import('balena-release/build/models').ImageModel;
serviceName: string;
logs: string;
props: BuiltImage.props;
registry: string;
repo: string;
}
export interface ComposeOpts {
dockerfilePath?: string;
inlineLogs?: boolean;
@ -40,6 +61,12 @@ export interface ComposeProject {
descriptors: ImageDescriptor[];
}
export interface Release {
client: import('pinejs-client').ApiClient;
release: Partial<import('balena-release/build/models').ReleaseModel>;
serviceImages: Partial<import('balena-release/build/models').ImageModel>;
}
interface TarDirectoryOptions {
convertEol?: boolean;
preFinalizeCallback?: (pack: Pack) => void | Promise<void>;

View File

@ -19,7 +19,7 @@ import * as Promise from 'bluebird';
import { stripIndent } from 'common-tags';
import * as path from 'path';
import { getBalenaSdk, getChalk } from './lazy';
import { getChalk } from './lazy';
import { IgnoreFileType } from './ignore';
export const appendProjectOptions = opts =>
@ -245,6 +245,11 @@ function originalTarDirectory(dir, param) {
});
}
/**
* @param {string} str
* @param {number} len
* @returns {string}
*/
const truncateString = function(str, len) {
if (str.length < len) {
return str;
@ -485,7 +490,21 @@ export function buildProject(
.finally(renderer.end);
}
const createRelease = function(apiEndpoint, auth, userId, appId, composition) {
/**
* @param {string} apiEndpoint
* @param {string} auth
* @param {number} userId
* @param {number} appId
* @param {import('resin-compose-parse').Composition} composition
* @returns {Promise<import('./compose-types').Release>}
*/
export const createRelease = function(
apiEndpoint,
auth,
userId,
appId,
composition,
) {
const _ = require('lodash');
const crypto = require('crypto');
const releaseMod = require('balena-release');
@ -524,7 +543,14 @@ const createRelease = function(apiEndpoint, auth, userId, appId, composition) {
});
};
const tagServiceImages = (docker, images, serviceImages) =>
/**
*
* @param {import('docker-toolbelt')} docker
* @param {Array<import('./compose-types').BuiltImage>} images
* @param {Partial<import('balena-release/build/models').ImageModel>} serviceImages
* @returns {Promise<Array<import('./compose-types').TaggedImage>>}
*/
export const tagServiceImages = (docker, images, serviceImages) =>
Promise.map(images, function(d) {
const serviceImage = serviceImages[d.serviceName];
const imageName = serviceImage.is_stored_at__image_location;
@ -549,7 +575,14 @@ const tagServiceImages = (docker, images, serviceImages) =>
}));
});
const getPreviousRepos = (sdk, docker, logger, appID) =>
/**
* @param {*} sdk
* @param {import('docker-toolbelt')} docker
* @param {import('./logger')} logger
* @param {number} appID
* @returns {Promise<string[]>}
*/
export const getPreviousRepos = (sdk, docker, logger, appID) =>
sdk.pine
.get({
resource: 'release',
@ -590,7 +623,15 @@ const getPreviousRepos = (sdk, docker, logger, appID) =>
return [];
});
const authorizePush = function(
/**
* @param {*} sdk
* @param {string} tokenAuthEndpoint
* @param {string} registry
* @param {string[]} images
* @param {string[]} previousRepos
* @returns {Promise<string>}
*/
export const authorizePush = function(
sdk,
tokenAuthEndpoint,
registry,
@ -613,10 +654,21 @@ const authorizePush = function(
})
.get('body')
.get('token')
.catchReturn({});
.catchReturn('');
};
const pushAndUpdateServiceImages = function(docker, token, images, afterEach) {
/**
* @param {import('docker-toolbelt')} docker
* @param {string} token
* @param {Array<import('./compose-types').TaggedImage>} images
* @param {(serviceImage: import('balena-release/build/models').ImageModel, props: object) => void} afterEach
*/
export const pushAndUpdateServiceImages = function(
docker,
token,
images,
afterEach,
) {
const { DockerProgress } = require('docker-progress');
const { retry } = require('./helpers');
const tty = require('./tty')(process.stdout);
@ -633,14 +685,18 @@ const pushAndUpdateServiceImages = function(docker, token, images, afterEach) {
return Promise.using(tty.cursorHidden(), () =>
Promise.map(images, ({ serviceImage, localImage, props, logs }, index) =>
Promise.join(
// @ts-ignore
localImage.inspect().get('Size'),
retry(
// @ts-ignore
() => progress.push(localImage.name, reporters[index], opts),
3, // `times` - retry 3 times
// @ts-ignore
localImage.name, // `label` included in retry log messages
2000, // `delayMs` - wait 2 seconds before the 1st retry
1.4, // `backoffScaler` - wait multiplier for each retry
).finally(renderer.end),
/** @type {(size: number, digest: string) => void} */
function(size, digest) {
serviceImage.image_size = size;
serviceImage.content_hash = digest;
@ -666,91 +722,6 @@ const pushAndUpdateServiceImages = function(docker, token, images, afterEach) {
);
};
export function deployProject(
docker,
logger,
composition,
images,
appId,
userId,
auth,
apiEndpoint,
skipLogUpload,
) {
const _ = require('lodash');
const releaseMod = require('balena-release');
const tty = require('./tty')(process.stdout);
const prefix = getChalk().cyan('[Info]') + ' ';
const spinner = createSpinner();
let runloop = runSpinner(tty, spinner, `${prefix}Creating release...`);
return createRelease(apiEndpoint, auth, userId, appId, composition)
.finally(runloop.end)
.then(function({ client, release, serviceImages }) {
logger.logDebug('Tagging images...');
return tagServiceImages(docker, images, serviceImages)
.tap(function(taggedImages) {
logger.logDebug('Authorizing push...');
const sdk = getBalenaSdk();
return getPreviousRepos(sdk, docker, logger, appId)
.then(previousRepos =>
authorizePush(
sdk,
apiEndpoint,
taggedImages[0].registry,
_.map(taggedImages, 'repo'),
previousRepos,
),
)
.then(function(token) {
logger.logInfo('Pushing images to registry...');
return pushAndUpdateServiceImages(
docker,
token,
taggedImages,
function(serviceImage) {
logger.logDebug(
`Saving image ${serviceImage.is_stored_at__image_location}`,
);
if (skipLogUpload) {
delete serviceImage.build_log;
}
return releaseMod.updateImage(
client,
serviceImage.id,
serviceImage,
);
},
);
})
.finally(function() {
logger.logDebug('Untagging images...');
return Promise.map(taggedImages, ({ localImage }) =>
localImage.remove(),
);
});
})
.then(() => {
release.status = 'success';
})
.tapCatch(() => {
release.status = 'failed';
})
.finally(function() {
runloop = runSpinner(tty, spinner, `${prefix}Saving release...`);
release.end_timestamp = new Date();
if (release.id == null) {
return;
}
return releaseMod
.updateRelease(client, release.id, release)
.finally(runloop.end);
})
.return(release);
});
}
// utilities
const renderProgressBar = function(percentage, stepCount) {
@ -874,39 +845,6 @@ var pullProgressAdapter = outStream =>
});
};
var createSpinner = function() {
const chars = '|/-\\';
let index = 0;
return () => chars[index++ % chars.length];
};
var runSpinner = function(tty, spinner, msg) {
const runloop = createRunLoop(function() {
tty.clearLine();
tty.writeLine(`${msg} ${spinner()}`);
return tty.cursorUp();
});
runloop.onEnd = function() {
tty.clearLine();
return tty.writeLine(msg);
};
return runloop;
};
var createRunLoop = function(tick) {
const timerId = setInterval(tick, 1000 / 10);
var runloop = {
onEnd() {
// noop
},
end() {
clearInterval(timerId);
return runloop.onEnd();
},
};
return runloop;
};
class BuildProgressUI {
constructor(tty, descriptors) {
this._handleEvent = this._handleEvent.bind(this);
@ -953,7 +891,7 @@ class BuildProgressUI {
this._startTime = null;
this._ended = false;
this._cancelled = false;
this._spinner = createSpinner();
this._spinner = require('./compose_ts').createSpinner();
this.streams = streams;
}
@ -974,7 +912,7 @@ class BuildProgressUI {
this._services.forEach(service => {
this.streams[service].write({ status: 'Preparing...' });
});
this._runloop = createRunLoop(this._display);
this._runloop = require('./compose_ts').createRunLoop(this._display);
this._startTime = Date.now();
}

View File

@ -27,6 +27,15 @@ import { Readable } from 'stream';
import * as tar from 'tar-stream';
import { ExpectedError } from '../errors';
import { getBalenaSdk, getChalk } from '../utils/lazy';
import {
BuiltImage,
ComposeOpts,
ComposeProject,
Release,
TaggedImage,
TarDirectoryOptions,
} from './compose-types';
import { DeviceInfo } from './device/api';
import Logger = require('./logger');
@ -47,9 +56,9 @@ const compositionFileNames = ['docker-compose.yml', 'docker-compose.yaml'];
*/
export async function loadProject(
logger: Logger,
opts: import('./compose-types').ComposeOpts,
opts: ComposeOpts,
image?: string,
): Promise<import('./compose-types').ComposeProject> {
): Promise<ComposeProject> {
const compose = await import('resin-compose-parse');
const { createProject } = await import('./compose');
let composeName: string;
@ -170,7 +179,7 @@ export async function tarDirectory(
preFinalizeCallback,
convertEol = false,
nogitignore = false,
}: import('./compose-types').TarDirectoryOptions,
}: TarDirectoryOptions,
): Promise<import('stream').Readable> {
(await import('assert')).strict.strictEqual(nogitignore, true);
const { filterFilesWithDockerignore } = await import('./ignore');
@ -567,3 +576,162 @@ export async function validateProjectDirectory(
return result;
}
async function getTokenForPreviousRepos(
docker: import('docker-toolbelt'),
logger: Logger,
appId: number,
apiEndpoint: string,
taggedImages: TaggedImage[],
): Promise<string> {
logger.logDebug('Authorizing push...');
const { authorizePush, getPreviousRepos } = await import('./compose');
const sdk = getBalenaSdk();
const previousRepos = await getPreviousRepos(sdk, docker, logger, appId);
if (!previousRepos || previousRepos.length === 0) {
return '';
}
const token = await authorizePush(
sdk,
apiEndpoint,
taggedImages[0].registry,
_.map(taggedImages, 'repo'),
previousRepos,
);
return token;
}
async function pushServiceImages(
docker: import('docker-toolbelt'),
logger: Logger,
pineClient: import('pinejs-client'),
taggedImages: TaggedImage[],
token: string,
skipLogUpload: boolean,
): Promise<void> {
const { pushAndUpdateServiceImages } = await import('./compose');
const releaseMod = await import('balena-release');
logger.logInfo('Pushing images to registry...');
await pushAndUpdateServiceImages(docker, token, taggedImages, async function(
serviceImage,
) {
logger.logDebug(
`Saving image ${serviceImage.is_stored_at__image_location}`,
);
if (skipLogUpload) {
delete serviceImage.build_log;
}
await releaseMod.updateImage(pineClient, serviceImage.id, serviceImage);
});
}
export async function deployProject(
docker: import('docker-toolbelt'),
logger: Logger,
composition: import('resin-compose-parse').Composition,
images: BuiltImage[],
appId: number,
userId: number,
auth: string,
apiEndpoint: string,
skipLogUpload: boolean,
): Promise<Partial<import('balena-release/build/models').ReleaseModel>> {
const releaseMod = require('balena-release');
const { createRelease, tagServiceImages } = await import('./compose');
const tty = (await import('./tty'))(process.stdout);
const prefix = getChalk().cyan('[Info]') + ' ';
const spinner = createSpinner();
let runloop = runSpinner(tty, spinner, `${prefix}Creating release...`);
let $release: Release;
try {
$release = await createRelease(
apiEndpoint,
auth,
userId,
appId,
composition,
);
} finally {
runloop.end();
}
const { client: pineClient, release, serviceImages } = $release;
try {
logger.logDebug('Tagging images...');
const taggedImages = await tagServiceImages(docker, images, serviceImages);
try {
const token = await getTokenForPreviousRepos(
docker,
logger,
appId,
apiEndpoint,
taggedImages,
);
await pushServiceImages(
docker,
logger,
pineClient,
taggedImages,
token,
skipLogUpload,
);
release.status = 'success';
} catch (err) {
release.status = 'failed';
throw err;
} finally {
logger.logDebug('Untagging images...');
await Bluebird.map(taggedImages, ({ localImage }) => localImage.remove());
}
} finally {
runloop = runSpinner(tty, spinner, `${prefix}Saving release...`);
release.end_timestamp = new Date();
if (release.id != null) {
try {
await releaseMod.updateRelease(pineClient, release.id, release);
} finally {
runloop.end();
}
}
}
return release;
}
export function createSpinner() {
const chars = '|/-\\';
let index = 0;
return () => chars[index++ % chars.length];
}
function runSpinner(
tty: ReturnType<typeof import('./tty')>,
spinner: () => string,
msg: string,
) {
const runloop = createRunLoop(function() {
tty.clearLine();
tty.writeLine(`${msg} ${spinner()}`);
return tty.cursorUp();
});
runloop.onEnd = function() {
tty.clearLine();
return tty.writeLine(msg);
};
return runloop;
}
export function createRunLoop(tick: (...args: any[]) => void) {
const timerId = setInterval(tick, 1000 / 10);
const runloop = {
onEnd() {
// noop
},
end() {
clearInterval(timerId);
return runloop.onEnd();
},
};
return runloop;
}