@ethersphere/bee-factory
Version:
Orchestration CLI for spinning up local development Bee cluster with Docker
407 lines (406 loc) • 18.5 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Docker = exports.ContainerType = exports.CONTRACT_LABEL_KEY_PREFIX = exports.BLOCKCHAIN_VERSION_LABEL_KEY = exports.WORKER_COUNT = exports.DEFAULT_IMAGE_PREFIX = exports.DEFAULT_ENV_PREFIX = void 0;
const dockerode_1 = __importDefault(require("dockerode"));
const error_1 = require("./error");
exports.DEFAULT_ENV_PREFIX = 'bee-factory';
exports.DEFAULT_IMAGE_PREFIX = 'bee-factory';
const BLOCKCHAIN_IMAGE_NAME_SUFFIX = '-blockchain';
const QUEEN_IMAGE_NAME_SUFFIX = '-queen';
const WORKER_IMAGE_NAME_SUFFIX = '-worker';
const NETWORK_NAME_SUFFIX = '-network';
exports.WORKER_COUNT = 4;
exports.BLOCKCHAIN_VERSION_LABEL_KEY = 'org.ethswarm.beefactory.blockchain-version';
exports.CONTRACT_LABEL_KEY_PREFIX = 'org.ethswarm.beefactory.contracts.';
var ContainerType;
(function (ContainerType) {
ContainerType["QUEEN"] = "queen";
ContainerType["BLOCKCHAIN"] = "blockchain";
ContainerType["WORKER_1"] = "worker1";
ContainerType["WORKER_2"] = "worker2";
ContainerType["WORKER_3"] = "worker3";
ContainerType["WORKER_4"] = "worker4";
})(ContainerType = exports.ContainerType || (exports.ContainerType = {}));
class Docker {
constructor(console, envPrefix, imagePrefix, repo) {
Object.defineProperty(this, "docker", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "console", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "runningContainers", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "envPrefix", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "imagePrefix", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "repo", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.docker = new dockerode_1.default();
this.console = console;
this.runningContainers = [];
this.envPrefix = envPrefix;
this.imagePrefix = imagePrefix;
this.repo = repo;
}
get networkName() {
return `${this.envPrefix}${NETWORK_NAME_SUFFIX}`;
}
get blockchainName() {
return `${this.envPrefix}${BLOCKCHAIN_IMAGE_NAME_SUFFIX}`;
}
blockchainImage(blockchainVersion) {
if (!this.repo)
throw new TypeError('Repo has to be defined!');
return `${this.repo}/${this.imagePrefix}${BLOCKCHAIN_IMAGE_NAME_SUFFIX}:${blockchainVersion}`;
}
get queenName() {
return `${this.envPrefix}${QUEEN_IMAGE_NAME_SUFFIX}`;
}
queenImage(beeVersion) {
if (!this.repo)
throw new TypeError('Repo has to be defined!');
return `${this.repo}/${this.imagePrefix}${QUEEN_IMAGE_NAME_SUFFIX}:${beeVersion}`;
}
workerName(index) {
return `${this.envPrefix}${WORKER_IMAGE_NAME_SUFFIX}-${index}`;
}
workerImage(beeVersion, workerNumber) {
if (!this.repo)
throw new TypeError('Repo has to be defined!');
return `${this.repo}/${this.imagePrefix}${WORKER_IMAGE_NAME_SUFFIX}-${workerNumber}:${beeVersion}`;
}
createNetwork() {
return __awaiter(this, void 0, void 0, function* () {
const networks = yield this.docker.listNetworks({ filters: { name: [this.networkName] } });
if (networks.length === 0) {
yield this.docker.createNetwork({ Name: this.networkName });
}
});
}
startBlockchainNode(blockchainVersion, options) {
return __awaiter(this, void 0, void 0, function* () {
if (options.fresh)
yield this.removeContainer(this.blockchainName);
yield this.pullImageIfNotFound(this.blockchainImage(blockchainVersion));
const container = yield this.findOrCreateContainer(this.blockchainName, {
Image: this.blockchainImage(blockchainVersion),
name: this.blockchainName,
ExposedPorts: {
'9545/tcp': {},
},
AttachStderr: false,
AttachStdout: false,
HostConfig: {
PortBindings: { '9545/tcp': [{ HostPort: '9545' }] },
NetworkMode: this.networkName,
},
});
this.runningContainers.push(container);
const state = yield container.inspect();
// If it is already running (because of whatever reason) we are not spawning new node
if (!state.State.Running) {
yield container.start();
}
else {
this.console.info('The blockchain container was already running, so not starting it again.');
}
});
}
startQueenNode(beeVersion, options) {
return __awaiter(this, void 0, void 0, function* () {
if (options.fresh)
yield this.removeContainer(this.queenName);
yield this.pullImageIfNotFound(this.queenImage(beeVersion));
const contractAddresses = yield this.getContractAddresses(this.queenImage(beeVersion));
const container = yield this.findOrCreateContainer(this.queenName, {
Image: this.queenImage(beeVersion),
name: this.queenName,
ExposedPorts: {
'1633/tcp': {},
'1634/tcp': {},
'1635/tcp': {},
},
Tty: true,
Cmd: ['start'],
Env: this.createBeeEnvParameters(contractAddresses),
AttachStderr: false,
AttachStdout: false,
HostConfig: {
NetworkMode: this.networkName,
PortBindings: {
'1633/tcp': [{ HostPort: '1633' }],
'1634/tcp': [{ HostPort: '1634' }],
'1635/tcp': [{ HostPort: '1635' }],
},
},
});
this.runningContainers.push(container);
const state = yield container.inspect();
// If it is already running (because of whatever reason) we are not spawning new node.
// Already in `findOrCreateContainer` the container is verified that it was spawned with expected version.
if (!state.State.Running) {
yield container.start();
}
else {
this.console.info('The Queen node container was already running, so not starting it again.');
}
});
}
startWorkerNode(beeVersion, workerNumber, queenAddress, options) {
return __awaiter(this, void 0, void 0, function* () {
if (options.fresh)
yield this.removeContainer(this.workerName(workerNumber));
yield this.pullImageIfNotFound(this.workerImage(beeVersion, workerNumber));
const contractAddresses = yield this.getContractAddresses(this.workerImage(beeVersion, workerNumber));
const container = yield this.findOrCreateContainer(this.workerName(workerNumber), {
Image: this.workerImage(beeVersion, workerNumber),
name: this.workerName(workerNumber),
ExposedPorts: {
'1633/tcp': {},
'1634/tcp': {},
'1635/tcp': {},
},
Cmd: ['start'],
Env: this.createBeeEnvParameters(contractAddresses, queenAddress),
AttachStderr: false,
AttachStdout: false,
HostConfig: {
NetworkMode: this.networkName,
PortBindings: {
'1633/tcp': [{ HostPort: (1633 + workerNumber * 10000).toString() }],
'1634/tcp': [{ HostPort: (1634 + workerNumber * 10000).toString() }],
'1635/tcp': [{ HostPort: (1635 + workerNumber * 10000).toString() }],
},
},
});
this.runningContainers.push(container);
const state = yield container.inspect();
// If it is already running (because of whatever reason) we are not spawning new node
if (!state.State.Running) {
yield container.start();
}
else {
this.console.info('The Queen node container was already running, so not starting it again.');
}
});
}
logs(target, outputStream, follow = false, tail) {
return __awaiter(this, void 0, void 0, function* () {
const { container } = yield this.findContainer(this.getContainerName(target));
if (!container) {
throw new Error('Queen container does not exists, even though it should have had!');
}
const logs = yield container.logs({ stdout: true, stderr: true, follow, tail });
if (!follow) {
outputStream.write(logs);
}
else {
logs.pipe(outputStream);
}
});
}
stopAll(allWithPrefix = false, deleteContainers = false) {
return __awaiter(this, void 0, void 0, function* () {
const containerProcessor = (container) => __awaiter(this, void 0, void 0, function* () {
try {
yield container.stop();
}
catch (e) {
// We ignore 304 that represents that the container is already stopped
if (e.statusCode !== 304) {
throw e;
}
}
if (deleteContainers) {
yield container.remove();
}
});
this.console.info('Stopping all containers');
yield Promise.all(this.runningContainers.map(containerProcessor));
if (allWithPrefix) {
const containers = yield this.docker.listContainers({ all: true });
yield Promise.all(containers
.filter(container => container.Names.filter(n => n.startsWith('/' + this.envPrefix)).length >= 1)
.map(container => this.docker.getContainer(container.Id))
.map(containerProcessor));
}
});
}
getBlockchainVersionFromQueenMetadata(beeVersion) {
return __awaiter(this, void 0, void 0, function* () {
// Lets pull the Queen's image if it is not present
yield this.pullImageIfNotFound(this.queenImage(beeVersion));
const queenMetadata = yield this.docker.getImage(this.queenImage(beeVersion)).inspect();
const version = queenMetadata.Config.Labels[exports.BLOCKCHAIN_VERSION_LABEL_KEY];
if (!version) {
throw new Error('Blockchain image version was not found in Queen image labels!');
}
return version;
});
}
getAllStatus() {
return __awaiter(this, void 0, void 0, function* () {
return {
queen: yield this.getStatusForContainer(ContainerType.QUEEN),
blockchain: yield this.getStatusForContainer(ContainerType.BLOCKCHAIN),
worker1: yield this.getStatusForContainer(ContainerType.WORKER_1),
worker2: yield this.getStatusForContainer(ContainerType.WORKER_2),
worker3: yield this.getStatusForContainer(ContainerType.WORKER_3),
worker4: yield this.getStatusForContainer(ContainerType.WORKER_4),
};
});
}
removeContainer(name) {
return __awaiter(this, void 0, void 0, function* () {
this.console.info(`Removing container with name "${name}"`);
const { container } = yield this.findContainer(name);
// Container does not exist so nothing to delete
if (!container) {
return;
}
yield container.remove({ v: true, force: true });
});
}
findOrCreateContainer(name, createOptions) {
return __awaiter(this, void 0, void 0, function* () {
const { container, image: foundImage } = yield this.findContainer(name);
if (container) {
this.console.info(`Container with name "${name}" found. Using it.`);
if (foundImage !== createOptions.Image) {
throw new error_1.ContainerImageConflictError(`Container with name "${name}" found but it was created with different image or image version then expected!`, foundImage, createOptions.Image);
}
return container;
}
this.console.info(`Container with name "${name}" not found. Creating new one.`);
try {
return yield this.docker.createContainer(createOptions);
}
catch (e) {
// 404 is Image Not Found ==> pull the image
if (e.statusCode !== 404) {
throw e;
}
this.console.info(`Image ${createOptions.Image} not found. Pulling it.`);
yield this.pullImageIfNotFound(createOptions.Image);
return yield this.docker.createContainer(createOptions);
}
});
}
findContainer(name) {
return __awaiter(this, void 0, void 0, function* () {
const containers = yield this.docker.listContainers({ all: true, filters: { name: [name] } });
if (containers.length === 0) {
return {};
}
if (containers.length > 1) {
throw new Error(`Found ${containers.length} containers for name "${name}". Expected only one.`);
}
return { container: this.docker.getContainer(containers[0].Id), image: containers[0].Image };
});
}
getStatusForContainer(name) {
return __awaiter(this, void 0, void 0, function* () {
const foundContainer = yield this.findContainer(this.getContainerName(name));
if (!foundContainer.container) {
return 'not-found';
}
const inspectStatus = yield foundContainer.container.inspect();
if (inspectStatus.State.Running) {
return 'running';
}
return 'exists';
});
}
getContainerName(name) {
switch (name) {
case ContainerType.BLOCKCHAIN:
return this.blockchainName;
case ContainerType.QUEEN:
return this.queenName;
case ContainerType.WORKER_1:
return this.workerName(1);
case ContainerType.WORKER_2:
return this.workerName(2);
case ContainerType.WORKER_3:
return this.workerName(3);
case ContainerType.WORKER_4:
return this.workerName(4);
default:
throw new Error('Unknown container!');
}
}
createBeeEnvParameters(contractAddresses, bootnode) {
const options = Object.assign({ 'warmup-time': '0', 'debug-api-enable': 'true', verbosity: '4', 'swap-enable': 'true', mainnet: 'false', 'swap-endpoint': `http://${this.blockchainName}:9545`, password: 'password', 'network-id': '4020', 'full-node': 'true', 'welcome-message': 'You have found the queen of the beehive...', 'cors-allowed-origins': '*', 'postage-stamp-start-block': '1' }, contractAddresses);
if (bootnode) {
options.bootnode = bootnode;
}
// Env variables for Bee has form of `BEE_WARMUP_TIME`, so we need to transform it.
return Object.entries(options).reduce((previous, current) => {
const keyName = `BEE_${current[0].toUpperCase().replace(/-/g, '_')}`;
previous.push(`${keyName}=${current[1]}`);
return previous;
}, []);
}
pullImageIfNotFound(name) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield this.docker.getImage(name).inspect();
}
catch (e) {
this.console.info(`Image ${name} not found locally, pulling it.`);
const pullStream = yield this.docker.pull(name);
yield new Promise(res => this.docker.modem.followProgress(pullStream, res));
}
});
}
getContractAddresses(imageName) {
return __awaiter(this, void 0, void 0, function* () {
const imageMetadata = yield this.docker.getImage(imageName).inspect();
const contractAddresses = {};
// @ts-ignore: Dockerode typings does not have iterator even though it is a simple object
for (const [labelKey, labelValue] of Object.entries(imageMetadata.Config.Labels)) {
if (labelKey.startsWith(exports.CONTRACT_LABEL_KEY_PREFIX)) {
contractAddresses[labelKey.replace(exports.CONTRACT_LABEL_KEY_PREFIX, '')] = labelValue;
}
}
return contractAddresses;
});
}
}
exports.Docker = Docker;