Convert lib/utils/compose to typescript

Change-type: patch
This commit is contained in:
Pagan Gazzard 2022-01-03 13:47:07 +00:00
parent a80f676804
commit bd021c0a2d
3 changed files with 172 additions and 135 deletions

View File

@ -15,6 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type { ImageModel, ReleaseModel } from 'balena-release/build/models';
import type { Composition, ImageDescriptor } from 'resin-compose-parse'; import type { Composition, ImageDescriptor } from 'resin-compose-parse';
import type { Pack } from 'tar-stream'; import type { Pack } from 'tar-stream';
@ -52,7 +53,7 @@ export interface ComposeOpts {
inlineLogs?: boolean; inlineLogs?: boolean;
multiDockerignore: boolean; multiDockerignore: boolean;
noParentCheck: boolean; noParentCheck: boolean;
projectName: string; projectName?: string;
projectPath: string; projectPath: string;
isLocal?: boolean; isLocal?: boolean;
} }
@ -79,7 +80,7 @@ export interface ComposeProject {
export interface Release { export interface Release {
client: ReturnType<typeof import('balena-release').createClient>; client: ReturnType<typeof import('balena-release').createClient>;
release: Pick< release: Pick<
import('balena-release/build/models').ReleaseModel, ReleaseModel,
| 'id' | 'id'
| 'status' | 'status'
| 'commit' | 'commit'
@ -91,7 +92,9 @@ export interface Release {
| 'start_timestamp' | 'start_timestamp'
| 'end_timestamp' | 'end_timestamp'
>; >;
serviceImages: Partial<import('balena-release/build/models').ImageModel>; serviceImages: Dictionary<
Omit<ImageModel, 'created_at' | 'is_a_build_of__service' | '__metadata'>
>;
} }
interface TarDirectoryOptions { interface TarDirectoryOptions {

View File

@ -15,14 +15,32 @@
* limitations under the License. * limitations under the License.
*/ */
import type { Renderer } from './compose_ts';
import type * as SDK from 'balena-sdk';
import type Dockerode = require('dockerode');
import * as path from 'path'; import * as path from 'path';
import type { Composition, ImageDescriptor } from 'resin-compose-parse';
import type {
BuiltImage,
ComposeOpts,
ComposeProject,
Release,
TaggedImage,
} from './compose-types';
import { getChalk } from './lazy'; import { getChalk } from './lazy';
import Logger = require('./logger');
import { ProgressCallback } from 'docker-progress';
/** export function generateOpts(options: {
* @returns Promise<{import('./compose-types').ComposeOpts}> source?: string;
*/ projectName?: string;
export function generateOpts(options) { nologs: boolean;
const { promises: fs } = require('fs'); 'noconvert-eol': boolean;
dockerfile?: string;
'multi-dockerignore': boolean;
'noparent-check': boolean;
}): Promise<ComposeOpts> {
const { promises: fs } = require('fs') as typeof import('fs');
return fs.realpath(options.source || '.').then((projectPath) => ({ return fs.realpath(options.source || '.').then((projectPath) => ({
projectName: options.projectName, projectName: options.projectName,
projectPath, projectPath,
@ -34,24 +52,19 @@ export function generateOpts(options) {
})); }));
} }
// Parse the given composition and return a structure with info. Input is: /** Parse the given composition and return a structure with info. Input is:
// - composePath: the *absolute* path to the directory containing the compose file * - composePath: the *absolute* path to the directory containing the compose file
// - composeStr: the contents of the compose file, as a string * - composeStr: the contents of the compose file, as a string
/**
* @param {string} composePath
* @param {string} composeStr
* @param {string | undefined} projectName The --projectName flag (build, deploy)
* @param {string | undefined} imageTag The --tag flag (build, deploy)
* @returns {import('./compose-types').ComposeProject}
*/ */
export function createProject( export function createProject(
composePath, composePath: string,
composeStr, composeStr: string,
projectName = '', projectName = '',
imageTag = '', imageTag = '',
) { ): ComposeProject {
const yml = require('js-yaml'); const yml = require('js-yaml') as typeof import('js-yaml');
const compose = require('resin-compose-parse'); const compose =
require('resin-compose-parse') as typeof import('resin-compose-parse');
// both methods below may throw. // both methods below may throw.
const rawComposition = yml.load(composeStr); const rawComposition = yml.load(composeStr);
@ -67,7 +80,8 @@ export function createProject(
descr.image.context != null && descr.image.context != null &&
descr.image.tag == null descr.image.tag == null
) { ) {
const { makeImageName } = require('./compose_ts'); const { makeImageName } =
require('./compose_ts') as typeof import('./compose_ts');
descr.image.tag = makeImageName(projectName, descr.serviceName, imageTag); descr.image.tag = makeImageName(projectName, descr.serviceName, imageTag);
} }
return descr; return descr;
@ -80,30 +94,20 @@ export function createProject(
}; };
} }
/**
* @param {string} apiEndpoint
* @param {string} auth
* @param {number} userId
* @param {number} appId
* @param {import('resin-compose-parse').Composition} composition
* @param {boolean} draft
* @param {string|undefined} semver
* @param {string|undefined} contract
* @returns {Promise<import('./compose-types').Release>}
*/
export const createRelease = async function ( export const createRelease = async function (
apiEndpoint, apiEndpoint: string,
auth, auth: string,
userId, userId: number,
appId, appId: number,
composition, composition: Composition,
draft, draft: boolean,
semver, semver?: string,
contract, contract?: string,
) { ): Promise<Release> {
const _ = require('lodash'); const _ = require('lodash') as typeof import('lodash');
const crypto = require('crypto'); const crypto = require('crypto') as typeof import('crypto');
const releaseMod = require('balena-release'); const releaseMod =
require('balena-release') as typeof import('balena-release');
const client = releaseMod.createClient({ apiEndpoint, auth }); const client = releaseMod.createClient({ apiEndpoint, auth });
@ -133,24 +137,26 @@ export const createRelease = async function (
'start_timestamp', 'start_timestamp',
'end_timestamp', 'end_timestamp',
]), ]),
serviceImages: _.mapValues(serviceImages, (serviceImage) => serviceImages: _.mapValues(
_.omit(serviceImage, [ serviceImages,
'created_at', (serviceImage) =>
'is_a_build_of__service', _.omit(serviceImage, [
'__metadata', 'created_at',
]), 'is_a_build_of__service',
'__metadata',
]) as Omit<
typeof serviceImage,
'created_at' | 'is_a_build_of__service' | '__metadata'
>,
), ),
}; };
}; };
/** export const tagServiceImages = (
* docker: Dockerode,
* @param {import('dockerode')} docker images: BuiltImage[],
* @param {Array<import('./compose-types').BuiltImage>} images serviceImages: Release['serviceImages'],
* @param {Partial<import('balena-release/build/models').ImageModel>} serviceImages ): Promise<TaggedImage[]> =>
* @returns {Promise<Array<import('./compose-types').TaggedImage>>}
*/
export const tagServiceImages = (docker, images, serviceImages) =>
Promise.all( Promise.all(
images.map(function (d) { images.map(function (d) {
const serviceImage = serviceImages[d.serviceName]; const serviceImage = serviceImages[d.serviceName];
@ -177,25 +183,24 @@ export const tagServiceImages = (docker, images, serviceImages) =>
}), }),
); );
/** export const getPreviousRepos = (
* @param {*} sdk sdk: SDK.BalenaSDK,
* @param {import('./logger')} logger logger: Logger,
* @param {number} appID appID: number,
* @returns {Promise<string[]>} ): Promise<string[]> =>
*/
export const getPreviousRepos = (sdk, logger, appID) =>
sdk.pine sdk.pine
.get({ .get<SDK.Release>({
resource: 'release', resource: 'release',
options: { options: {
$select: 'id',
$filter: { $filter: {
belongs_to__application: appID, belongs_to__application: appID,
status: 'success', status: 'success',
}, },
$select: ['id'],
$expand: { $expand: {
contains__image: { contains__image: {
$expand: 'image', $select: 'image',
$expand: { image: { $select: 'is_stored_at__image_location' } },
}, },
}, },
$orderby: 'id desc', $orderby: 'id desc',
@ -205,8 +210,11 @@ export const getPreviousRepos = (sdk, logger, appID) =>
.then(function (release) { .then(function (release) {
// grab all images from the latest release, return all image locations in the registry // grab all images from the latest release, return all image locations in the registry
if (release.length > 0) { if (release.length > 0) {
const images = release[0].contains__image; const images = release[0].contains__image as Array<{
const { getRegistryAndName } = require('resin-multibuild'); image: [SDK.Image];
}>;
const { getRegistryAndName } =
require('resin-multibuild') as typeof import('resin-multibuild');
return Promise.all( return Promise.all(
images.map(function (d) { images.map(function (d) {
const imageName = d.image[0].is_stored_at__image_location || ''; const imageName = d.image[0].is_stored_at__image_location || '';
@ -226,21 +234,13 @@ export const getPreviousRepos = (sdk, logger, appID) =>
return []; return [];
}); });
/**
* @param {*} sdk
* @param {string} tokenAuthEndpoint
* @param {string} registry
* @param {string[]} images
* @param {string[]} previousRepos
* @returns {Promise<string>}
*/
export const authorizePush = function ( export const authorizePush = function (
sdk, sdk: SDK.BalenaSDK,
tokenAuthEndpoint, tokenAuthEndpoint: string,
registry, registry: string,
images, images: string[],
previousRepos, previousRepos: string[],
) { ): Promise<string> {
if (!Array.isArray(images)) { if (!Array.isArray(images)) {
images = [images]; images = [images];
} }
@ -261,17 +261,20 @@ export const authorizePush = function (
// utilities // utilities
const renderProgressBar = function (percentage, stepCount) { const renderProgressBar = function (percentage: number, stepCount: number) {
const _ = require('lodash'); const _ = require('lodash') as typeof import('lodash');
percentage = _.clamp(percentage, 0, 100); percentage = _.clamp(percentage, 0, 100);
const barCount = Math.floor((stepCount * percentage) / 100); const barCount = Math.floor((stepCount * percentage) / 100);
const spaceCount = stepCount - barCount; const spaceCount = stepCount - barCount;
const bar = `[${_.repeat('=', barCount)}>${_.repeat(' ', spaceCount)}]`; const bar = `[${_.repeat('=', barCount)}>${_.repeat(' ', spaceCount)}]`;
return `${bar} ${_.padStart(percentage, 3)}%`; return `${bar} ${_.padStart(`${percentage}`, 3)}%`;
}; };
export const pushProgressRenderer = function (tty, prefix) { export const pushProgressRenderer = function (
const fn = function (e) { tty: ReturnType<typeof import('./tty')>,
prefix: string,
): ProgressCallback & { end: () => void } {
const fn: ProgressCallback & { end: () => void } = function (e) {
const { error, percentage } = e; const { error, percentage } = e;
if (error != null) { if (error != null) {
throw new Error(error); throw new Error(error);
@ -285,14 +288,39 @@ export const pushProgressRenderer = function (tty, prefix) {
return fn; return fn;
}; };
export class BuildProgressUI { export class BuildProgressUI implements Renderer {
constructor(tty, descriptors) { public streams;
private _prefix;
private _prefixWidth;
private _tty;
private _services;
private _startTime: undefined | number;
private _ended;
private _serviceToDataMap: Dictionary<{
status?: string;
progress?: number;
error?: Error;
}> = {};
private _cancelled;
private _spinner;
private _runloop:
| undefined
| ReturnType<typeof import('./compose_ts').createRunLoop>;
// these are to handle window wrapping
private _maxLineWidth: undefined | number;
private _lineWidths: number[] = [];
constructor(
tty: ReturnType<typeof import('./tty')>,
descriptors: ImageDescriptor[],
) {
this._handleEvent = this._handleEvent.bind(this); this._handleEvent = this._handleEvent.bind(this);
this.start = this.start.bind(this); this.start = this.start.bind(this);
this.end = this.end.bind(this); this.end = this.end.bind(this);
this._display = this._display.bind(this); this._display = this._display.bind(this);
const _ = require('lodash'); const _ = require('lodash') as typeof import('lodash');
const through = require('through2'); const through = require('through2') as typeof import('through2');
const eventHandler = this._handleEvent; const eventHandler = this._handleEvent;
const services = _.map(descriptors, 'serviceName'); const services = _.map(descriptors, 'serviceName');
@ -310,7 +338,6 @@ export class BuildProgressUI {
.value(); .value();
this._tty = tty; this._tty = tty;
this._serviceToDataMap = {};
this._services = services; this._services = services;
// Logger magically prefixes the log line with [Build] etc., but it doesn't // Logger magically prefixes the log line with [Build] etc., but it doesn't
@ -320,22 +347,22 @@ export class BuildProgressUI {
const offset = 10; // account for escape sequences inserted for colouring const offset = 10; // account for escape sequences inserted for colouring
this._prefixWidth = this._prefixWidth =
offset + prefix.length + _.max(_.map(services, 'length')); offset + prefix.length + _.max(_.map(services, (s) => s.length))!;
this._prefix = prefix; this._prefix = prefix;
// these are to handle window wrapping
this._maxLineWidth = null;
this._lineWidths = [];
this._startTime = null;
this._ended = false; this._ended = false;
this._cancelled = false; this._cancelled = false;
this._spinner = require('./compose_ts').createSpinner(); this._spinner = (
require('./compose_ts') as typeof import('./compose_ts')
).createSpinner();
this.streams = streams; this.streams = streams;
} }
_handleEvent(service, event) { _handleEvent(
service: string,
event: { status?: string; progress?: number; error?: Error },
) {
this._serviceToDataMap[service] = event; this._serviceToDataMap[service] = event;
} }
@ -344,20 +371,19 @@ export class BuildProgressUI {
this._services.forEach((service) => { this._services.forEach((service) => {
this.streams[service].write({ status: 'Preparing...' }); this.streams[service].write({ status: 'Preparing...' });
}); });
this._runloop = require('./compose_ts').createRunLoop(this._display); this._runloop = (
require('./compose_ts') as typeof import('./compose_ts')
).createRunLoop(this._display);
this._startTime = Date.now(); this._startTime = Date.now();
} }
/** end(summary?: Dictionary<string>) {
* @param {Dictionary<string> | undefined} summary
*/
end(summary) {
if (this._ended) { if (this._ended) {
return; return;
} }
this._ended = true; this._ended = true;
this._runloop?.end(); this._runloop?.end();
this._runloop = null; this._runloop = undefined;
this._clear(); this._clear();
this._renderStatus(true); this._renderStatus(true);
@ -378,7 +404,7 @@ export class BuildProgressUI {
} }
_getServiceSummary() { _getServiceSummary() {
const _ = require('lodash'); const _ = require('lodash') as typeof import('lodash');
const services = this._services; const services = this._services;
const serviceToDataMap = this._serviceToDataMap; const serviceToDataMap = this._serviceToDataMap;
@ -405,11 +431,11 @@ export class BuildProgressUI {
.value(); .value();
} }
_renderStatus(end) { _renderStatus(end = false) {
end ??= false; const moment = require('moment') as typeof import('moment');
(
const moment = require('moment'); require('moment-duration-format') as typeof import('moment-duration-format')
require('moment-duration-format')(moment); )(moment);
this._tty.clearLine(); this._tty.clearLine();
this._tty.write(this._prefix); this._tty.write(this._prefix);
@ -434,11 +460,11 @@ export class BuildProgressUI {
} }
} }
_renderSummary(serviceToStrMap) { _renderSummary(serviceToStrMap: Dictionary<string>) {
const _ = require('lodash'); const _ = require('lodash') as typeof import('lodash');
const chalk = getChalk(); const chalk = getChalk();
const truncate = require('cli-truncate'); const truncate = require('cli-truncate') as typeof import('cli-truncate');
const strlen = require('string-width'); const strlen = require('string-width') as typeof import('string-width');
this._services.forEach((service, index) => { this._services.forEach((service, index) => {
let str = _.padEnd(this._prefix + chalk.bold(service), this._prefixWidth); let str = _.padEnd(this._prefix + chalk.bold(service), this._prefixWidth);
@ -454,13 +480,23 @@ export class BuildProgressUI {
} }
} }
export class BuildProgressInline { export class BuildProgressInline implements Renderer {
constructor(outStream, descriptors) { public streams;
private _prefixWidth;
private _outStream;
private _services;
private _startTime: number | undefined;
private _ended;
constructor(
outStream: NodeJS.ReadWriteStream,
descriptors: Array<{ serviceName: string }>,
) {
this.start = this.start.bind(this); this.start = this.start.bind(this);
this.end = this.end.bind(this); this.end = this.end.bind(this);
this._renderEvent = this._renderEvent.bind(this); this._renderEvent = this._renderEvent.bind(this);
const _ = require('lodash'); const _ = require('lodash') as typeof import('lodash');
const through = require('through2'); const through = require('through2') as typeof import('through2');
const services = _.map(descriptors, 'serviceName'); const services = _.map(descriptors, 'serviceName');
const eventHandler = this._renderEvent; const eventHandler = this._renderEvent;
@ -477,10 +513,9 @@ export class BuildProgressInline {
.value(); .value();
const offset = 10; // account for escape sequences inserted for colouring const offset = 10; // account for escape sequences inserted for colouring
this._prefixWidth = offset + _.max(_.map(services, 'length')); this._prefixWidth = offset + _.max(_.map(services, (s) => s.length))!;
this._outStream = outStream; this._outStream = outStream;
this._services = services; this._services = services;
this._startTime = null;
this._ended = false; this._ended = false;
this.streams = streams; this.streams = streams;
@ -494,12 +529,11 @@ export class BuildProgressInline {
this._startTime = Date.now(); this._startTime = Date.now();
} }
/** end(summary?: Dictionary<string>) {
* @param {Dictionary<string> | undefined} summary const moment = require('moment') as typeof import('moment');
*/ (
end(summary) { require('moment-duration-format') as typeof import('moment-duration-format')
const moment = require('moment'); )(moment);
require('moment-duration-format')(moment);
if (this._ended) { if (this._ended) {
return; return;
@ -527,8 +561,8 @@ export class BuildProgressInline {
this._outStream.write(`Built ${serviceStr} in ${durationStr}\n`); this._outStream.write(`Built ${serviceStr} in ${durationStr}\n`);
} }
_renderEvent(service, event) { _renderEvent(service: string, event: { status?: string; error?: Error }) {
const _ = require('lodash'); const _ = require('lodash') as typeof import('lodash');
const str = (function () { const str = (function () {
const { status, error } = event; const { status, error } = event;

View File

@ -235,7 +235,7 @@ interface BuildTaskPlus extends MultiBuild.BuildTask {
logBuffer?: string[]; logBuffer?: string[];
} }
interface Renderer { export interface Renderer {
start: () => void; start: () => void;
end: (buildSummaryByService?: Dictionary<string>) => void; end: (buildSummaryByService?: Dictionary<string>) => void;
streams: Dictionary<NodeJS.ReadWriteStream>; streams: Dictionary<NodeJS.ReadWriteStream>;