mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-02-20 17:33:18 +00:00
Convert selected functions to Typescript and async/await (compose.js)
Connects-to: #1045 Change-type: patch
This commit is contained in:
parent
480228d8f4
commit
8522363cd3
@ -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,
|
||||
|
27
lib/utils/compose-types.d.ts
vendored
27
lib/utils/compose-types.d.ts
vendored
@ -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>;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user