feat: ts cli orchestrator (#81)

This commit is contained in:
Adam Uhlíř 2022-04-29 15:51:09 +02:00 committed by GitHub
parent 9ac5b11421
commit 360f3e07c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 45052 additions and 29034 deletions

6
.editorconfig Normal file
View File

@ -0,0 +1,6 @@
[*]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
dist/**

109
.eslintrc.js Normal file
View File

@ -0,0 +1,109 @@
module.exports = {
extends: ['plugin:@typescript-eslint/recommended', 'prettier', 'plugin:prettier/recommended'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
project: './tsconfig.test.json',
},
env: {
jest: true,
},
globals: {
browser: true,
page: true,
},
plugins: ['jest', 'unused-imports'],
rules: {
'array-bracket-newline': ['error', 'consistent'],
strict: ['error', 'safe'],
'block-scoped-var': 'error',
complexity: 'warn',
'default-case': 'error',
'dot-notation': 'warn',
eqeqeq: 'error',
'guard-for-in': 'warn',
'linebreak-style': ['warn', 'unix'],
'no-alert': 'error',
'no-case-declarations': 'error',
'no-console': 'error',
'no-constant-condition': 'error',
'no-continue': 'warn',
'no-div-regex': 'error',
'no-empty': 'warn',
'no-empty-pattern': 'error',
'no-implicit-coercion': 'error',
'prefer-arrow-callback': 'warn',
'no-labels': 'error',
'no-loop-func': 'error',
'no-nested-ternary': 'warn',
'no-script-url': 'error',
'no-warning-comments': 'warn',
'quote-props': ['error', 'as-needed'],
'require-yield': 'error',
'max-nested-callbacks': ['error', 4],
'max-depth': ['error', 4],
'space-before-function-paren': [
'error',
{
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
},
],
'padding-line-between-statements': [
'error',
{ blankLine: 'always', prev: '*', next: 'if' },
{ blankLine: 'always', prev: '*', next: 'function' },
{ blankLine: 'always', prev: '*', next: 'return' },
],
'no-useless-constructor': 'off',
'no-dupe-class-members': 'off',
'no-unused-expressions': 'off',
curly: ['error', 'multi-line'],
'object-curly-spacing': ['error', 'always'],
'comma-dangle': ['error', 'always-multiline'],
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-unused-expressions': 'error',
'@typescript-eslint/member-delimiter-style': [
'error',
{
multiline: {
delimiter: 'none',
requireLast: true,
},
singleline: {
delimiter: 'semi',
requireLast: false,
},
},
],
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-expect-error': 'allow-with-description',
'ts-ignore': 'allow-with-description',
'ts-nocheck': 'allow-with-description',
'ts-check': 'allow-with-description',
minimumDescriptionLength: 6,
},
],
'require-await': 'off',
'@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'warn',
{ vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
],
},
overrides: [
{
files: ['*.spec.ts'],
rules: {
'max-nested-callbacks': ['error', 10], // allow describe/it nesting
},
},
],
}

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

54
.github/workflows/check.yaml vendored Normal file
View File

@ -0,0 +1,54 @@
name: Check
on:
push:
branches:
- 'master'
pull_request:
branches:
- '**'
jobs:
check:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
## Try getting the node modules from cache, if failed npm ci
- uses: actions/cache@v2
id: cache-npm
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.OS }}-node-${{ env.cache-name }}-
${{ runner.OS }}-node-
${{ runner.OS }}-
- name: Install npm deps
if: steps.cache-npm.outputs.cache-hit != 'true'
run: npm ci
- name: Commit linting
uses: wagoid/commitlint-github-action@v2
- name: Code linting
run: npm run lint:check
env:
CI: true
- name: Types check
run: npm run types:check
- name: Build nodejs code
run: npm run build

View File

@ -67,6 +67,8 @@ jobs:
- name: Build images
id: build
run: |
cd ./generator
BUILD_PARAMS=""
if [ $BUILD_IMAGE == 'true' ] ; then
BUILD_PARAMS+=" --build-base-bee --base-bee-commit-hash=$BEE_VERSION"

42
.github/workflows/test.yaml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Node.js tests
on:
push:
branches:
- 'master'
pull_request:
branches:
- '**'
jobs:
node-tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
## Try getting the node modules from cache, if failed npm ci
- uses: actions/cache@v2
id: cache-npm
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.OS }}-node-${{ matrix.node }}-${{ env.cache-name }}-
${{ runner.OS }}-node-${{ matrix.node }}-
- name: Install npm deps
if: steps.cache-npm.outputs.cache-hit != 'true'
run: npm ci
- name: Run node tests
run: npm test -- --bail true

34
.gitignore vendored
View File

@ -1,4 +1,30 @@
node_modules/
build/
# In case of building bee image, the bee source code will be cloned to this folder
bee/
# Logs
logs
*.log
# OS files
.DS_Store
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
.nyc_output
# node-waf configuration
.lock-wscript
build
dist
# Dependency directory
node_modules
# Generated files
docs

27
LICENSE Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2022 The Swarm Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Swarm nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

189
README.md
View File

@ -1,136 +1,73 @@
# Bee Factory
This project builds up a test environment with Bee clients and with a test blockchain.
The created environment is runnable on local machine as well.
All services run in `Docker` containers only.
[![Tests](https://github.com/ethersphere/bee-factory/actions/workflows/tests.yaml/badge.svg)](https://github.com/ethersphere/bee-factory/actions/workflows/tests.yaml)
[![Dependency Status](https://david-dm.org/ethersphere/bee-factory.svg?style=flat-square)](https://david-dm.org/ethersphere/bee-factory)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fethersphere%2Fbee-factory.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fethersphere%2Fbee-factory?ref=badge_shield)
[![](https://img.shields.io/badge/made%20by-Swarm-blue.svg?style=flat-square)](https://swarm.ethereum.org/)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard)
![](https://img.shields.io/badge/npm-%3E%3D6.9.0-orange.svg?style=flat-square)
![](https://img.shields.io/badge/Node.js-%3E%3D12.0.0-orange.svg?style=flat-square)
Currently, the repository supports running Bee nodes up to 5 by default.
> CLI tool to spin up Docker cluster of Bee nodes for advanced testing and/or development
**Warning: This project is in beta state. There might (and most probably will) be changes in the future to its API and working. Also, no guarantees can be made about its stability, efficiency, and security at this stage.**
## Table of Contents
- [Install](#install)
- [Usage](#usage)
- [Contribute](#contribute)
- [License](#license)
## Install
**Requirements:** Docker
```shell
$ npm install -g @ethersphere/bee-factory
```
## Usage
```shell
# This spin up the cluster and exits
$ bee-factory start --detach 1.2.0 1.5.1-d0a77598-stateful
# This attaches to the Queen container and displays its logs
$ bee-factory logs queen
# This stops the cluster and keeping the containers so next time they are spinned up the data are kept
# but data are not persisted across version's bump!
$ bee-factory stop
# You can also spin up the cluster without the --detach which then directly
# attaches to the Queen logs and the cluster is terminated upon SIGINT (Ctrl+C)
$ bee-factory start 1.2.0 1.5.1-d0a77598-stateful
```
For more details see the `--help` page of the CLI and its commands.
## Contribute
There are some ways you can make this module better:
- Consult our [open issues](https://github.com/ethersphere/bee-factory/issues) and take on one of them
- Help our tests reach 100% coverage!
- Join us in our [Discord chat](https://discord.gg/wdghaQsGq5) in the #develop-on-swarm channel if you have questions or want to give feedback
### Developing
You can run the CLI while developing using `npm start -- <command> ...`.
## Maintainers
- [nugaon](https://github.com/nugaon)
- [Cafe137](https://github.com/Cafe137)
- [auhau](https://github.com/auhau)
- [Cafe137](https://github.com/cafe137)
See what "Maintainer" means [here](https://github.com/ethersphere/repo-maintainer).
## License
## Usage
The whole Bee environment (with blockchain) can be started by [running one script](###Run-Environment),
but for that you need to have the necessary Docker images, which is possible to build yourself by [running some scripts](###Setup-the-environment)
[BSD-3-Clause](./LICENSE)
First you may want to set all global variables that the scripts will use.
For that, there is a [.env](scripts/.env) file which contains all necessary variables that you need.
```sh
set -a && source ./scripts/.env && set +a
```
If you do not set these global variables, the scripts will use those which are available in the [.env](scripts/.env) file.
## Setup the environment
Create the common Docker network for the environment with
```sh
./scripts/network.sh
```
To start the blockchain, run the following command in the root directory of the project:
```sh
./scripts/blockchain.sh
```
After that, it's possible to deploy Swarm smart contracts
```sh
npm run migrate:contracts
```
Before you start the Bee nodes with the deployed Swap Factory, you have to fund your overlay addresses of your Bee nodes for the successful start.
The [supply.js](src/supply.js) script can fund the addresses which are defined in [bee-overlay-addresses.json](bee-overlay-addresses.json) file.
To run this script just execute
```sh
npm run supply
```
and the configured accounts will get 1 ether and 100 BZZ Token.
After all above went successfully you can start the Bee nodes
```sh
./scripts/bee.sh start --workers=4
```
OR it is possible to build docker images on a desired state, so that a fresh environment can be started on each run.
### Build Docker Images
Basically, a full-featured Bee environment has 2 types of Docker image:
- Bee images: Bee clients with pre-defined keys (and optionally including the state which you nodes have in its [data-dirs](scripts/bee-data-dirs))
```sh
./scripts/bee-docker-build.sh
```
- Blockchain image: Ganache blockchain which you may want to take a snapshot of after the contracts are deployed and the pre-defined Bee client keys are funded already.
```sh
./scripts/blockchain-docker-build.sh
```
## Run Environment
If you have all Docker images that your [environment file](scripts/.env) requires,
start the Bee cluster
```sh
./scripts/environment.sh start
```
### Restricted API
If you want to enable permission check feature of Bee on the API endpoints you can use `--restrict` flag. This will
use default password `SwarmToTheMoon` or if you want you can pass your own password as `--restrict=someOtherPassword`.
This feature requires to have `htpasswd` command available which is part of the `apache2-utils` package.
### Pull images
Bee Factory can build images for CIs, but it is also possible to pull image to your computer as well.
For that you have to login to the Github docker registry with
```sh
docker login docker.pkg.github.com
```
it will ask for your _GitHub_ username and for the password. For the latter you can generate a [Personal Access Token](https://github.com/settings/tokens).
The suggested permissions for the token are `read:org` and `read:packages`.
## Utilities
It is possible to generate random traffic in your cluster:
```sh
$ npm run gen:traffic
```
The script is in an infinite loop, so if you want to stop the generation you have to terminate it manually in your terminal by pressing `Ctrl^C`.
If you don't specify any parameters it will produce 400 chunks/0.5 sec that the script tries to upload on the `http://localhost:1633` - that is the binded port of the queen node if you orchestrated the environment with the `envrionment.sh`.
The following way you can pass parameter
1. MIN_CHEQUE_NUMBER - Minimum required cheques for Bee under the given BEE_DEBUG_API_URL. If -1 then it does not check for cheques [Number,Default:-1]
2. BEE_API_URL;BEE_DEBUG_API_URL - Bee API and Debug API URL separated by semicolon. The random data will sent to the Bee API URL, and the generated cheques will be checked on the Bee Debug URL. The two URLs should belong to different Bee clients as the generated data will propagate from that client to the network. [string,Default:'http://localhost:1633;http://localhost:11635']
```sh
$ npm run gen:traffic -- <MIN_CHEQUE_NUMBER> <BEE_API_URL;BEE_DEBUG_API_URL> <BEE_API_URL;BEE_DEBUG_API_URL> (...)
```
e.g.
```sh
$ npm run gen:traffic -- 2 http://localhost:1633;http://localhost:11635
```
With the example above, random data will be generated until _minimum_ two cheques will generated on Bee client that serves debug endpoint `http://localhost:11635`
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fethersphere%2Fbee-factory.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fethersphere%2Fbee-factory?ref=badge_large)

6
commitlint.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'body-max-line-length': [0, 'always', Infinity], // disable commit body length restriction
},
}

136
generator/README.md Normal file
View File

@ -0,0 +1,136 @@
# Bee Factory Generator
This project builds up a test environment with Bee clients and with a test blockchain.
The created environment is runnable on local machine as well.
All services run in `Docker` containers only.
Currently, the repository supports running Bee nodes up to 5 by default.
## Maintainers
- [nugaon](https://github.com/nugaon)
- [Cafe137](https://github.com/Cafe137)
See what "Maintainer" means [here](https://github.com/ethersphere/repo-maintainer).
## Usage
The whole Bee environment (with blockchain) can be started by [running one script](###Run-Environment),
but for that you need to have the necessary Docker images, which is possible to build yourself by [running some scripts](###Setup-the-environment)
First you may want to set all global variables that the scripts will use.
For that, there is a [.env](scripts/.env) file which contains all necessary variables that you need.
```sh
set -a && source ./scripts/.env && set +a
```
If you do not set these global variables, the scripts will use those which are available in the [.env](scripts/.env) file.
## Setup the environment
Create the common Docker network for the environment with
```sh
./scripts/network.sh
```
To start the blockchain, run the following command in the root directory of the project:
```sh
./scripts/blockchain.sh
```
After that, it's possible to deploy Swarm smart contracts
```sh
npm run migrate:contracts
```
Before you start the Bee nodes with the deployed Swap Factory, you have to fund your overlay addresses of your Bee nodes for the successful start.
The [supply.js](src/supply.js) script can fund the addresses which are defined in [bee-overlay-addresses.json](bee-overlay-addresses.json) file.
To run this script just execute
```sh
npm run supply
```
and the configured accounts will get 1 ether and 100 BZZ Token.
After all above went successfully you can start the Bee nodes
```sh
./scripts/bee.sh start --workers=4
```
OR it is possible to build docker images on a desired state, so that a fresh environment can be started on each run.
### Build Docker Images
Basically, a full-featured Bee environment has 2 types of Docker image:
- Bee images: Bee clients with pre-defined keys (and optionally including the state which you nodes have in its [data-dirs](bee-data-dirs))
```sh
./scripts/bee-docker-build.sh
```
- Blockchain image: Ganache blockchain which you may want to take a snapshot of after the contracts are deployed and the pre-defined Bee client keys are funded already.
```sh
./scripts/blockchain-docker-build.sh
```
## Index Environment
If you have all Docker images that your [environment file](scripts/.env) requires,
start the Bee cluster
```sh
./scripts/environment.sh start
```
### Restricted API
If you want to enable permission check feature of Bee on the API endpoints you can use `--restrict` flag. This will
use default password `SwarmToTheMoon` or if you want you can pass your own password as `--restrict=someOtherPassword`.
This feature requires to have `htpasswd` command available which is part of the `apache2-utils` package.
### Pull images
Bee Factory can build images for CIs, but it is also possible to pull image to your computer as well.
For that you have to login to the Github docker registry with
```sh
docker login docker.pkg.github.com
```
it will ask for your _GitHub_ username and for the password. For the latter you can generate a [Personal Access Token](https://github.com/settings/tokens).
The suggested permissions for the token are `read:org` and `read:packages`.
## Utilities
It is possible to generate random traffic in your cluster:
```sh
$ npm run gen:traffic
```
The script is in an infinite loop, so if you want to stop the generation you have to terminate it manually in your terminal by pressing `Ctrl^C`.
If you don't specify any parameters it will produce 400 chunks/0.5 sec that the script tries to upload on the `http://localhost:1633` - that is the binded port of the queen node if you orchestrated the environment with the `envrionment.sh`.
The following way you can pass parameter
1. MIN_CHEQUE_NUMBER - Minimum required cheques for Bee under the given BEE_DEBUG_API_URL. If -1 then it does not check for cheques [Number,Default:-1]
2. BEE_API_URL;BEE_DEBUG_API_URL - Bee API and Debug API URL separated by semicolon. The random data will sent to the Bee API URL, and the generated cheques will be checked on the Bee Debug URL. The two URLs should belong to different Bee clients as the generated data will propagate from that client to the network. [string,Default:'http://localhost:1633;http://localhost:11635']
```sh
$ npm run gen:traffic -- <MIN_CHEQUE_NUMBER> <BEE_API_URL;BEE_DEBUG_API_URL> <BEE_API_URL;BEE_DEBUG_API_URL> (...)
```
e.g.
```sh
$ npm run gen:traffic -- 2 http://localhost:1633;http://localhost:11635
```
With the example above, random data will be generated until _minimum_ two cheques will generated on Bee client that serves debug endpoint `http://localhost:11635`

33050
generator/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
generator/package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "bee-factory",
"version": "0.0.1",
"description": "Setup Swarm Test Blockchain and arbitrary numbered Bee nodes",
"scripts": {
"compile:contracts": "truffle compile",
"migrate:contracts": "truffle migrate",
"supply": "truffle exec src/supply.js",
"build:env": "./scripts/build-environment.sh",
"publish:env": "./scripts/publish-environment.sh",
"run:env": "./scripts/environment.sh",
"setApiVersions": "node ./src/setApiVersions.js",
"gen:traffic": "node ./scripts/gen-traffic.js"
},
"dependencies": {
"@ethersphere/bee-js": "^3.2.0",
"@openzeppelin/contracts": "^3.1.0",
"truffle": "^5.3.5"
},
"keywords": [
"swarm",
"ethereum-swarm",
"bee-factory"
],
"license": "BSD-3-Clause"
}

43
jest.config.ts Normal file
View File

@ -0,0 +1,43 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/en/configuration.html
*/
import type { Config } from '@jest/types'
export default async (): Promise<Config.InitialOptions> => {
return {
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// An array of regexp pattern strings used to skip coverage collection
coveragePathIgnorePatterns: ['/node_modules/'],
// An array of directory names to be searched recursively up from the requiring module's location
moduleDirectories: ['node_modules'],
// Run tests from one or more projects
projects: [
{
displayName: 'integration',
testEnvironment: 'node',
preset: 'ts-jest',
// Because of integration tests we have to run the tests serially
runner: '@codejedi365/jest-serial-runner',
testRegex: 'test/integration/.*\\.spec\\.ts',
},
] as unknown[] as string[], // bad types
// The root directory that Jest should scan for tests and modules within
rootDir: 'test',
// Increase timeout since we are spinning Bee containers
testTimeout: 4 * 60 * 1000,
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
testPathIgnorePatterns: ['/node_modules/'],
}
}

38983
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,78 @@
{
"name": "bee-factory",
"version": "0.0.1",
"description": "Setup Swarm Test Blockchain and arbitrary numbered Bee nodes",
"name": "@ethersphere/bee-factory",
"version": "0.1.0",
"description": "Orchestration CLI for spinning up local development Bee cluster with Docker",
"keywords": [
"bee",
"swarm",
"decentralised",
"storage",
"ethereum",
"typescript",
"p2p",
"docker"
],
"homepage": "https://github.com/ethersphere/bee-factory",
"bugs": {
"url": "https://github.com/ethersphere/bee-factory/issues/"
},
"license": "BSD-3-Clause",
"repository": {
"type": "git",
"url": "https://github.com/ethersphere/bee-factory.git"
},
"bin": {
"bee-factory": "./dist/index.js"
},
"main": "dist/index.js",
"files": [
"dist"
],
"scripts": {
"compile:contracts": "truffle compile",
"migrate:contracts": "truffle migrate",
"supply": "truffle exec src/supply.js",
"build:env": "./scripts/build-environment.sh",
"publish:env": "./scripts/publish-environment.sh",
"run:env": "./scripts/environment.sh",
"setApiVersions": "node ./src/setApiVersions.js",
"gen:traffic": "node ./scripts/gen-traffic.js"
"prepublishOnly": "cross-env NODE_ENV=production npm run build",
"build": "rimraf dist && tsc",
"start": "ts-node src/index.ts",
"test": "jest --verbose --config=jest.config.ts",
"types:check": "tsc --project tsconfig.test.json",
"lint": "eslint --fix \"src/**/*.ts\" \"test/**/*.ts\" && prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"lint:check": "eslint \"src/**/*.ts\" \"test/**/*.ts\" && prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
"depcheck": "depcheck ."
},
"dependencies": {
"@ethersphere/bee-js": "^3.2.0",
"@openzeppelin/contracts": "^3.1.0",
"truffle": "^5.3.5"
"chalk": "^2.4.2",
"dockerode": "^3.3.1",
"furious-commander": "^1.7.0",
"node-fetch": "3.0.0-beta.9",
"ora": "^5.3.0"
},
"keywords": [
"swarm",
"ethereum-swarm",
"bee-factory"
],
"license": "BSD-3-Clause"
"devDependencies": {
"@codejedi365/jest-serial-runner": "^2.0.0",
"@commitlint/cli": "^16.2.1",
"@commitlint/config-conventional": "^16.2.1",
"@ethersphere/bee-js": "^3.3.4",
"@fluffy-spoon/substitute": "^1.208.0",
"@jest/types": "^27.0.2",
"@types/dockerode": "^3.3.3",
"@types/jest": "^27.4.1",
"@types/node": "^15.12.4",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"depcheck": "^1.4.3",
"eslint": "^8.8.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-jest": "^25.7.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"glob": "^7.1.7",
"jest": "^27.4.7",
"prettier": "^2.3.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.4",
"ts-node": "^10.7.0",
"typescript": "^4.5.5"
},
"engines": {
"node": ">=12.0.0",
"npm": ">=6.0.0"
}
}

10
src/application.ts Normal file
View File

@ -0,0 +1,10 @@
import { Application } from 'furious-commander/dist/application'
import PackageJson from '../package.json'
export const application: Application = {
name: 'Bee Factory',
command: 'bee-factory',
description: 'Orchestration CLI for spinning up local development Bee cluster with Docker',
version: PackageJson.version,
autocompletion: 'fromOption',
}

60
src/command/logs.ts Normal file
View File

@ -0,0 +1,60 @@
import { Argument, LeafCommand, Option } from 'furious-commander'
import { RootCommand } from './root-command'
import { ContainerType, DEFAULT_ENV_PREFIX, DEFAULT_IMAGE_PREFIX, Docker } from '../utils/docker'
export class Logs extends RootCommand implements LeafCommand {
public readonly name = 'logs'
public readonly description = `Prints logs for given container. Valid container's names are: ${Object.values(
ContainerType,
).join(', ')}`
@Option({
key: 'image-prefix',
type: 'string',
description: 'Docker image name prefix',
envKey: 'FACTORY_IMAGE_PREFIX',
default: DEFAULT_IMAGE_PREFIX,
})
public imagePrefix!: string
@Option({
key: 'env-prefix',
type: 'string',
description: "Docker container's names prefix",
envKey: 'FACTORY_ENV_PREFIX',
default: DEFAULT_ENV_PREFIX,
})
public envPrefix!: string
@Option({
key: 'follow',
alias: 'f',
type: 'boolean',
description: 'Stays attached to the container and output any new logs.',
default: false,
})
public follow!: boolean
@Option({
key: 'tail',
alias: 't',
type: 'number',
description: 'Prints specified number of last log lines.',
})
public tail!: number
@Argument({ key: 'container', description: 'Container name as described above', required: true })
public container!: ContainerType
public async run(): Promise<void> {
await super.init()
if (!Object.values(ContainerType).includes(this.container)) {
throw new Error(`Passed container name is not valid! Valid values: ${Object.values(ContainerType).join(', ')}`)
}
const docker = new Docker(this.console, this.envPrefix, this.imagePrefix)
await docker.logs(this.container, process.stdout, this.follow, this.tail)
}
}

View File

@ -0,0 +1,29 @@
import { ExternalOption } from 'furious-commander'
import { Logging, VerbosityLevel } from './logging'
export class RootCommand {
@ExternalOption('quiet')
public quiet!: boolean
@ExternalOption('verbose')
public verbose!: boolean
public verbosity!: VerbosityLevel
public console!: Logging
public readonly appName = 'bee-factory'
protected async init(): Promise<void> {
this.verbosity = VerbosityLevel.Normal
if (this.quiet) {
this.verbosity = VerbosityLevel.Quiet
}
if (this.verbose) {
this.verbosity = VerbosityLevel.Verbose
}
this.console = new Logging(this.verbosity)
}
}

View File

@ -0,0 +1,45 @@
import { Printer } from './printer'
export enum VerbosityLevel {
/** No output message, only at errors or result strings (e.g. hash of uploaded file) */
Quiet,
/** Formatted informal messages at end of operations, output row number is equal at same operations */
Normal,
/** dim messages, gives info about state of the operation frequently. Default */
Verbose,
}
type PrinterFnc = (message: string, ...args: unknown[]) => void
export class Logging {
public readonly verbosityLevel: VerbosityLevel
// Callable logging functions (instead of console.log)
/** Error messages */
public error: PrinterFnc
/** Identical with console.log */
public log: PrinterFnc
/** Informal messages (e.g. Tips) */
public info: PrinterFnc
constructor(verbosityLevel: VerbosityLevel) {
this.verbosityLevel = verbosityLevel
switch (verbosityLevel) {
case VerbosityLevel.Verbose:
this.error = Printer.error
this.log = Printer.log
this.info = Printer.info
break
case VerbosityLevel.Normal:
this.error = Printer.error
this.log = Printer.log
this.info = Printer.emptyFunction
break
default:
// quiet
this.error = Printer.error
this.log = Printer.emptyFunction
this.info = Printer.emptyFunction
}
}
}

View File

@ -0,0 +1,25 @@
/* eslint-disable no-console */
import chalk from 'chalk'
export const FORMATTED_ERROR = chalk.red.bold('ERROR')
export const Printer = {
emptyFunction: (): void => {
return
},
divider: (char = '-'): void => {
console.log(char.repeat(process.stdout.columns))
},
error: (message: string, ...args: unknown[]): void => {
console.error(message, ...args)
},
log: (message: string, ...args: unknown[]): void => {
console.log(message, ...args)
},
info: (message: string, ...args: unknown[]): void => {
console.log(chalk.dim(message), ...args)
},
dimFunction: (message: string, ...args: unknown[]): void => {
console.log(chalk.dim(message), ...args)
},
}

208
src/command/start.ts Normal file
View File

@ -0,0 +1,208 @@
import { Argument, LeafCommand, Option } from 'furious-commander'
import { RootCommand } from './root-command'
import {
ContainerType,
DEFAULT_ENV_PREFIX,
DEFAULT_IMAGE_PREFIX,
Docker,
RunOptions,
WORKER_COUNT,
} from '../utils/docker'
import { waitForBlockchain, waitForQueen, waitForWorkers } from '../utils/wait'
import ora from 'ora'
import { VerbosityLevel } from './root-command/logging'
const DEFAULT_REPO = 'ghcr.io/ethersphere/bee-factory'
export const ENV_ENV_PREFIX_KEY = 'FACTORY_ENV_PREFIX'
const ENV_IMAGE_PREFIX_KEY = 'FACTORY_IMAGE_PREFIX'
const ENV_REPO_KEY = 'FACTORY_DOCKER_REPO'
const ENV_DETACH_KEY = 'FACTORY_DETACH'
const ENV_FRESH_KEY = 'FACTORY_FRESH'
export class Start extends RootCommand implements LeafCommand {
public readonly name = 'start'
public readonly description = 'Spin up the Bee Factory cluster'
@Option({
key: 'fresh',
alias: 'f',
type: 'boolean',
description: 'The cluster data will be purged before start',
envKey: ENV_FRESH_KEY,
default: false,
})
public fresh!: boolean
@Option({
key: 'detach',
alias: 'd',
type: 'boolean',
description: 'Spin up the cluster and exit. No logging is outputted.',
envKey: ENV_DETACH_KEY,
default: false,
})
public detach!: boolean
@Option({
key: 'repo',
type: 'string',
description: 'Docker repo',
envKey: ENV_REPO_KEY,
default: DEFAULT_REPO,
})
public repo!: string
@Option({
key: 'image-prefix',
type: 'string',
description: 'Docker image name prefix',
envKey: ENV_IMAGE_PREFIX_KEY,
default: DEFAULT_IMAGE_PREFIX,
})
public imagePrefix!: string
@Option({
key: 'env-prefix',
type: 'string',
description: "Docker container's names prefix",
envKey: ENV_ENV_PREFIX_KEY,
default: DEFAULT_ENV_PREFIX,
})
public envPrefix!: string
@Argument({ key: 'blockchain-version', description: 'Blockchain image version', required: true })
public blockchainVersion!: string
@Argument({ key: 'bee-version', description: 'Bee image version', required: true })
public beeVersion!: string
public async run(): Promise<void> {
await super.init()
const dockerOptions = await this.buildDockerOptions()
const docker = new Docker(this.console, this.envPrefix, this.imagePrefix)
const status = await docker.getAllStatus()
if (Object.values(status).every(st => st === 'running')) {
this.console.log('All containers are up and running')
if (this.detach) {
return
}
await docker.logs(ContainerType.QUEEN, process.stdout)
}
let queenAddress: string
process.on('SIGINT', async () => {
try {
await docker.stopAll(false)
} catch (e) {
this.console.error(`Error: ${e}`)
}
process.exit()
})
const networkSpinner = ora({
text: 'Spawning network...',
spinner: 'point',
color: 'yellow',
isSilent: this.verbosity !== VerbosityLevel.Quiet,
}).start()
try {
await docker.createNetwork()
networkSpinner.succeed('Network is up')
} catch (e) {
networkSpinner.fail(`It was not possible to spawn network!`)
throw e
}
const blockchainSpinner = ora({
text: 'Starting blockchain node...',
spinner: 'point',
color: 'yellow',
isSilent: this.verbosity !== VerbosityLevel.Quiet,
}).start()
try {
await docker.startBlockchainNode(this.blockchainVersion, this.beeVersion, dockerOptions)
blockchainSpinner.text = 'Waiting until blockchain is ready...'
await waitForBlockchain()
blockchainSpinner.succeed('Blockchain node is up and listening')
} catch (e) {
blockchainSpinner.fail(`It was not possible to start blockchain node!`)
await this.stopDocker(docker)
throw e
}
const queenSpinner = ora({
text: 'Starting queen Bee node...',
spinner: 'point',
color: 'yellow',
isSilent: this.verbosity !== VerbosityLevel.Quiet,
}).start()
try {
await docker.startQueenNode(this.beeVersion, dockerOptions)
queenSpinner.text = 'Waiting until queen node is ready...'
queenAddress = await waitForQueen(
async () => (await docker.getStatusForContainer(ContainerType.QUEEN)) === 'running',
)
queenSpinner.succeed('Queen node is up and listening')
} catch (e) {
queenSpinner.fail(`It was not possible to start queen node!`)
await this.stopDocker(docker)
throw e
}
const workerSpinner = ora({
text: 'Starting worker Bee nodes...',
spinner: 'point',
color: 'yellow',
isSilent: this.verbosity !== VerbosityLevel.Quiet,
}).start()
try {
for (let i = 1; i <= WORKER_COUNT; i++) {
await docker.startWorkerNode(this.beeVersion, i, queenAddress, dockerOptions)
}
workerSpinner.text = 'Waiting until all workers connect to queen...'
await waitForWorkers(async () => Object.values(await docker.getAllStatus()).every(node => node === 'running'))
workerSpinner.succeed('Worker nodes are up and listening')
} catch (e) {
workerSpinner.fail(`It was not possible to start worker nodes!`)
await this.stopDocker(docker)
throw e
}
if (!this.detach) {
await docker.logs(ContainerType.QUEEN, process.stdout)
}
}
private async stopDocker(docker: Docker) {
const dockerSpinner = ora({
text: 'Stopping all containers...',
spinner: 'point',
color: 'red',
isSilent: this.verbosity !== VerbosityLevel.Quiet,
}).start()
await docker.stopAll(false)
dockerSpinner.stop()
}
private async buildDockerOptions(): Promise<RunOptions> {
return {
repo: this.repo,
fresh: this.fresh,
}
}
}

53
src/command/stop.ts Normal file
View File

@ -0,0 +1,53 @@
import { LeafCommand, Option } from 'furious-commander'
import { RootCommand } from './root-command'
import { DEFAULT_ENV_PREFIX, DEFAULT_IMAGE_PREFIX, Docker } from '../utils/docker'
import ora from 'ora'
import { VerbosityLevel } from './root-command/logging'
export class Stop extends RootCommand implements LeafCommand {
public readonly name = 'stop'
public readonly description = 'Stops the Bee Factory cluster'
@Option({
key: 'env-prefix',
type: 'string',
description: "Docker container's names prefix",
envKey: 'FACTORY_DOCKER_PREFIX',
default: DEFAULT_ENV_PREFIX,
})
public envPrefix!: string
@Option({
key: 'image-prefix',
type: 'string',
description: 'Docker image name prefix',
envKey: 'FACTORY_DOCKER_PREFIX',
default: DEFAULT_IMAGE_PREFIX,
})
public imagePrefix!: string
@Option({
key: 'rm',
type: 'boolean',
description: 'Remove the containers',
})
public deleteContainers!: boolean
public async run(): Promise<void> {
await super.init()
const docker = new Docker(this.console, this.envPrefix, this.imagePrefix)
const dockerSpinner = ora({
text: 'Stopping all containers...',
spinner: 'point',
color: 'yellow',
isSilent: this.verbosity !== VerbosityLevel.Quiet,
}).start()
await docker.stopAll(true, this.deleteContainers)
dockerSpinner.succeed('Containers stopped')
}
}

46
src/config.ts Normal file
View File

@ -0,0 +1,46 @@
import { IOption } from 'furious-commander'
import PackageJson from '../package.json'
import { Start } from './command/start'
import { Stop } from './command/stop'
import { Logs } from './command/logs'
export const quiet: IOption<boolean> = {
key: 'quiet',
alias: 'q',
description: 'Does not print anything',
type: 'boolean',
default: false,
conflicts: 'verbose',
}
export const verbose: IOption<boolean> = {
key: 'verbose',
alias: 'v',
description: 'Display logs',
type: 'boolean',
default: false,
conflicts: 'quiet',
}
export const help: IOption<boolean> = {
key: 'help',
alias: 'h',
description: 'Print context specific help and exit',
type: 'boolean',
default: false,
}
export const version: IOption<boolean> = {
key: 'version',
alias: 'V',
description: 'Print version and exit',
type: 'boolean',
default: false,
handler: () => {
process.stdout.write(PackageJson.version + '\n')
},
}
export const optionParameters: IOption<unknown>[] = [quiet, verbose, help, version]
export const rootCommandClasses = [Start, Stop, Logs]

11
src/index.ts Normal file
View File

@ -0,0 +1,11 @@
import { cli } from 'furious-commander'
import { application } from './application'
import { optionParameters, rootCommandClasses } from './config'
import { printer } from './printer'
cli({
rootCommandClasses,
optionParameters,
printer,
application,
})

12
src/printer.ts Normal file
View File

@ -0,0 +1,12 @@
import chalk from 'chalk'
import { Printer } from 'furious-commander/dist/printer'
import { Printer as SwarmPrinter } from './command/root-command/printer'
export const printer: Printer = {
print: SwarmPrinter.log,
printError: SwarmPrinter.error,
printHeading: (text: string) => SwarmPrinter.log(chalk.bold('█ ' + text)),
formatDim: (text: string) => chalk.dim(text),
formatImportant: (text: string) => chalk.bold(text),
getGenericErrorMessage: () => 'Failed to run command!',
}

382
src/utils/docker.ts Normal file
View File

@ -0,0 +1,382 @@
import Dockerode, { Container, ContainerCreateOptions } from 'dockerode'
import { Logging } from '../command/root-command/logging'
import { ContainerImageConflictError } from './error'
export const DEFAULT_ENV_PREFIX = 'bee-factory'
export const DEFAULT_IMAGE_PREFIX = 'swarm-test'
const BLOCKCHAIN_IMAGE_NAME_SUFFIX = '-blockchain'
const QUEEN_IMAGE_NAME_SUFFIX = '-queen'
const WORKER_IMAGE_NAME_SUFFIX = '-worker'
const NETWORK_NAME_SUFFIX = '-network'
export const WORKER_COUNT = 4
const SWAP_FACTORY_ADDRESS = '0x5b1869D9A4C187F2EAa108f3062412ecf0526b24'
const POSTAGE_STAMP_ADDRESS = '0xCfEB869F69431e42cdB54A4F4f105C19C080A601'
const PRICE_ORACLE_ADDRESS = '0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B'
export interface RunOptions {
repo: string
fresh: boolean
}
export enum ContainerType {
QUEEN = 'queen',
BLOCKCHAIN = 'blockchain',
WORKER_1 = 'worker1',
WORKER_2 = 'worker2',
WORKER_3 = 'worker3',
WORKER_4 = 'worker4',
}
export type Status = 'running' | 'exists' | 'not-found'
type FindResult = { container?: Container; image?: string }
export interface AllStatus {
blockchain: Status
queen: Status
worker1: Status
worker2: Status
worker3: Status
worker4: Status
}
export interface DockerError extends Error {
reason: string
statusCode: number
}
export class Docker {
private docker: Dockerode
private console: Logging
private runningContainers: Container[]
private envPrefix: string
private imagePrefix: string
private get networkName() {
return `${this.envPrefix}${NETWORK_NAME_SUFFIX}`
}
private get blockchainName() {
return `${this.envPrefix}${BLOCKCHAIN_IMAGE_NAME_SUFFIX}`
}
private get queenName() {
return `${this.envPrefix}${QUEEN_IMAGE_NAME_SUFFIX}`
}
private workerName(index: number) {
return `${this.envPrefix}${WORKER_IMAGE_NAME_SUFFIX}-${index}`
}
constructor(console: Logging, envPrefix: string, imagePrefix: string) {
this.docker = new Dockerode()
this.console = console
this.runningContainers = []
this.envPrefix = envPrefix
this.imagePrefix = imagePrefix
}
public async createNetwork(): Promise<void> {
const networks = await this.docker.listNetworks({ filters: { name: [this.networkName] } })
if (networks.length === 0) {
await this.docker.createNetwork({ Name: this.networkName })
}
}
public async startBlockchainNode(blockchainVersion: string, beeVersion: string, options: RunOptions): Promise<void> {
if (options.fresh) await this.removeContainer(this.blockchainName)
const container = await this.findOrCreateContainer(this.blockchainName, {
Image: `${options.repo}/${this.imagePrefix}${BLOCKCHAIN_IMAGE_NAME_SUFFIX}:${blockchainVersion}-for-${beeVersion}`,
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 = await container.inspect()
// If it is already running (because of whatever reason) we are not spawning new node
if (!state.State.Running) {
await container.start()
} else {
this.console.info('The blockchain container was already running, so not starting it again.')
}
}
public async startQueenNode(beeVersion: string, options: RunOptions): Promise<void> {
if (options.fresh) await this.removeContainer(this.queenName)
const container = await this.findOrCreateContainer(this.queenName, {
Image: `${options.repo}/${this.imagePrefix}${QUEEN_IMAGE_NAME_SUFFIX}:${beeVersion}`,
name: this.queenName,
ExposedPorts: {
'1633/tcp': {},
'1634/tcp': {},
'1635/tcp': {},
},
Tty: true,
Cmd: ['start'],
Env: this.createBeeEnvParameters(),
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 = await 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) {
await container.start()
} else {
this.console.info('The Queen node container was already running, so not starting it again.')
}
}
public async startWorkerNode(
beeVersion: string,
workerNumber: number,
queenAddress: string,
options: RunOptions,
): Promise<void> {
if (options.fresh) await this.removeContainer(this.workerName(workerNumber))
const container = await this.findOrCreateContainer(this.workerName(workerNumber), {
Image: `${options.repo}/${this.imagePrefix}${WORKER_IMAGE_NAME_SUFFIX}-${workerNumber}:${beeVersion}`,
name: this.workerName(workerNumber),
ExposedPorts: {
'1633/tcp': {},
'1634/tcp': {},
'1635/tcp': {},
},
Cmd: ['start'],
Env: this.createBeeEnvParameters(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 = await container.inspect()
// If it is already running (because of whatever reason) we are not spawning new node
if (!state.State.Running) {
await container.start()
} else {
this.console.info('The Queen node container was already running, so not starting it again.')
}
}
public async logs(
target: ContainerType,
outputStream: NodeJS.WriteStream,
follow = false,
tail?: number,
): Promise<void> {
const { container } = await this.findContainer(this.getContainerName(target))
if (!container) {
throw new Error('Queen container does not exists, even though it should have had!')
}
const logs = await container.logs({ stdout: true, stderr: true, follow, tail })
if (!follow) {
outputStream.write(logs as unknown as Buffer)
} else {
logs.pipe(outputStream)
}
}
public async stopAll(allWithPrefix = false, deleteContainers = false): Promise<void> {
const containerProcessor = async (container: Container) => {
try {
await container.stop()
} catch (e) {
// We ignore 304 that represents that the container is already stopped
if ((e as DockerError).statusCode !== 304) {
throw e
}
}
if (deleteContainers) {
await container.remove()
}
}
this.console.info('Stopping all containers')
await Promise.all(this.runningContainers.map(containerProcessor))
if (allWithPrefix) {
const containers = await this.docker.listContainers({ all: true })
await Promise.all(
containers
.filter(container => container.Names.filter(n => n.startsWith('/' + this.envPrefix)).length >= 1)
.map(container => this.docker.getContainer(container.Id))
.map(containerProcessor),
)
}
}
public async getAllStatus(): Promise<AllStatus> {
return {
queen: await this.getStatusForContainer(ContainerType.QUEEN),
blockchain: await this.getStatusForContainer(ContainerType.BLOCKCHAIN),
worker1: await this.getStatusForContainer(ContainerType.WORKER_1),
worker2: await this.getStatusForContainer(ContainerType.WORKER_2),
worker3: await this.getStatusForContainer(ContainerType.WORKER_3),
worker4: await this.getStatusForContainer(ContainerType.WORKER_4),
}
}
private async removeContainer(name: string): Promise<void> {
this.console.info(`Removing container with name "${name}"`)
const { container } = await this.findContainer(name)
// Container does not exist so nothing to delete
if (!container) {
return
}
await container.remove({ v: true, force: true })
}
private async findOrCreateContainer(name: string, createOptions: ContainerCreateOptions): Promise<Container> {
const { container, image: foundImage } = await this.findContainer(name)
if (container) {
this.console.info(`Container with name "${name}" found. Using it.`)
if (foundImage !== createOptions.Image) {
throw new 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 await this.docker.createContainer(createOptions)
} catch (e) {
// 404 is Image Not Found ==> pull the image
if ((e as DockerError).statusCode !== 404) {
throw e
}
this.console.info(`Image ${createOptions.Image} not found. Pulling it.`)
const pullStream = await this.docker.pull(createOptions.Image!)
await new Promise(res => this.docker.modem.followProgress(pullStream, res))
return await this.docker.createContainer(createOptions)
}
}
private async findContainer(name: string): Promise<FindResult> {
const containers = await 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 }
}
public async getStatusForContainer(name: ContainerType): Promise<Status> {
const foundContainer = await this.findContainer(this.getContainerName(name))
if (!foundContainer.container) {
return 'not-found'
}
const inspectStatus = await foundContainer.container.inspect()
if (inspectStatus.State.Running) {
return 'running'
}
return 'exists'
}
private getContainerName(name: ContainerType) {
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!')
}
}
private createBeeEnvParameters(bootnode?: string): string[] {
const options: Record<string, string> = {
'warmup-time': '0',
'debug-api-enable': 'true',
verbosity: '4',
'swap-enable': 'true',
'swap-endpoint': `http://${this.blockchainName}:9545`,
'swap-factory-address': SWAP_FACTORY_ADDRESS,
password: 'password',
'postage-stamp-address': POSTAGE_STAMP_ADDRESS,
'price-oracle-address': PRICE_ORACLE_ADDRESS,
'network-id': '4020',
'full-node': 'true',
'welcome-message': 'You have found the queen of the beehive...',
'cors-allowed-origins': '*',
}
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<string[]>((previous, current) => {
const keyName = `BEE_${current[0].toUpperCase().replace(/-/g, '_')}`
previous.push(`${keyName}=${current[1]}`)
return previous
}, [])
}
}

15
src/utils/error.ts Normal file
View File

@ -0,0 +1,15 @@
/**
* Thrown when the error is not related to Bee/network
*/
export class TimeoutError extends Error {}
export class ContainerImageConflictError extends Error {
existingContainersImage: string
newContainersImage: string
constructor(message: string, existingContainersImage: string, newContainersImage: string) {
super(message)
this.existingContainersImage = existingContainersImage
this.newContainersImage = newContainersImage
}
}

8
src/utils/index.ts Normal file
View File

@ -0,0 +1,8 @@
/**
* Sleep for N miliseconds
*
* @param ms Number of miliseconds to sleep
*/
export async function sleep(ms: number): Promise<void> {
return new Promise<void>(resolve => setTimeout(() => resolve(), ms))
}

109
src/utils/wait.ts Normal file
View File

@ -0,0 +1,109 @@
import fetch, { FetchError } from 'node-fetch'
import { sleep } from './index'
import { TimeoutError } from './error'
import { BeeDebug } from '@ethersphere/bee-js'
import { WORKER_COUNT } from './docker'
const AWAIT_SLEEP = 3_000
const BLOCKCHAIN_BODY_REQUEST = JSON.stringify({ jsonrpc: '2.0', method: 'eth_chainId', id: 1 })
const EXPECTED_CHAIN_ID = '0xfb4'
const ALLOWED_ERRORS = ['ECONNREFUSED', 'ECONNRESET']
function isAllowedError(e: FetchError): boolean {
if (e.code && ALLOWED_ERRORS.includes(e.code)) {
return true
}
// Errors from Bee-js does not have the `FetchError` structure (eq. `code` property)
// so we assert message itself.
if (e.message.includes('socket hang up')) {
return true
}
return ALLOWED_ERRORS.some(substring => e.message.includes(substring))
}
export async function waitForBlockchain(waitingIterations = 30): Promise<void> {
for (let i = 0; i < waitingIterations; i++) {
try {
const request = await fetch('http://127.0.0.1:9545', {
method: 'POST',
body: BLOCKCHAIN_BODY_REQUEST,
headers: { 'Content-Type': 'application/json' },
})
const response = (await request.json()) as { result: string }
if (response.result === EXPECTED_CHAIN_ID) {
return
}
} catch (e) {
if (!isAllowedError(e as FetchError)) {
throw e
}
}
await sleep(AWAIT_SLEEP)
}
throw new TimeoutError('Waiting for blockchain container timed-out')
}
export async function waitForQueen(verifyQueenIsUp: () => Promise<boolean>, waitingIterations = 120): Promise<string> {
const beeDebug = new BeeDebug('http://127.0.0.1:1635')
for (let i = 0; i < waitingIterations; i++) {
try {
if (!(await verifyQueenIsUp())) {
throw new Error('Queen node is not running!')
}
const addresses = await beeDebug.getNodeAddresses()
if (addresses.underlay.length > 0) {
const addr = addresses.underlay.find(addr => !addr.includes('127.0.0.1'))
if (addr) {
return addr
}
}
} catch (e) {
if (!isAllowedError(e as FetchError)) {
throw e
}
}
await sleep(AWAIT_SLEEP)
}
throw new TimeoutError('Waiting for queen container timed-out')
}
export async function waitForWorkers(
verifyWorkersAreUp: () => Promise<boolean>,
waitingIterations = 120,
): Promise<void> {
const beeDebug = new BeeDebug('http://127.0.0.1:1635')
for (let i = 0; i < waitingIterations; i++) {
try {
if (!(await verifyWorkersAreUp())) {
throw new Error('Some of the workers node is not running!')
}
const peers = await beeDebug.getPeers()
if (peers.length >= WORKER_COUNT) {
return
}
} catch (e) {
if (!isAllowedError(e as FetchError)) {
throw e
}
}
await sleep(AWAIT_SLEEP)
}
throw new TimeoutError('Waiting for worker nodes timed-out')
}

View File

@ -0,0 +1,127 @@
/* eslint-disable no-console */
import Dockerode from 'dockerode'
import crypto from 'crypto'
import { run } from '../utils/run'
import { ENV_ENV_PREFIX_KEY } from '../../src/command/start'
import { Bee, BeeDebug, Reference } from '@ethersphere/bee-js'
import { DockerError } from '../../src/utils/docker'
import { findContainer, waitForUsablePostageStamp } from '../utils/docker'
const BLOCKCHAIN_VERSION = '1.2.0'
const BEE_VERSION = '1.5.1-d0a77598-stateful'
let testFailed = false
function wrapper(fn: () => Promise<unknown>): () => Promise<unknown> {
return async () => {
try {
return await fn()
} catch (e) {
testFailed = true
throw e
}
}
}
describe('start command', () => {
let docker: Dockerode
let bee: Bee, beeDebug: BeeDebug
const envPrefix = `bee-factory-test-${crypto.randomBytes(4).toString('hex')}`
beforeAll(() => {
docker = new Dockerode()
bee = new Bee('http://127.0.0.1:1633')
beeDebug = new BeeDebug('http://127.0.0.1:1635')
// This will force Bee Factory to create
process.env[ENV_ENV_PREFIX_KEY] = envPrefix
})
afterEach(async () => {
if (testFailed) {
await run(['logs', 'queen'])
}
await run(['stop'])
})
afterAll(async () => {
await run(['stop', '--rm']) // Cleanup the testing containers
})
it(
'should start cluster',
wrapper(async () => {
// As spinning the cluster with --detach the command will exit once the cluster is up and running
await run(['start', '--detach', BLOCKCHAIN_VERSION, BEE_VERSION])
await expect(findContainer(docker, 'queen')).resolves.toBeDefined()
await expect(findContainer(docker, 'blockchain')).resolves.toBeDefined()
await expect(findContainer(docker, 'worker-1')).resolves.toBeDefined()
await expect(findContainer(docker, 'worker-2')).resolves.toBeDefined()
await expect(findContainer(docker, 'worker-3')).resolves.toBeDefined()
await expect(findContainer(docker, 'worker-4')).resolves.toBeDefined()
await expect(beeDebug.getHealth()).resolves.toHaveProperty('status')
}),
)
describe('should create docker network', () => {
beforeAll(async () => {
await run(['stop', '--rm']) // Cleanup the testing containers
try {
// Make sure the network does not exists
await (await docker.getNetwork(`${envPrefix}-network`)).remove()
} catch (e) {
if ((e as DockerError).statusCode !== 404) {
throw e
}
}
})
it(
'',
wrapper(async () => {
await run(['start', '--detach', BLOCKCHAIN_VERSION, BEE_VERSION])
expect(docker.getNetwork(`${envPrefix}-network`)).toBeDefined()
}),
)
})
describe('should remove containers with --fresh option', () => {
let reference: Reference, data: string
beforeAll(async () => {
console.log('(before) Starting up Bee Factory')
await run(['start', '--detach', BLOCKCHAIN_VERSION, BEE_VERSION])
console.log('(before) Creating postage stamp ')
const postage = await beeDebug.createPostageBatch('10', 18)
console.log('(before) Waiting for the postage stamp to be usable')
await waitForUsablePostageStamp(beeDebug, postage)
data = `hello from ${Date.now()}`
reference = (await bee.uploadData(postage, data)).reference
// Lets just verify that it the current container has the data
expect((await bee.downloadData(reference)).text()).toEqual(data)
console.log('(before) Stopping the Bee Factory')
await run(['stop'])
})
it(
'',
wrapper(async () => {
console.log('(test) Starting the Bee Factory')
await run(['start', '--fresh', '--detach', BLOCKCHAIN_VERSION, BEE_VERSION])
console.log('(test) Trying to fetch the data')
await expect(bee.downloadData(reference)).rejects.toHaveProperty('status', 404)
}),
)
})
})

View File

@ -0,0 +1,76 @@
/* eslint-disable no-console */
import Dockerode from 'dockerode'
import crypto from 'crypto'
import { run } from '../utils/run'
import { ENV_ENV_PREFIX_KEY } from '../../src/command/start'
import { findContainer } from '../utils/docker'
const BLOCKCHAIN_VERSION = '1.2.0'
const BEE_VERSION = '1.5.1-d0a77598-stateful'
describe('stop command', () => {
let docker: Dockerode
const envPrefix = `bee-factory-test-${crypto.randomBytes(4).toString('hex')}`
beforeAll(() => {
docker = new Dockerode()
// This will force Bee Factory to create
process.env[ENV_ENV_PREFIX_KEY] = envPrefix
})
afterAll(async () => {
await run(['stop', '--rm']) // Cleanup the testing containers
})
describe('should stop cluster', () => {
beforeAll(async () => {
// As spinning the cluster with --detach the command will exit once the cluster is up and running
await run(['start', '--detach', BLOCKCHAIN_VERSION, BEE_VERSION])
})
it('', async () => {
await expect(findContainer(docker, 'queen')).resolves.toHaveProperty('State.Status', 'running')
await expect(findContainer(docker, 'blockchain')).resolves.toHaveProperty('State.Status', 'running')
await expect(findContainer(docker, 'worker-1')).resolves.toHaveProperty('State.Status', 'running')
await expect(findContainer(docker, 'worker-2')).resolves.toHaveProperty('State.Status', 'running')
await expect(findContainer(docker, 'worker-3')).resolves.toHaveProperty('State.Status', 'running')
await expect(findContainer(docker, 'worker-4')).resolves.toHaveProperty('State.Status', 'running')
await run(['stop'])
await expect(findContainer(docker, 'queen')).resolves.toHaveProperty('State.Status', 'exited')
await expect(findContainer(docker, 'blockchain')).resolves.toHaveProperty('State.Status', 'exited')
await expect(findContainer(docker, 'worker-1')).resolves.toHaveProperty('State.Status', 'exited')
await expect(findContainer(docker, 'worker-2')).resolves.toHaveProperty('State.Status', 'exited')
await expect(findContainer(docker, 'worker-3')).resolves.toHaveProperty('State.Status', 'exited')
await expect(findContainer(docker, 'worker-4')).resolves.toHaveProperty('State.Status', 'exited')
})
})
describe('should stop cluster and remove containers', () => {
beforeAll(async () => {
// As spinning the cluster with --detach the command will exit once the cluster is up and running
await run(['start', '--detach', BLOCKCHAIN_VERSION, BEE_VERSION])
})
it('', async () => {
await expect(findContainer(docker, 'queen')).resolves.toHaveProperty('State.Status', 'running')
await expect(findContainer(docker, 'blockchain')).resolves.toHaveProperty('State.Status', 'running')
await expect(findContainer(docker, 'worker-1')).resolves.toHaveProperty('State.Status', 'running')
await expect(findContainer(docker, 'worker-2')).resolves.toHaveProperty('State.Status', 'running')
await expect(findContainer(docker, 'worker-3')).resolves.toHaveProperty('State.Status', 'running')
await expect(findContainer(docker, 'worker-4')).resolves.toHaveProperty('State.Status', 'running')
await run(['stop', '--rm'])
await expect(findContainer(docker, 'queen')).rejects.toHaveProperty('statusCode', 404)
await expect(findContainer(docker, 'blockchain')).rejects.toHaveProperty('statusCode', 404)
await expect(findContainer(docker, 'worker-1')).rejects.toHaveProperty('statusCode', 404)
await expect(findContainer(docker, 'worker-2')).rejects.toHaveProperty('statusCode', 404)
await expect(findContainer(docker, 'worker-3')).rejects.toHaveProperty('statusCode', 404)
await expect(findContainer(docker, 'worker-4')).rejects.toHaveProperty('statusCode', 404)
})
})
})

26
test/utils/docker.ts Normal file
View File

@ -0,0 +1,26 @@
import Dockerode from 'dockerode'
import { ENV_ENV_PREFIX_KEY } from '../../src/command/start'
import { BatchId, BeeDebug } from '@ethersphere/bee-js'
export async function findContainer(docker: Dockerode, name: string): Promise<Dockerode.ContainerInspectInfo> {
return docker.getContainer(`${process.env[ENV_ENV_PREFIX_KEY]}-${name}`).inspect()
}
export async function sleep(ms: number): Promise<void> {
return new Promise<void>(resolve => setTimeout(() => resolve(), ms))
}
export async function waitForUsablePostageStamp(beeDebug: BeeDebug, id: BatchId, timeout = 120_000): Promise<void> {
const TIME_STEP = 1500
for (let time = 0; time < timeout; time += TIME_STEP) {
const stamp = await beeDebug.getPostageBatch(id)
if (stamp.usable) {
return
}
await sleep(TIME_STEP)
}
throw new Error('Timeout on waiting for postage stamp to become usable')
}

12
test/utils/run.ts Normal file
View File

@ -0,0 +1,12 @@
import { cli } from 'furious-commander'
import { optionParameters, rootCommandClasses } from '../../src/config'
export async function run(argv: string[]): ReturnType<typeof cli> {
const commandBuilder = await cli({
rootCommandClasses,
optionParameters,
testArguments: argv,
})
return commandBuilder
}

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"include": ["src"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"alwaysStrict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"typeRoots": ["./src/types", "node_modules/@types"],
"target": "es2015",
"rootDirs": ["src"],
"module": "commonjs",
"outDir": "dist",
"resolveJsonModule": true,
"useDefineForClassFields": true
},
}

11
tsconfig.test.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"include": [
"src",
"test",
"jest.config.ts"
],
"compilerOptions": {
"noEmit": true
}
}