mirror of
https://github.com/logos-storage/codex-factory.git
synced 2026-01-02 13:03:07 +00:00
feat: replace cli framework furious-commander with oclif
This commit is contained in:
parent
d566c52045
commit
221587b0e4
@ -1 +0,0 @@
|
||||
dist/**
|
||||
109
.eslintrc.js
109
.eslintrc.js
@ -1,109 +0,0 @@
|
||||
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
1
.gitattributes
vendored
@ -1 +0,0 @@
|
||||
* text=auto
|
||||
13
.github/dependabot.yml
vendored
13
.github/dependabot.yml
vendored
@ -1,13 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
# Always increase the version in package.json as well (for patch versions by default only package-lock.json i updated)
|
||||
versioning-strategy: increase
|
||||
54
.github/workflows/check.yaml
vendored
54
.github/workflows/check.yaml
vendored
@ -1,54 +0,0 @@
|
||||
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@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
## Try getting the node modules from cache, if failed npm ci
|
||||
- uses: actions/cache@v3
|
||||
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@v5
|
||||
|
||||
- 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
|
||||
60
.github/workflows/test.yaml
vendored
60
.github/workflows/test.yaml
vendored
@ -1,4 +1,4 @@
|
||||
name: Tests
|
||||
name: Check
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -7,42 +7,32 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
jobs:
|
||||
node-tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
|
||||
tests:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x, 16.x, 18.x]
|
||||
|
||||
os: ['ubuntu-latest', 'windows-latest']
|
||||
node_version: [lts/-1, lts/*, latest]
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
## Try getting the node modules from cache, if failed npm ci
|
||||
- uses: actions/cache@v3
|
||||
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 tests for older versions
|
||||
if: matrix.node-version != '18.x'
|
||||
run: npm test -- --bail true
|
||||
- name: Run tests for Node 18
|
||||
if: matrix.node-version == '18.x'
|
||||
env:
|
||||
NODE_OPTIONS: "--no-experimental-fetch"
|
||||
run: npm test -- --bail true
|
||||
node-version: ${{ matrix.node_version }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
36
.gitignore
vendored
36
.gitignore
vendored
@ -1,30 +1,14 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
*-debug.log
|
||||
*-error.log
|
||||
**/.DS_Store
|
||||
/.idea
|
||||
/dist
|
||||
/tmp
|
||||
/node_modules
|
||||
oclif.manifest.json
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
build
|
||||
dist
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
|
||||
# Generated files
|
||||
docs
|
||||
|
||||
15
.mocharc.json
Normal file
15
.mocharc.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
],
|
||||
"watch-extensions": [
|
||||
"ts"
|
||||
],
|
||||
"recursive": true,
|
||||
"reporter": "spec",
|
||||
"timeout": 60000,
|
||||
"node-option": [
|
||||
"loader=ts-node/esm",
|
||||
"experimental-specifier-resolution=node"
|
||||
]
|
||||
}
|
||||
13
.prettierrc
13
.prettierrc
@ -1,13 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf",
|
||||
"arrowParens": "avoid",
|
||||
"proseWrap": "always"
|
||||
}
|
||||
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@ -0,0 +1 @@
|
||||
"@oclif/prettier-config"
|
||||
@ -1 +0,0 @@
|
||||
* @AuHau
|
||||
17
README.md
17
README.md
@ -1,15 +1,14 @@
|
||||
# Codex Factory
|
||||
|
||||
[](https://oclif.io)
|
||||
[](https://npmjs.org/package/@codex-storage/codex-factory)
|
||||
[](https://npmjs.org/package/@codex-storage/codex-factory)
|
||||
[](https://github.com/codex-storage/codex-factory/actions/workflows/test.yaml)
|
||||
[](https://github.com/RichardLitt/standard-readme)
|
||||
[](https://github.com/feross/standard)
|
||||

|
||||

|
||||

|
||||
|
||||
> CLI tool to spin up Docker cluster of Codex 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)
|
||||
@ -58,9 +57,9 @@ For more details see the `--help` page of the CLI and its commands.
|
||||
|
||||
You can omit the Codex version argument when running `codex-factory start` command if you specify it in one of the expected places:
|
||||
|
||||
- `CODEX_FACTORY_VERSION` env. variable
|
||||
- `package.json` placed in current working directory (cwd) under the `engines.codex` property.
|
||||
- `.codexfactory.json` placed in current working directory (cwd) with property `version`.
|
||||
- `CODEX_FACTORY_VERSION` env. variable
|
||||
- `package.json` placed in current working directory (cwd) under the `engines.codex` property.
|
||||
- `.codexfactory.json` placed in current working directory (cwd) with property `version`.
|
||||
|
||||
#### Build versions
|
||||
|
||||
@ -94,6 +93,6 @@ You can run the CLI while developing using `npm start -- <command> ...`.
|
||||
|
||||
## License
|
||||
|
||||
[BSD-3-Clause](./LICENSE)
|
||||
[BSD-3-Clause](LICENSE)
|
||||
|
||||
Originally written for Swarm's Bee client: https://github.com/ethersphere/bee-factory
|
||||
|
||||
3
bin/dev.cmd
Normal file
3
bin/dev.cmd
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %*
|
||||
5
bin/dev.js
Executable file
5
bin/dev.js
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env -S node --loader ts-node/esm --no-warnings
|
||||
|
||||
import {execute} from '@oclif/core'
|
||||
|
||||
await execute({development: true, dir: import.meta.url})
|
||||
3
bin/run.cmd
Normal file
3
bin/run.cmd
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\run" %*
|
||||
5
bin/run.js
Executable file
5
bin/run.js
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {execute} from '@oclif/core'
|
||||
|
||||
await execute({dir: import.meta.url})
|
||||
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'body-max-line-length': [0, 'always', Infinity], // disable commit body length restriction
|
||||
},
|
||||
}
|
||||
9
eslint.config.mjs
Normal file
9
eslint.config.mjs
Normal file
@ -0,0 +1,9 @@
|
||||
import {includeIgnoreFile} from '@eslint/compat'
|
||||
import oclif from 'eslint-config-oclif'
|
||||
import prettier from 'eslint-config-prettier'
|
||||
import path from 'node:path'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
|
||||
const gitignorePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '.gitignore')
|
||||
|
||||
export default [includeIgnoreFile(gitignorePath), ...oclif, prettier]
|
||||
@ -1,35 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
preset: 'ts-jest',
|
||||
runner: '@ethersphere/jest-serial-runner',
|
||||
testRegex: 'test/integration/.*\\.spec\\.ts',
|
||||
testEnvironment: 'node',
|
||||
|
||||
// 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'],
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
rootDir: 'test',
|
||||
|
||||
// Increase timeout since we are spinning Codex 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/'],
|
||||
}
|
||||
}
|
||||
18544
package-lock.json
generated
18544
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
134
package.json
134
package.json
@ -1,8 +1,58 @@
|
||||
{
|
||||
"name": "@codex-storage/codex-factory",
|
||||
"version": "0.1.0",
|
||||
"description": "Orchestration CLI for spinning up local development Codex cluster with Docker",
|
||||
"version": "0.1.0",
|
||||
"author": "Codex Authors",
|
||||
"bin": {
|
||||
"codex-factory": "bin/run.js"
|
||||
},
|
||||
"bugs": "https://github.com/codex-storage/codex-factory/issues",
|
||||
"dependencies": {
|
||||
"@codex-storage/sdk-js": "^0.1.2",
|
||||
"@oclif/core": "^4",
|
||||
"@oclif/plugin-help": "^6",
|
||||
"chalk": "^4.1.2",
|
||||
"dockerode": "^4",
|
||||
"node-fetch": "^3",
|
||||
"ora": "^8",
|
||||
"semver": "^7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1",
|
||||
"@oclif/prettier-config": "^0.2.1",
|
||||
"@oclif/test": "^4",
|
||||
"@types/chai": "^4",
|
||||
"@types/chai-as-promised": "^8.0.2",
|
||||
"@types/dockerode": "^3.3.38",
|
||||
"@types/mocha": "^10",
|
||||
"@types/node": "^18",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/semver": "^7.7.0",
|
||||
"chai": "^4",
|
||||
"chai-as-promised": "^8.0.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-oclif": "^6",
|
||||
"eslint-config-prettier": "^10",
|
||||
"mocha": "^10",
|
||||
"oclif": "^4",
|
||||
"shx": "^0.3.3",
|
||||
"ts-node": "^10",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=6.0.0",
|
||||
"codex": "0.2.1",
|
||||
"supportedCodex": ">0.2.0"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"./dist",
|
||||
"./oclif.manifest.json"
|
||||
],
|
||||
"homepage": "https://github.com/codex-storage/codex-factory",
|
||||
"keywords": [
|
||||
"oclif",
|
||||
"codex",
|
||||
"codex-storage",
|
||||
"decentralised",
|
||||
@ -11,70 +61,28 @@
|
||||
"p2p",
|
||||
"docker"
|
||||
],
|
||||
"homepage": "https://github.com/codex-storage/codex-factory",
|
||||
"bugs": {
|
||||
"url": "https://github.com/codex-storage/codex-factory/issues/"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/codex-storage/codex-factory.git"
|
||||
"main": "./dist/index.js",
|
||||
"type": "module",
|
||||
"oclif": {
|
||||
"bin": "codex-factory",
|
||||
"dirname": "codex-factory",
|
||||
"commands": "./dist/commands",
|
||||
"plugins": [
|
||||
"@oclif/plugin-help"
|
||||
],
|
||||
"topicSeparator": " ",
|
||||
"topics": {}
|
||||
},
|
||||
"bin": {
|
||||
"codex-factory": "./dist/src/index.js"
|
||||
},
|
||||
"main": "dist/src/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": "https://github.com/codex-storage/codex-factory/",
|
||||
"scripts": {
|
||||
"prepublishOnly": "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 ."
|
||||
"start": "bin/dev.js",
|
||||
"build": "shx rm -rf dist && tsc -b",
|
||||
"lint": "eslint",
|
||||
"postpack": "shx rm -f oclif.manifest.json",
|
||||
"prepack": "oclif manifest && oclif readme",
|
||||
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
|
||||
"version": "oclif readme && git add README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codex-storage/sdk-js": "^0.1.2",
|
||||
"chalk": "^4.1.2",
|
||||
"dockerode": "^3.3.4",
|
||||
"furious-commander": "^1.7.1",
|
||||
"node-fetch": "3.0.0-beta.9",
|
||||
"ora": "^5.3.0",
|
||||
"semver": "^7.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.1.2",
|
||||
"@commitlint/config-conventional": "^17.1.0",
|
||||
"@ethersphere/jest-serial-runner": "^1.0.0",
|
||||
"@fluffy-spoon/substitute": "^1.208.0",
|
||||
"@jest/types": "^29.2.1",
|
||||
"@types/dockerode": "^3.3.11",
|
||||
"@types/jest": "^29.2.2",
|
||||
"@types/node": "^18.11.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"depcheck": "^1.4.3",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jest": "^27.1.3",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"jest": "^29.3.0",
|
||||
"jest-runner": "^29.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=6.0.0",
|
||||
"codex": "4ace774",
|
||||
"supportedCodex": ">=0.2.0"
|
||||
}
|
||||
"types": "dist/index.d.ts"
|
||||
}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
import { Application } from 'furious-commander/dist/application'
|
||||
import PackageJson from '../package.json'
|
||||
|
||||
export const application: Application = {
|
||||
name: 'Codex Factory',
|
||||
command: 'codex-factory',
|
||||
description: 'Orchestration CLI for spinning up local development Codex cluster with Docker',
|
||||
version: PackageJson.version,
|
||||
autocompletion: 'fromOption',
|
||||
}
|
||||
48
src/base.ts
Normal file
48
src/base.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Command, Flags, Interfaces } from "@oclif/core";
|
||||
|
||||
import { Logging, VerbosityLevel } from "./utils/logging.js";
|
||||
|
||||
export type Flags<T extends typeof Command> = Interfaces.InferredFlags<T["flags"] & typeof BaseCommand["baseFlags"]>
|
||||
export type Args<T extends typeof Command> = Interfaces.InferredArgs<T["args"]>
|
||||
|
||||
export abstract class BaseCommand<T extends typeof Command> extends Command {
|
||||
static baseFlags = {
|
||||
quiet: Flags.boolean({
|
||||
default: false,
|
||||
description: "Does not print anything.",
|
||||
}),
|
||||
verbose: Flags.boolean({
|
||||
default: false,
|
||||
description: "Display logs.",
|
||||
}),
|
||||
};
|
||||
protected args!: Args<T>;
|
||||
public console!: Logging;
|
||||
protected flags!: Flags<T>;
|
||||
public verbosity!: VerbosityLevel;
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await super.init();
|
||||
const { args, flags } = await this.parse({
|
||||
args: this.ctor.args,
|
||||
baseFlags: (super.ctor as typeof BaseCommand).baseFlags,
|
||||
enableJsonFlag: this.ctor.enableJsonFlag,
|
||||
flags: this.ctor.flags,
|
||||
strict: this.ctor.strict,
|
||||
});
|
||||
this.flags = flags as Flags<T>;
|
||||
this.args = args as Args<T>;
|
||||
|
||||
this.verbosity = VerbosityLevel.Normal;
|
||||
|
||||
if (flags.quiet) {
|
||||
this.verbosity = VerbosityLevel.Quiet;
|
||||
}
|
||||
|
||||
if (flags.verbose) {
|
||||
this.verbosity = VerbosityLevel.Verbose;
|
||||
}
|
||||
|
||||
this.console = new Logging(this.verbosity);
|
||||
}
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
import { Argument, GroupCommand, LeafCommand, Option } from 'furious-commander'
|
||||
import { RootCommand } from './root-command'
|
||||
import { ContainerType, DEFAULT_ENV_PREFIX, DEFAULT_IMAGE_PREFIX, Docker } from '../utils/docker'
|
||||
|
||||
class ClientRequestCommand extends RootCommand implements LeafCommand {
|
||||
public readonly name = 'request'
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientGroupCommand implements GroupCommand {
|
||||
public readonly name = 'client'
|
||||
|
||||
public subCommandClasses = [ClientRequestCommand]
|
||||
|
||||
public readonly description = 'Group of commands to operate client node'
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
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 = 'codex-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)
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
/* 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)
|
||||
},
|
||||
}
|
||||
@ -1,235 +0,0 @@
|
||||
import { Argument, LeafCommand, Option } from 'furious-commander'
|
||||
import { RootCommand } from './root-command'
|
||||
import {
|
||||
ContainerType,
|
||||
DEFAULT_ENV_PREFIX,
|
||||
DEFAULT_IMAGE_PREFIX,
|
||||
Docker,
|
||||
RunOptions,
|
||||
HOST_COUNT,
|
||||
} from '../utils/docker'
|
||||
import { waitForBlockchain, waitForClient, waitForHosts } from '../utils/wait'
|
||||
import ora from 'ora'
|
||||
import { VerbosityLevel } from './root-command/logging'
|
||||
import { findCodexVersion, validateVersion } from '../utils/config-sources'
|
||||
|
||||
const DEFAULT_REPO = 'codexstorage'
|
||||
|
||||
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_HOSTS_KEY = 'FACTORY_HOSTS'
|
||||
const ENV_FRESH_KEY = 'FACTORY_FRESH'
|
||||
|
||||
export class Start extends RootCommand implements LeafCommand {
|
||||
public readonly name = 'start'
|
||||
|
||||
public readonly description = 'Spin up the Codex 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: 'hosts',
|
||||
alias: 'h',
|
||||
type: 'number',
|
||||
description: `Number of hosts to spin. Value between 0 and ${HOST_COUNT} including.`,
|
||||
envKey: ENV_HOSTS_KEY,
|
||||
default: HOST_COUNT,
|
||||
})
|
||||
public hosts!: number
|
||||
|
||||
@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: 'codex-version', description: 'Codex image version', required: false })
|
||||
public codexVersion!: string
|
||||
|
||||
public async run(): Promise<void> {
|
||||
await super.init()
|
||||
|
||||
if (this.hosts < 0 || this.hosts > HOST_COUNT) {
|
||||
throw new Error(`Worker count has to be between 0 and ${HOST_COUNT} including.`)
|
||||
}
|
||||
|
||||
if (!this.codexVersion) {
|
||||
this.codexVersion = await findCodexVersion()
|
||||
this.console.log('Codex version not specified. Found it configured externally.')
|
||||
this.console.log(`Spinning up cluster with Codex version ${this.codexVersion}.`)
|
||||
}
|
||||
|
||||
this.codexVersion = validateVersion(this.codexVersion)
|
||||
|
||||
const dockerOptions = await this.buildDockerOptions()
|
||||
const docker = new Docker(this.console, this.envPrefix, this.imagePrefix, this.repo)
|
||||
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.CLIENT, process.stdout)
|
||||
}
|
||||
|
||||
let clientDockerIpAddress: 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: 'Getting blockchain image version...',
|
||||
spinner: 'point',
|
||||
color: 'yellow',
|
||||
isSilent: this.verbosity === VerbosityLevel.Quiet,
|
||||
}).start()
|
||||
|
||||
try {
|
||||
const blockchainImage = await docker.getBlockchainImage(this.codexVersion)
|
||||
|
||||
blockchainSpinner.text = 'Starting blockchain node...'
|
||||
await docker.startBlockchainNode(blockchainImage, 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 clientSpinner = ora({
|
||||
text: 'Starting Codex client node...',
|
||||
spinner: 'point',
|
||||
color: 'yellow',
|
||||
isSilent: this.verbosity === VerbosityLevel.Quiet,
|
||||
}).start()
|
||||
|
||||
async function clientStatus(): Promise<boolean> {
|
||||
return (await docker.getStatusForContainer(ContainerType.CLIENT)) === 'running'
|
||||
}
|
||||
|
||||
try {
|
||||
await docker.startClientNode(this.codexVersion, dockerOptions)
|
||||
clientSpinner.text = 'Waiting until client node is ready...'
|
||||
clientDockerIpAddress = await waitForClient(clientStatus)
|
||||
clientSpinner.succeed('Client boot node is up and listening')
|
||||
} catch (e) {
|
||||
clientSpinner.fail(`It was not possible to start client node!`)
|
||||
await this.stopDocker(docker)
|
||||
throw e
|
||||
}
|
||||
|
||||
if (this.hosts > 0) {
|
||||
const hostSpinner = ora({
|
||||
text: 'Starting Codex host nodes...',
|
||||
spinner: 'point',
|
||||
color: 'yellow',
|
||||
isSilent: this.verbosity === VerbosityLevel.Quiet,
|
||||
}).start()
|
||||
|
||||
try {
|
||||
for (let i = 1; i <= this.hosts; i++) {
|
||||
await docker.startHostNode(this.codexVersion, i, clientDockerIpAddress, dockerOptions)
|
||||
}
|
||||
|
||||
hostSpinner.text = 'Waiting until all host nodes connect to client...'
|
||||
await waitForHosts(this.hosts, docker.getAllStatus.bind(docker))
|
||||
hostSpinner.succeed('Host nodes are up and listening')
|
||||
} catch (e) {
|
||||
hostSpinner.fail(`It was not possible to start host nodes!`)
|
||||
await this.stopDocker(docker)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.detach) {
|
||||
await docker.logs(ContainerType.CLIENT, process.stdout, true)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
fresh: this.fresh,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
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 Codex 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')
|
||||
}
|
||||
}
|
||||
59
src/commands/logs.ts
Normal file
59
src/commands/logs.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Args, Flags } from '@oclif/core'
|
||||
|
||||
import { BaseCommand } from '../base.js'
|
||||
import {
|
||||
ContainerType,
|
||||
DEFAULT_ENV_PREFIX,
|
||||
DEFAULT_IMAGE_PREFIX,
|
||||
Docker
|
||||
} from '../utils/docker.js'
|
||||
|
||||
const ENV_ENV_PREFIX_KEY = 'FACTORY_ENV_PREFIX'
|
||||
const ENV_IMAGE_PREFIX_KEY = 'FACTORY_IMAGE_PREFIX'
|
||||
|
||||
export default class Logs extends BaseCommand<typeof Logs> {
|
||||
static override args = {
|
||||
container: Args.string({ description: "Container name as described above", required: true }),
|
||||
};
|
||||
static override description = `Prints logs for given container. Valid container's names are: ${Object.values(
|
||||
ContainerType
|
||||
).join(', ')}`
|
||||
static override examples = [
|
||||
'<%= config.bin %> <%= command.id %>'
|
||||
]
|
||||
static override flags = {
|
||||
envPrefix: Flags.string({
|
||||
default: DEFAULT_ENV_PREFIX,
|
||||
description: 'Docker container\'s names prefix',
|
||||
env: ENV_ENV_PREFIX_KEY
|
||||
}),
|
||||
follow: Flags.boolean({
|
||||
char: 'f',
|
||||
default: false,
|
||||
description: 'Stays attached to the container and output any new logs'
|
||||
}),
|
||||
imagePrefix: Flags.string({
|
||||
default: DEFAULT_IMAGE_PREFIX,
|
||||
description: 'Docker image name prefix',
|
||||
env: ENV_IMAGE_PREFIX_KEY
|
||||
}),
|
||||
tail: Flags.integer({
|
||||
char: 't',
|
||||
description: 'Prints specified number of last log lines.',
|
||||
required: false
|
||||
})
|
||||
}
|
||||
|
||||
public async run (): Promise<void> {
|
||||
const { args, flags } = await this.parse(Logs)
|
||||
|
||||
if (!Object.values(ContainerType).includes(args.container as ContainerType)) {
|
||||
this.console.error(`Passed container name is not valid! Valid values: ${Object.values(ContainerType).join(', ')}`)
|
||||
// eslint-disable-next-line n/no-process-exit,unicorn/no-process-exit
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const docker = new Docker(this.console, flags.envPrefix, flags.imagePrefix)
|
||||
await docker.logs(args.container as ContainerType, process.stdout, flags.follow, flags.tail)
|
||||
}
|
||||
}
|
||||
222
src/commands/start.ts
Normal file
222
src/commands/start.ts
Normal file
@ -0,0 +1,222 @@
|
||||
import { Args, Flags } from "@oclif/core";
|
||||
import { OutputFlags } from "@oclif/core/interfaces";
|
||||
import ora from "ora";
|
||||
|
||||
import { BaseCommand } from "../base.js";
|
||||
import { findCodexVersion, validateVersion } from "../utils/config-sources.js";
|
||||
import {
|
||||
ContainerType,
|
||||
DEFAULT_ENV_PREFIX,
|
||||
DEFAULT_IMAGE_PREFIX,
|
||||
Docker,
|
||||
HOST_COUNT,
|
||||
RunOptions,
|
||||
} from "../utils/docker.js";
|
||||
import { VerbosityLevel } from "../utils/logging.js";
|
||||
import { waitForBlockchain, waitForClient, waitForHosts } from "../utils/wait.js";
|
||||
|
||||
const DEFAULT_REPO = "codexstorage";
|
||||
|
||||
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_HOSTS_KEY = "FACTORY_HOSTS";
|
||||
const ENV_FRESH_KEY = "FACTORY_FRESH";
|
||||
|
||||
export default class Start extends BaseCommand<typeof Start> {
|
||||
static override args = {
|
||||
codexVersion: Args.string({ description: "Codex image version", required: false }),
|
||||
};
|
||||
static override description = "Spin up the Codex Factory cluster";
|
||||
static override examples = [
|
||||
"<%= config.bin %> <%= command.id %>",
|
||||
"<%= config.bin %> <%= command.id %> 0.2.0",
|
||||
"<%= config.bin %> <%= command.id %> latest",
|
||||
];
|
||||
static override flags = {
|
||||
detach: Flags.boolean({
|
||||
char: "d",
|
||||
default: false,
|
||||
description: "Spin up the cluster and exit. No logging is outputted.",
|
||||
env: ENV_DETACH_KEY,
|
||||
}),
|
||||
envPrefix: Flags.string({
|
||||
default: DEFAULT_ENV_PREFIX,
|
||||
description: "Docker container's names prefix",
|
||||
env: ENV_ENV_PREFIX_KEY,
|
||||
}),
|
||||
fresh: Flags.boolean({
|
||||
char: "f",
|
||||
default: false,
|
||||
description: "The cluster data will be purged before start.",
|
||||
env: ENV_FRESH_KEY,
|
||||
}),
|
||||
hosts: Flags.integer({
|
||||
char: "h",
|
||||
default: HOST_COUNT,
|
||||
description: `Number of hosts to spin. Value between 0 and ${HOST_COUNT} including.`,
|
||||
env: ENV_HOSTS_KEY,
|
||||
}),
|
||||
imagePrefix: Flags.string({
|
||||
default: DEFAULT_IMAGE_PREFIX,
|
||||
description: "Docker image name prefix",
|
||||
env: ENV_IMAGE_PREFIX_KEY,
|
||||
}),
|
||||
repo: Flags.string({
|
||||
default: DEFAULT_REPO,
|
||||
description: "Docker repo where images are published",
|
||||
env: ENV_REPO_KEY,
|
||||
}),
|
||||
|
||||
|
||||
};
|
||||
|
||||
public async run(): Promise<void> {
|
||||
const { args, flags } = await this.parse(Start);
|
||||
|
||||
if (flags.hosts < 0 || flags.hosts > HOST_COUNT) {
|
||||
throw new Error(`Worker count has to be between 0 and ${HOST_COUNT} including.`)
|
||||
}
|
||||
|
||||
if (!args.codexVersion) {
|
||||
args.codexVersion = await findCodexVersion()
|
||||
this.console.log('Codex version not specified. Found it configured externally.')
|
||||
this.console.log(`Spinning up cluster with Codex version ${args.codexVersion}.`)
|
||||
}
|
||||
|
||||
args.codexVersion = validateVersion(args.codexVersion, this.config.pjson)
|
||||
|
||||
const dockerOptions = await this.buildDockerOptions(flags)
|
||||
const docker = new Docker(this.console, flags.envPrefix, flags.imagePrefix, flags.repo)
|
||||
const status = await docker.getAllStatus()
|
||||
|
||||
if (Object.values(status).every(st => st === 'running')) {
|
||||
this.console.log('All containers are up and running')
|
||||
|
||||
if (flags.detach) {
|
||||
return
|
||||
}
|
||||
|
||||
await docker.logs(ContainerType.CLIENT, process.stdout)
|
||||
}
|
||||
|
||||
let clientDockerIpAddress: string
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
try {
|
||||
await docker.stopAll(false)
|
||||
} catch (error) {
|
||||
this.console.error(`Error: ${error}`)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit()
|
||||
})
|
||||
|
||||
const networkSpinner = ora({
|
||||
color: 'yellow',
|
||||
isSilent: this.verbosity === VerbosityLevel.Quiet,
|
||||
spinner: 'point',
|
||||
text: 'Spawning network...',
|
||||
}).start()
|
||||
|
||||
try {
|
||||
await docker.createNetwork()
|
||||
networkSpinner.succeed('Network is up')
|
||||
} catch (error) {
|
||||
networkSpinner.fail(`It was not possible to spawn network!`)
|
||||
throw error
|
||||
}
|
||||
|
||||
const blockchainSpinner = ora({
|
||||
color: 'yellow',
|
||||
isSilent: this.verbosity === VerbosityLevel.Quiet,
|
||||
spinner: 'point',
|
||||
text: 'Getting blockchain image version...',
|
||||
}).start()
|
||||
|
||||
try {
|
||||
const blockchainImage = await docker.getBlockchainImage(args.codexVersion)
|
||||
|
||||
blockchainSpinner.text = 'Starting blockchain node...'
|
||||
await docker.startBlockchainNode(blockchainImage, dockerOptions)
|
||||
blockchainSpinner.text = 'Waiting until blockchain is ready...'
|
||||
await waitForBlockchain()
|
||||
blockchainSpinner.succeed('Blockchain node is up and listening')
|
||||
} catch (error) {
|
||||
blockchainSpinner.fail(`It was not possible to start blockchain node!`)
|
||||
await this.stopDocker(docker)
|
||||
throw error
|
||||
}
|
||||
|
||||
const clientSpinner = ora({
|
||||
color: 'yellow',
|
||||
isSilent: this.verbosity === VerbosityLevel.Quiet,
|
||||
spinner: 'point',
|
||||
text: 'Starting Codex client node...',
|
||||
}).start()
|
||||
|
||||
async function clientStatus(): Promise<boolean> {
|
||||
return (await docker.getStatusForContainer(ContainerType.CLIENT)) === 'running'
|
||||
}
|
||||
|
||||
try {
|
||||
await docker.startClientNode(args.codexVersion, dockerOptions)
|
||||
clientSpinner.text = 'Waiting until client node is ready...'
|
||||
clientDockerIpAddress = await waitForClient(clientStatus)
|
||||
clientSpinner.succeed('Client boot node is up and listening')
|
||||
} catch (error) {
|
||||
clientSpinner.fail(`It was not possible to start client node!`)
|
||||
await this.stopDocker(docker)
|
||||
throw error
|
||||
}
|
||||
|
||||
if (flags.hosts > 0) {
|
||||
const hostSpinner = ora({
|
||||
color: 'yellow',
|
||||
isSilent: this.verbosity === VerbosityLevel.Quiet,
|
||||
spinner: 'point',
|
||||
text: 'Starting Codex host nodes...',
|
||||
}).start()
|
||||
|
||||
try {
|
||||
for (let i = 1; i <= flags.hosts; i++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await docker.startHostNode(args.codexVersion, i, clientDockerIpAddress, dockerOptions)
|
||||
}
|
||||
|
||||
hostSpinner.text = 'Waiting until all host nodes connect to client...'
|
||||
await waitForHosts(flags.hosts, docker.getAllStatus.bind(docker))
|
||||
hostSpinner.succeed('Host nodes are up and listening')
|
||||
} catch (error) {
|
||||
hostSpinner.fail(`It was not possible to start host nodes!`)
|
||||
await this.stopDocker(docker)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (!flags.detach) {
|
||||
await docker.logs(ContainerType.CLIENT, process.stdout, true)
|
||||
}
|
||||
}
|
||||
|
||||
private async buildDockerOptions(flags: OutputFlags<typeof Start.flags>): Promise<RunOptions> {
|
||||
return {
|
||||
fresh: flags.fresh,
|
||||
}
|
||||
}
|
||||
|
||||
private async stopDocker(docker: Docker) {
|
||||
const dockerSpinner = ora({
|
||||
color: 'red',
|
||||
isSilent: this.verbosity === VerbosityLevel.Quiet,
|
||||
spinner: 'point',
|
||||
text: 'Stopping all containers...',
|
||||
}).start()
|
||||
|
||||
await docker.stopAll(false)
|
||||
|
||||
dockerSpinner.stop()
|
||||
}
|
||||
}
|
||||
56
src/commands/stop.ts
Normal file
56
src/commands/stop.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Flags } from '@oclif/core'
|
||||
import ora from 'ora'
|
||||
|
||||
import { BaseCommand } from '../base.js'
|
||||
import {
|
||||
DEFAULT_ENV_PREFIX,
|
||||
DEFAULT_IMAGE_PREFIX,
|
||||
Docker,
|
||||
} from '../utils/docker.js'
|
||||
import { VerbosityLevel } from '../utils/logging.js'
|
||||
|
||||
|
||||
const ENV_ENV_PREFIX_KEY = 'FACTORY_ENV_PREFIX'
|
||||
const ENV_IMAGE_PREFIX_KEY = 'FACTORY_IMAGE_PREFIX'
|
||||
const ENV_RM = 'FACTORY_RM'
|
||||
|
||||
export default class Stop extends BaseCommand<typeof Stop> {
|
||||
static override description = 'Stops the Codex Factory cluster'
|
||||
static override examples = [
|
||||
'<%= config.bin %> <%= command.id %>',
|
||||
]
|
||||
static override flags = {
|
||||
envPrefix: Flags.string({
|
||||
default: DEFAULT_ENV_PREFIX,
|
||||
description: 'Docker container\'s names prefix',
|
||||
env: ENV_ENV_PREFIX_KEY
|
||||
}),
|
||||
imagePrefix: Flags.string({
|
||||
default: DEFAULT_IMAGE_PREFIX,
|
||||
description: 'Docker image name prefix',
|
||||
env: ENV_IMAGE_PREFIX_KEY
|
||||
}),
|
||||
rm: Flags.boolean({
|
||||
default: false,
|
||||
description: 'Remove the containers',
|
||||
env: ENV_RM
|
||||
})
|
||||
}
|
||||
|
||||
public async run (): Promise<void> {
|
||||
const { flags } = await this.parse(Stop)
|
||||
|
||||
const docker = new Docker(this.console, flags.envPrefix, flags.imagePrefix)
|
||||
|
||||
const dockerSpinner = ora({
|
||||
color: 'yellow',
|
||||
isSilent: this.verbosity === VerbosityLevel.Quiet,
|
||||
spinner: 'point',
|
||||
text: 'Stopping all containers...'
|
||||
}).start()
|
||||
|
||||
await docker.stopAll(true, flags.rm)
|
||||
|
||||
dockerSpinner.succeed('Containers stopped')
|
||||
}
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
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]
|
||||
14
src/index.ts
14
src/index.ts
@ -1,13 +1 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { cli } from 'furious-commander'
|
||||
import { application } from './application'
|
||||
import { optionParameters, rootCommandClasses } from './config'
|
||||
import { printer } from './printer'
|
||||
|
||||
cli({
|
||||
rootCommandClasses,
|
||||
optionParameters,
|
||||
printer,
|
||||
application,
|
||||
})
|
||||
export {run} from '@oclif/core'
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
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!',
|
||||
}
|
||||
@ -1,60 +1,65 @@
|
||||
import { readFile as readFileCb } from 'fs'
|
||||
import * as path from 'path'
|
||||
import { promisify } from 'util'
|
||||
import PackageJson from '../../package.json'
|
||||
import type { PJSON } from '@oclif/core/interfaces'
|
||||
|
||||
import { readFile as readFileCb } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import semver from 'semver'
|
||||
|
||||
|
||||
const readFile = promisify(readFileCb)
|
||||
const VERSION_REGEX = /^\d+\.\d+\.\d+$/
|
||||
const COMMIT_HASH_REGEX = /^([a-f0-9]{7,10})$/i
|
||||
|
||||
export function validateVersion(version: string): string {
|
||||
export function validateVersion (version: string, pkgJson: PJSON): string {
|
||||
if (version === 'latest') {
|
||||
return version
|
||||
}
|
||||
|
||||
if (VERSION_REGEX.test(version)) {
|
||||
const supportedCodexVersion = PackageJson.engines.supportedCodex
|
||||
const supportedCodexVersion = pkgJson.engines.supportedCodex
|
||||
|
||||
if (!semver.satisfies(version, supportedCodexVersion, { includePrerelease: true })) {
|
||||
throw new Error(
|
||||
`Unsupported Codex version!\nThis version of Codex Factory supports versions: ${supportedCodexVersion}, but you have requested start of ${version}`,
|
||||
`Unsupported Codex version!\nThis version of Codex Factory supports versions: ${supportedCodexVersion}, but you have requested start of ${version}`
|
||||
)
|
||||
}
|
||||
|
||||
return version
|
||||
} else if (COMMIT_HASH_REGEX.test(version)) {
|
||||
return `sha-${version}`
|
||||
} else {
|
||||
throw new Error('The version does not have expected format!')
|
||||
}
|
||||
|
||||
if (COMMIT_HASH_REGEX.test(version)) {
|
||||
return `sha-${version}`
|
||||
}
|
||||
|
||||
throw new Error('The version does not have expected format!')
|
||||
|
||||
}
|
||||
|
||||
async function searchPackageJson(): Promise<string | undefined> {
|
||||
const expectedPath = path.join(process.cwd(), 'package.json')
|
||||
async function searchPackageJson (): Promise<string | undefined> {
|
||||
const expectedPath = join(process.cwd(), 'package.json')
|
||||
|
||||
try {
|
||||
const pkgJson = JSON.parse(await readFile(expectedPath, { encoding: 'utf8' }))
|
||||
|
||||
return pkgJson?.engines?.codex
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function searchCodexFactory(): Promise<string | undefined> {
|
||||
const expectedPath = path.join(process.cwd(), '.codexfactory.json')
|
||||
async function searchCodexFactory (): Promise<string | undefined> {
|
||||
const expectedPath = join(process.cwd(), '.codexfactory.json')
|
||||
|
||||
try {
|
||||
const pkgJson = JSON.parse(await readFile(expectedPath, { encoding: 'utf8' }))
|
||||
|
||||
return pkgJson?.version
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export async function findCodexVersion(): Promise<string> {
|
||||
export async function findCodexVersion (): Promise<string> {
|
||||
if (process.env.CODEX_FACTORY_VERSION) {
|
||||
return process.env.CODEX_FACTORY_VERSION
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
/* eslint-disable camelcase */
|
||||
import Dockerode, { Container, ContainerCreateOptions } from 'dockerode'
|
||||
import { Logging } from '../command/root-command/logging'
|
||||
import { ContainerImageConflictError } from './error'
|
||||
|
||||
import { ContainerImageConflictError } from './error.js'
|
||||
import { Logging } from './logging.js'
|
||||
|
||||
export const DEFAULT_ENV_PREFIX = 'codex-factory'
|
||||
export const DEFAULT_IMAGE_PREFIX = 'nim-codex'
|
||||
@ -40,15 +42,15 @@ export interface RunOptions {
|
||||
}
|
||||
|
||||
export enum ContainerType {
|
||||
CLIENT = 'client',
|
||||
BLOCKCHAIN = 'blockchain',
|
||||
CLIENT = 'client',
|
||||
HOST = 'host',
|
||||
HOST_2 = 'host2',
|
||||
HOST_3 = 'host3',
|
||||
HOST_4 = 'host4',
|
||||
}
|
||||
|
||||
export type Status = 'running' | 'exists' | 'not-found'
|
||||
export type Status = 'exists' | 'not-found' | 'running'
|
||||
type FindResult = { container?: Container; image?: string }
|
||||
|
||||
export interface AllStatus {
|
||||
@ -66,34 +68,12 @@ export interface DockerError extends Error {
|
||||
}
|
||||
|
||||
export class Docker {
|
||||
private docker: Dockerode
|
||||
private console: Logging
|
||||
private runningContainers: Container[]
|
||||
private docker: Dockerode
|
||||
private envPrefix: string
|
||||
private imagePrefix: string
|
||||
private repo?: string
|
||||
|
||||
private get networkName() {
|
||||
return `${this.envPrefix}${NETWORK_NAME_SUFFIX}`
|
||||
}
|
||||
|
||||
private get blockchainName() {
|
||||
return `${this.envPrefix}${BLOCKCHAIN_IMAGE_NAME_SUFFIX}`
|
||||
}
|
||||
|
||||
private hostName(index: number) {
|
||||
return `${this.envPrefix}${HOST_IMAGE_NAME_SUFFIX}-${index}`
|
||||
}
|
||||
|
||||
private get clientName() {
|
||||
return `${this.envPrefix}${CLIENT_IMAGE_NAME_SUFFIX}`
|
||||
}
|
||||
|
||||
private codexImage(codexVersion: string) {
|
||||
if (!this.repo) throw new TypeError('Repo has to be defined!')
|
||||
|
||||
return `${this.repo}/${this.imagePrefix}:${codexVersion}`
|
||||
}
|
||||
private runningContainers: Container[]
|
||||
|
||||
constructor(console: Logging, envPrefix: string, imagePrefix: string, repo?: string) {
|
||||
this.docker = new Dockerode()
|
||||
@ -104,6 +84,18 @@ export class Docker {
|
||||
this.repo = repo
|
||||
}
|
||||
|
||||
private get blockchainName() {
|
||||
return `${this.envPrefix}${BLOCKCHAIN_IMAGE_NAME_SUFFIX}`
|
||||
}
|
||||
|
||||
private get clientName() {
|
||||
return `${this.envPrefix}${CLIENT_IMAGE_NAME_SUFFIX}`
|
||||
}
|
||||
|
||||
private get networkName() {
|
||||
return `${this.envPrefix}${NETWORK_NAME_SUFFIX}`
|
||||
}
|
||||
|
||||
public async createNetwork(): Promise<void> {
|
||||
const networks = await this.docker.listNetworks({ filters: { name: [this.networkName] } })
|
||||
|
||||
@ -112,164 +104,14 @@ export class Docker {
|
||||
}
|
||||
}
|
||||
|
||||
public async startBlockchainNode(blockchainImage: string, options: RunOptions): Promise<void> {
|
||||
if (options.fresh) await this.removeContainer(this.blockchainName)
|
||||
await this.pullImageIfNotFound(blockchainImage)
|
||||
|
||||
const container = await this.findOrCreateContainer(this.blockchainName, {
|
||||
Image: blockchainImage,
|
||||
name: this.blockchainName,
|
||||
ExposedPorts: {
|
||||
'8545/tcp': {},
|
||||
},
|
||||
AttachStderr: false,
|
||||
AttachStdout: false,
|
||||
HostConfig: {
|
||||
PortBindings: { '8545/tcp': [{ HostPort: '8545' }] },
|
||||
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 startClientNode(codexVersion: string, options: RunOptions): Promise<void> {
|
||||
if (options.fresh) await this.removeContainer(this.clientName)
|
||||
await this.pullImageIfNotFound(this.codexImage(codexVersion))
|
||||
|
||||
const container = await this.findOrCreateContainer(this.clientName, {
|
||||
Image: this.codexImage(codexVersion),
|
||||
name: this.clientName,
|
||||
ExposedPorts: {
|
||||
'8070/tcp': {},
|
||||
'8080/tcp': {},
|
||||
'8090/udp': {},
|
||||
},
|
||||
Tty: true,
|
||||
Cmd: ['codex', 'persistence'],
|
||||
Env: this.createCodexEnvParameters(HARDHAT_ACCOUNTS[1], 0),
|
||||
AttachStderr: false,
|
||||
AttachStdout: false,
|
||||
HostConfig: {
|
||||
NetworkMode: this.networkName,
|
||||
PortBindings: {
|
||||
'8070/tcp': [{ HostPort: '8070' }],
|
||||
'8080/tcp': [{ HostPort: '8080' }],
|
||||
'8090/udp': [{ HostPort: '8090' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
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 Client node container was already running, so not starting it again.')
|
||||
}
|
||||
}
|
||||
|
||||
public async startHostNode(
|
||||
codexVersion: string,
|
||||
hostNumber: number,
|
||||
bootstrapAddress: string,
|
||||
options: RunOptions,
|
||||
): Promise<void> {
|
||||
if (options.fresh) await this.removeContainer(this.hostName(hostNumber))
|
||||
await this.pullImageIfNotFound(this.codexImage(codexVersion))
|
||||
|
||||
const container = await this.findOrCreateContainer(this.hostName(hostNumber), {
|
||||
Image: this.codexImage(codexVersion),
|
||||
name: this.hostName(hostNumber),
|
||||
ExposedPorts: {
|
||||
[`${8070 + hostNumber}/tcp`]: {},
|
||||
[`${8080 + hostNumber}/tcp`]: {},
|
||||
[`${8090 + hostNumber}/udp`]: {},
|
||||
},
|
||||
Env: this.createCodexEnvParameters(HARDHAT_ACCOUNTS[hostNumber + 1], hostNumber, bootstrapAddress),
|
||||
Cmd: ['codex', 'persistence', 'prover'],
|
||||
AttachStderr: false,
|
||||
AttachStdout: false,
|
||||
HostConfig: {
|
||||
NetworkMode: this.networkName,
|
||||
PortBindings: {
|
||||
[`${8070 + hostNumber}/tcp`]: [{ HostPort: (8070 + hostNumber).toString() }],
|
||||
[`${8080 + hostNumber}/tcp`]: [{ HostPort: (8080 + hostNumber).toString() }],
|
||||
[`${8090 + hostNumber}/udp`]: [{ HostPort: (8090 + hostNumber).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 client 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('Client 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 {
|
||||
blockchain: await this.getStatusForContainer(ContainerType.BLOCKCHAIN),
|
||||
client: await this.getStatusForContainer(ContainerType.CLIENT),
|
||||
host: await this.getStatusForContainer(ContainerType.HOST),
|
||||
host_2: await this.getStatusForContainer(ContainerType.HOST_2),
|
||||
host_3: await this.getStatusForContainer(ContainerType.HOST_3),
|
||||
host_4: await this.getStatusForContainer(ContainerType.HOST_4),
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,27 +129,236 @@ export class Docker {
|
||||
return blockchainImage
|
||||
}
|
||||
|
||||
public async getAllStatus(): Promise<AllStatus> {
|
||||
return {
|
||||
client: await this.getStatusForContainer(ContainerType.CLIENT),
|
||||
blockchain: await this.getStatusForContainer(ContainerType.BLOCKCHAIN),
|
||||
host: await this.getStatusForContainer(ContainerType.HOST),
|
||||
host_2: await this.getStatusForContainer(ContainerType.HOST_2),
|
||||
host_3: await this.getStatusForContainer(ContainerType.HOST_3),
|
||||
host_4: await this.getStatusForContainer(ContainerType.HOST_4),
|
||||
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'
|
||||
}
|
||||
|
||||
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('Client container does not exists, even though it should have had!')
|
||||
}
|
||||
|
||||
// @ts-expect-error: Follow is not in typings
|
||||
const logs = await container.logs({ follow, stderr: true, stdout: true, tail })
|
||||
|
||||
if (follow) {
|
||||
// @ts-expect-error: Pipe not defined
|
||||
logs.pipe(outputStream)
|
||||
} else {
|
||||
outputStream.write(logs as unknown as Buffer)
|
||||
}
|
||||
}
|
||||
|
||||
private async removeContainer(name: string): Promise<void> {
|
||||
this.console.info(`Removing container with name "${name}"`)
|
||||
const { container } = await this.findContainer(name)
|
||||
public async startBlockchainNode(blockchainImage: string, options: RunOptions): Promise<void> {
|
||||
if (options.fresh) await this.removeContainer(this.blockchainName)
|
||||
await this.pullImageIfNotFound(blockchainImage)
|
||||
|
||||
// Container does not exist so nothing to delete
|
||||
if (!container) {
|
||||
return
|
||||
const container = await this.findOrCreateContainer(this.blockchainName, {
|
||||
AttachStderr: false,
|
||||
AttachStdout: false,
|
||||
ExposedPorts: {
|
||||
'8545/tcp': {},
|
||||
},
|
||||
HostConfig: {
|
||||
NetworkMode: this.networkName,
|
||||
PortBindings: { '8545/tcp': [{ HostPort: '8545' }] },
|
||||
},
|
||||
Image: blockchainImage,
|
||||
name: this.blockchainName,
|
||||
})
|
||||
|
||||
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) {
|
||||
this.console.info('The blockchain container was already running, so not starting it again.')
|
||||
} else {
|
||||
await container.start()
|
||||
}
|
||||
}
|
||||
|
||||
public async startClientNode(codexVersion: string, options: RunOptions): Promise<void> {
|
||||
if (options.fresh) await this.removeContainer(this.clientName)
|
||||
await this.pullImageIfNotFound(this.codexImage(codexVersion))
|
||||
|
||||
const container = await this.findOrCreateContainer(this.clientName, {
|
||||
AttachStderr: false,
|
||||
AttachStdout: false,
|
||||
Cmd: ['codex', 'persistence'],
|
||||
Env: this.createCodexEnvParameters(HARDHAT_ACCOUNTS[1], 0),
|
||||
ExposedPorts: {
|
||||
'8070/tcp': {},
|
||||
'8080/tcp': {},
|
||||
'8090/udp': {},
|
||||
},
|
||||
HostConfig: {
|
||||
NetworkMode: this.networkName,
|
||||
PortBindings: {
|
||||
'8070/tcp': [{ HostPort: '8070' }],
|
||||
'8080/tcp': [{ HostPort: '8080' }],
|
||||
'8090/udp': [{ HostPort: '8090' }],
|
||||
},
|
||||
},
|
||||
Image: this.codexImage(codexVersion),
|
||||
name: this.clientName,
|
||||
Tty: true,
|
||||
})
|
||||
|
||||
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) {
|
||||
this.console.info('The Client node container was already running, so not starting it again.')
|
||||
} else {
|
||||
await container.start()
|
||||
}
|
||||
}
|
||||
|
||||
public async startHostNode(
|
||||
codexVersion: string,
|
||||
hostNumber: number,
|
||||
bootstrapAddress: string,
|
||||
options: RunOptions,
|
||||
): Promise<void> {
|
||||
if (options.fresh) await this.removeContainer(this.hostName(hostNumber))
|
||||
await this.pullImageIfNotFound(this.codexImage(codexVersion))
|
||||
|
||||
const container = await this.findOrCreateContainer(this.hostName(hostNumber), {
|
||||
AttachStderr: false,
|
||||
AttachStdout: false,
|
||||
Cmd: ['codex', 'persistence', 'prover'],
|
||||
Env: this.createCodexEnvParameters(HARDHAT_ACCOUNTS[hostNumber + 1], hostNumber, bootstrapAddress),
|
||||
ExposedPorts: {
|
||||
[`${8070 + hostNumber}/tcp`]: {},
|
||||
[`${8080 + hostNumber}/tcp`]: {},
|
||||
[`${8090 + hostNumber}/udp`]: {},
|
||||
},
|
||||
HostConfig: {
|
||||
NetworkMode: this.networkName,
|
||||
PortBindings: {
|
||||
[`${8070 + hostNumber}/tcp`]: [{ HostPort: (8070 + hostNumber).toString() }],
|
||||
[`${8080 + hostNumber}/tcp`]: [{ HostPort: (8080 + hostNumber).toString() }],
|
||||
[`${8090 + hostNumber}/udp`]: [{ HostPort: (8090 + hostNumber).toString() }],
|
||||
},
|
||||
},
|
||||
Image: this.codexImage(codexVersion),
|
||||
name: this.hostName(hostNumber),
|
||||
})
|
||||
|
||||
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) {
|
||||
this.console.info('The client node container was already running, so not starting it again.')
|
||||
} else {
|
||||
await container.start()
|
||||
}
|
||||
}
|
||||
|
||||
public async stopAll(allWithPrefix = false, deleteContainers = false): Promise<void> {
|
||||
const containerProcessor = async (container: Container) => {
|
||||
try {
|
||||
await container.stop()
|
||||
} catch (error) {
|
||||
// We ignore 304 that represents that the container is already stopped
|
||||
if ((error as DockerError).statusCode !== 304) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteContainers) {
|
||||
await container.remove()
|
||||
}
|
||||
}
|
||||
|
||||
await container.remove({ v: true, force: true })
|
||||
this.console.info('Stopping all containers')
|
||||
await Promise.all(this.runningContainers.map((element) => containerProcessor(element)))
|
||||
|
||||
if (allWithPrefix) {
|
||||
const containers = await this.docker.listContainers({ all: true })
|
||||
await Promise.all(
|
||||
containers
|
||||
.filter(container => container.Names.some(n => n.startsWith('/' + this.envPrefix)))
|
||||
.map(container => this.docker.getContainer(container.Id))
|
||||
.map((element) => containerProcessor(element)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private codexImage(codexVersion: string) {
|
||||
if (!this.repo) throw new TypeError('Repo has to be defined!')
|
||||
|
||||
return `${this.repo}/${this.imagePrefix}:${codexVersion}`
|
||||
}
|
||||
|
||||
private createCodexEnvParameters(ethAccount: string, portIndex: number, bootnode?: string): string[] {
|
||||
const options: Record<string, string> = {
|
||||
'api-bindaddr': '0.0.0.0',
|
||||
'api-cors-origin': '*',
|
||||
'api-port': `${8080 + portIndex}`,
|
||||
'disc-port': `${8090 + portIndex}`,
|
||||
'eth-account': ethAccount,
|
||||
'eth-provider': `http://${this.blockchainName}:8545`,
|
||||
'listen-addrs': `/ip4/0.0.0.0/tcp/${8070 + portIndex}`,
|
||||
'log-level': 'NOTICE; TRACE: marketplace,sales,node,restapi',
|
||||
'marketplace-address': '0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44',
|
||||
validator: 'true',
|
||||
'validator-max-slots': '1000',
|
||||
}
|
||||
|
||||
// Env variables for Codex has form of `CODEX_LOG_LEVEL`, so we need to transform it.
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
const envVariables = Object.entries(options).reduce<string[]>((previous, current) => {
|
||||
const keyName = `CODEX_${current[0].toUpperCase().replaceAll('-', '_')}`
|
||||
previous.push(`${keyName}=${current[1]}`)
|
||||
|
||||
return previous
|
||||
}, [])
|
||||
|
||||
if (bootnode) {
|
||||
envVariables.push(`BOOTSTRAP_NODE_URL=${bootnode}:8080`)
|
||||
}
|
||||
|
||||
envVariables.push('NAT_IP_AUTO=true')
|
||||
|
||||
return envVariables
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
private async findOrCreateContainer(name: string, createOptions: ContainerCreateOptions): Promise<Container> {
|
||||
@ -331,108 +382,75 @@ export class Docker {
|
||||
|
||||
try {
|
||||
return await this.docker.createContainer(createOptions)
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
// 404 is Image Not Found ==> pull the image
|
||||
if ((e as DockerError).statusCode !== 404) {
|
||||
throw e
|
||||
if ((error as DockerError).statusCode !== 404) {
|
||||
throw error
|
||||
}
|
||||
|
||||
this.console.info(`Image ${createOptions.Image} not found. Pulling it.`)
|
||||
await this.pullImageIfNotFound(createOptions.Image!)
|
||||
|
||||
return await this.docker.createContainer(createOptions)
|
||||
return 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:
|
||||
case ContainerType.BLOCKCHAIN: {
|
||||
return this.blockchainName
|
||||
case ContainerType.CLIENT:
|
||||
}
|
||||
|
||||
case ContainerType.CLIENT: {
|
||||
return this.clientName
|
||||
case ContainerType.HOST:
|
||||
}
|
||||
|
||||
case ContainerType.HOST: {
|
||||
return this.hostName(1)
|
||||
case ContainerType.HOST_2:
|
||||
}
|
||||
|
||||
case ContainerType.HOST_2: {
|
||||
return this.hostName(2)
|
||||
case ContainerType.HOST_3:
|
||||
}
|
||||
|
||||
case ContainerType.HOST_3: {
|
||||
return this.hostName(3)
|
||||
case ContainerType.HOST_4:
|
||||
}
|
||||
|
||||
case ContainerType.HOST_4: {
|
||||
return this.hostName(4)
|
||||
default:
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error('Unknown container!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createCodexEnvParameters(ethAccount: string, portIndex: number, bootnode?: string): string[] {
|
||||
const options: Record<string, string> = {
|
||||
'eth-provider': `http://${this.blockchainName}:8545`,
|
||||
'eth-account': ethAccount,
|
||||
'disc-port': `${8090 + portIndex}`,
|
||||
'listen-addrs': `/ip4/0.0.0.0/tcp/${8070 + portIndex}`,
|
||||
'api-port': `${8080 + portIndex}`,
|
||||
'api-bindaddr': '0.0.0.0',
|
||||
'api-cors-origin': '*',
|
||||
validator: 'true',
|
||||
'validator-max-slots': '1000',
|
||||
'marketplace-address': '0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44',
|
||||
'log-level': 'NOTICE; TRACE: marketplace,sales,node,restapi',
|
||||
}
|
||||
|
||||
// Env variables for Codex has form of `CODEX_LOG_LEVEL`, so we need to transform it.
|
||||
const envVariables = Object.entries(options).reduce<string[]>((previous, current) => {
|
||||
const keyName = `CODEX_${current[0].toUpperCase().replace(/-/g, '_')}`
|
||||
previous.push(`${keyName}=${current[1]}`)
|
||||
|
||||
return previous
|
||||
}, [])
|
||||
|
||||
if (bootnode) {
|
||||
envVariables.push(`BOOTSTRAP_NODE_URL=${bootnode}:8080`)
|
||||
}
|
||||
|
||||
envVariables.push('NAT_IP_AUTO=true')
|
||||
|
||||
return envVariables
|
||||
private hostName(index: number) {
|
||||
return `${this.envPrefix}${HOST_IMAGE_NAME_SUFFIX}-${index}`
|
||||
}
|
||||
|
||||
private async pullImageIfNotFound(name: string): Promise<void> {
|
||||
try {
|
||||
await this.docker.getImage(name).inspect()
|
||||
} catch (e) {
|
||||
} catch {
|
||||
this.console.info(`Image ${name} not found locally, pulling it.`)
|
||||
const pullStream = await this.docker.pull(name)
|
||||
|
||||
await new Promise(res => this.docker.modem.followProgress(pullStream, res))
|
||||
await new Promise(res => {this.docker.modem.followProgress(pullStream, res)})
|
||||
}
|
||||
}
|
||||
|
||||
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({ force: true, v: true })
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,5 +4,5 @@
|
||||
* @param ms Number of miliseconds to sleep
|
||||
*/
|
||||
export async function sleep(ms: number): Promise<void> {
|
||||
return new Promise<void>(resolve => setTimeout(() => resolve(), ms))
|
||||
return new Promise<void>(resolve => {setTimeout(() => resolve(), ms)})
|
||||
}
|
||||
|
||||
@ -1,4 +1,25 @@
|
||||
import { Printer } from './printer'
|
||||
import chalk from 'chalk'
|
||||
|
||||
const Printer = {
|
||||
dimFunction(message: string, ...args: unknown[]): void {
|
||||
console.log(chalk.dim(message), ...args)
|
||||
},
|
||||
divider(char = '-'): void {
|
||||
console.log(char.repeat(process.stdout.columns))
|
||||
},
|
||||
emptyFunction(): void {
|
||||
|
||||
},
|
||||
error(message: string, ...args: unknown[]): void {
|
||||
console.error(chalk.red(message), ...args)
|
||||
},
|
||||
info(message: string, ...args: unknown[]): void {
|
||||
console.log(chalk.dim(message), ...args)
|
||||
},
|
||||
log(message: string, ...args: unknown[]): void {
|
||||
console.log(message, ...args)
|
||||
},
|
||||
}
|
||||
|
||||
export enum VerbosityLevel {
|
||||
/** No output message, only at errors or result strings (e.g. hash of uploaded file) */
|
||||
@ -12,34 +33,38 @@ export enum VerbosityLevel {
|
||||
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
|
||||
// Callable logging functions (instead of console.log)
|
||||
/** Informal messages (e.g. Tips) */
|
||||
public info: PrinterFnc
|
||||
/** Identical with console.log */
|
||||
public log: PrinterFnc
|
||||
/** Informal messages (e.g. Tips) */
|
||||
public info: PrinterFnc
|
||||
public readonly verbosityLevel: VerbosityLevel
|
||||
|
||||
constructor(verbosityLevel: VerbosityLevel) {
|
||||
this.verbosityLevel = verbosityLevel
|
||||
switch (verbosityLevel) {
|
||||
case VerbosityLevel.Verbose:
|
||||
case VerbosityLevel.Normal: {
|
||||
this.error = Printer.error
|
||||
this.log = Printer.log
|
||||
this.info = Printer.emptyFunction
|
||||
break
|
||||
}
|
||||
|
||||
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:
|
||||
}
|
||||
|
||||
default: {
|
||||
// quiet
|
||||
this.error = Printer.error
|
||||
this.log = Printer.emptyFunction
|
||||
this.info = Printer.emptyFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,20 @@
|
||||
import fetch, { FetchError } from 'node-fetch'
|
||||
import { sleep } from './index'
|
||||
import { TimeoutError } from './error'
|
||||
import { AllStatus } from './docker'
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { Codex } from '@codex-storage/sdk-js'
|
||||
import fetch, { FetchError } from 'node-fetch'
|
||||
|
||||
const AWAIT_SLEEP = 3_000
|
||||
import { AllStatus } from './docker.js'
|
||||
import { TimeoutError } from './error.js'
|
||||
import { sleep } from './index.js'
|
||||
|
||||
const BLOCKCHAIN_BODY_REQUEST = JSON.stringify({ jsonrpc: '2.0', method: 'eth_chainId', id: 1 })
|
||||
const AWAIT_SLEEP = 3000
|
||||
|
||||
const BLOCKCHAIN_BODY_REQUEST = JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' })
|
||||
const EXPECTED_CHAIN_ID = '0x7a69'
|
||||
const ALLOWED_ERRORS = ['ECONNREFUSED', 'ECONNRESET', 'UND_ERR_SOCKET']
|
||||
|
||||
function isAllowedError(e: FetchError): boolean {
|
||||
//@ts-ignore: Node 18 native fetch returns error where the underlying error is wrapped and placed in e.cause
|
||||
if (e.cause) {
|
||||
//@ts-ignore: Node 18 native fetch returns error where the underlying error is wrapped and placed in e.cause
|
||||
// @ts-expect-error: Node 18 native fetch returns error where the underlying error is wrapped and placed in e.cause
|
||||
e = e.cause
|
||||
}
|
||||
|
||||
@ -35,27 +36,28 @@ function extractIpFromMultiaddr(multiaddr: string): string {
|
||||
|
||||
if (match) {
|
||||
return match[1]
|
||||
} else {
|
||||
throw new Error('Unsupported multiaddr')
|
||||
}
|
||||
|
||||
throw new Error('Unsupported multiaddr')
|
||||
|
||||
}
|
||||
|
||||
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:8545', {
|
||||
method: 'POST',
|
||||
body: BLOCKCHAIN_BODY_REQUEST,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
})
|
||||
const response = (await request.json()) as { result: string }
|
||||
|
||||
if (response.result === EXPECTED_CHAIN_ID) {
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
if (!isAllowedError(e as FetchError)) {
|
||||
throw e
|
||||
} catch (error) {
|
||||
if (!isAllowedError(error as FetchError)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,9 +92,9 @@ export async function waitForClient(
|
||||
return extractIpFromMultiaddr(addr)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (!isAllowedError(e as FetchError)) {
|
||||
throw e
|
||||
} catch (error) {
|
||||
if (!isAllowedError(error as FetchError)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,6 +116,7 @@ export async function waitForHosts(
|
||||
if (status[`host` as keyof AllStatus] !== 'running') {
|
||||
throw new Error('Some of the hosts node is not running!')
|
||||
}
|
||||
|
||||
for (let i = 2; i <= hostCount; i++) {
|
||||
if (status[`host_${i}` as keyof AllStatus] !== 'running') {
|
||||
throw new Error('Some of the hosts node is not running!')
|
||||
@ -131,9 +134,9 @@ export async function waitForHosts(
|
||||
if (info.data.table.nodes.length >= hostCount) {
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
if (!isAllowedError(e as FetchError)) {
|
||||
throw e
|
||||
} catch (error) {
|
||||
if (!isAllowedError(error as FetchError)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
182
test/commands/start.test.ts
Normal file
182
test/commands/start.test.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import { Codex } from '@codex-storage/sdk-js'
|
||||
import { runCommand } from '@oclif/test'
|
||||
import { use as chaiUse, expect } from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import Dockerode from 'dockerode'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
|
||||
import { ENV_ENV_PREFIX_KEY } from '../../src/commands/start'
|
||||
import { DockerError } from '../../src/utils/docker'
|
||||
import { deleteNetwork, findContainer } from '../utils/docker'
|
||||
|
||||
chaiUse(chaiAsPromised)
|
||||
let testFailed = false
|
||||
|
||||
function wrapper (fn: () => Promise<unknown>): () => Promise<unknown> {
|
||||
return async () => {
|
||||
try {
|
||||
const result = await fn()
|
||||
testFailed = false
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
testFailed = true
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
try {
|
||||
console.log('SIGINT received, stopping the cluster')
|
||||
|
||||
if (process.env[ENV_ENV_PREFIX_KEY]) {
|
||||
console.log('Cleaning up containers')
|
||||
await runCommand('stop --rm') // Cleanup the testing containers
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error}`)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit()
|
||||
})
|
||||
|
||||
describe('start command', () => {
|
||||
let docker: Dockerode
|
||||
let codexClient: Codex
|
||||
const envPrefix = `codex-factory-test-${randomBytes(4).toString('hex')}`
|
||||
|
||||
before(() => {
|
||||
docker = new Dockerode()
|
||||
codexClient = new Codex('http://127.0.0.1:8080')
|
||||
|
||||
// This will force Codex Factory to create fresh images
|
||||
process.env[ENV_ENV_PREFIX_KEY] = envPrefix
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (testFailed) {
|
||||
console.log('List of containers:')
|
||||
const containers = await docker.listContainers()
|
||||
for (const c of containers) console.log(` - ${c.Names.join(', ')}`)
|
||||
|
||||
await runCommand('logs client')
|
||||
}
|
||||
|
||||
await runCommand('stop')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runCommand('stop --rm') // Cleanup the testing containers
|
||||
await deleteNetwork(docker, `${envPrefix}-network`)
|
||||
})
|
||||
|
||||
it(
|
||||
'should start cluster',
|
||||
wrapper(async () => {
|
||||
// As spinning the cluster with --detach the command will exit once the cluster is up and running
|
||||
await runCommand('start --detach')
|
||||
|
||||
await expect(findContainer(docker, 'client')).to.eventually.be.not.undefined
|
||||
await expect(findContainer(docker, 'blockchain')).to.eventually.be.not.undefined
|
||||
await expect(findContainer(docker, 'host-1')).to.eventually.be.not.undefined
|
||||
await expect(findContainer(docker, 'host-2')).to.eventually.be.not.undefined
|
||||
await expect(findContainer(docker, 'host-3')).to.eventually.be.not.undefined
|
||||
await expect(findContainer(docker, 'host-4')).to.eventually.be.not.undefined
|
||||
|
||||
expect((await codexClient.debug.info()).data).to.have.property('id')
|
||||
})
|
||||
)
|
||||
|
||||
describe('should start cluster with just few hosts', () => {
|
||||
before(async () => {
|
||||
await runCommand('stop --rm') // Cleanup the testing containers
|
||||
})
|
||||
|
||||
it(
|
||||
'test',
|
||||
wrapper(async () => {
|
||||
// As spinning the cluster with --detach the command will exit once the cluster is up and running
|
||||
await runCommand('start --hosts 2')
|
||||
|
||||
await expect(findContainer(docker, 'client')).to.eventually.be.not.undefined
|
||||
await expect(findContainer(docker, 'blockchain')).to.eventually.be.not.undefined
|
||||
await expect(findContainer(docker, 'host-1')).to.eventually.be.not.undefined
|
||||
await expect(findContainer(docker, 'host-2')).to.eventually.be.not.undefined
|
||||
await expect(findContainer(docker, 'host-3')).to.be.rejected
|
||||
await expect(findContainer(docker, 'host-4')).to.be.rejected
|
||||
|
||||
expect((await codexClient.debug.info()).data).to.have.property('id')
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('should create docker network', () => {
|
||||
before(async () => {
|
||||
await runCommand('stop --rm') // Cleanup the testing containers
|
||||
|
||||
try {
|
||||
// Make sure the network does not exists
|
||||
await (await docker.getNetwork(`${envPrefix}-network`)).remove()
|
||||
} catch (error) {
|
||||
if ((error as DockerError).statusCode !== 404) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it(
|
||||
'test',
|
||||
wrapper(async () => {
|
||||
await runCommand('start --detach')
|
||||
|
||||
|
||||
expect(docker.getNetwork(`${envPrefix}-network`)).to.be.not.undefined
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('should remove containers with --fresh option', () => {
|
||||
let availabilityId: string
|
||||
|
||||
before(async () => {
|
||||
console.log('(before) Starting up Codex Factory')
|
||||
await runCommand('start --detach')
|
||||
const result = await codexClient.marketplace.createAvailability({
|
||||
duration: 100,
|
||||
minPricePerBytePerSecond: 100,
|
||||
totalCollateral: 1,
|
||||
totalSize: 3000
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
throw result.data
|
||||
}
|
||||
|
||||
availabilityId = result.data.id
|
||||
|
||||
console.log('(before) Stopping the Codex Factory')
|
||||
await runCommand('stop') // Cleanup the testing containers
|
||||
})
|
||||
|
||||
it(
|
||||
'test',
|
||||
wrapper(async () => {
|
||||
console.log('(test) Starting the Codex Factory')
|
||||
await runCommand('start --fresh --detach')
|
||||
|
||||
console.log('(test) Trying to fetch the data')
|
||||
const availabilities = await codexClient.marketplace.availabilities()
|
||||
|
||||
if (availabilities.error) {
|
||||
throw availabilities.data
|
||||
}
|
||||
|
||||
if (availabilities.data.some(({ id }) => id === availabilityId)) {
|
||||
throw new Error('Availability was not removed!')
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
92
test/commands/stop.test.ts
Normal file
92
test/commands/stop.test.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { runCommand } from '@oclif/test'
|
||||
import { use as chaiUse, expect } from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import Dockerode from 'dockerode'
|
||||
import {randomBytes} from 'node:crypto'
|
||||
|
||||
import { ENV_ENV_PREFIX_KEY } from '../../src/commands/start'
|
||||
import { deleteNetwork, findContainer } from '../utils/docker'
|
||||
|
||||
chaiUse(chaiAsPromised)
|
||||
|
||||
describe('stop command', () => {
|
||||
let docker: Dockerode
|
||||
const envPrefix = `codex-factory-test-${randomBytes(4).toString('hex')}`
|
||||
|
||||
before(() => {
|
||||
docker = new Dockerode()
|
||||
|
||||
// This will force Codex Factory to create fresh images
|
||||
process.env[ENV_ENV_PREFIX_KEY] = envPrefix
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runCommand('stop --rm') // Cleanup the testing containers
|
||||
await deleteNetwork(docker, `${envPrefix}-network`)
|
||||
})
|
||||
|
||||
describe('should stop cluster', () => {
|
||||
before(async () => {
|
||||
// As spinning the cluster with --detach the command will exit once the cluster is up and running
|
||||
await runCommand('start --detach')
|
||||
})
|
||||
|
||||
it('test', async () => {
|
||||
await expect(findContainer(docker, 'client')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'blockchain')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-1')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-2')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-3')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-4')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
|
||||
await runCommand('stop') // Cleanup the testing containers
|
||||
|
||||
await expect(findContainer(docker, 'client')).to.eventually.have.nested.property('State.Status', 'exited')
|
||||
await expect(findContainer(docker, 'blockchain')).to.eventually.have.nested.property('State.Status', 'exited')
|
||||
await expect(findContainer(docker, 'host-1')).to.eventually.have.nested.property('State.Status', 'exited')
|
||||
await expect(findContainer(docker, 'host-2')).to.eventually.have.nested.property('State.Status', 'exited')
|
||||
await expect(findContainer(docker, 'host-3')).to.eventually.have.nested.property('State.Status', 'exited')
|
||||
await expect(findContainer(docker, 'host-4')).to.eventually.have.nested.property('State.Status', 'exited')
|
||||
})
|
||||
})
|
||||
|
||||
describe('should stop cluster and remove containers', () => {
|
||||
before(async () => {
|
||||
// As spinning the cluster with --detach the command will exit once the cluster is up and running
|
||||
await runCommand('start --detach')
|
||||
|
||||
})
|
||||
|
||||
it('test', async () => {
|
||||
await expect(findContainer(docker, 'client')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'blockchain')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-1')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-2')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-3')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-4')).to.eventually.have.nested.property('State.Status', 'running')
|
||||
|
||||
await runCommand('stop --rm') // Cleanup the testing containers
|
||||
|
||||
await expect(findContainer(docker, 'client')).to.be.rejected
|
||||
await expect(findContainer(docker, 'blockchain')).to.be.rejected
|
||||
await expect(findContainer(docker, 'host-1')).to.be.rejected
|
||||
await expect(findContainer(docker, 'host-2')).to.be.rejected
|
||||
await expect(findContainer(docker, 'host-3')).to.be.rejected
|
||||
await expect(findContainer(docker, 'host-4')).to.be.rejected
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
try {
|
||||
if (process.env[ENV_ENV_PREFIX_KEY]) {
|
||||
console.log('Cleaning up containers')
|
||||
await runCommand('stop --rm') // Cleanup the testing containers
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error}`)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit()
|
||||
})
|
||||
@ -1,162 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
import Dockerode from 'dockerode'
|
||||
import crypto from 'crypto'
|
||||
import { Codex } from '@codex-storage/sdk-js'
|
||||
|
||||
import { run } from '../utils/run'
|
||||
import { ENV_ENV_PREFIX_KEY } from '../../src/command/start'
|
||||
|
||||
import { DockerError } from '../../src/utils/docker'
|
||||
import { findContainer } from '../utils/docker'
|
||||
|
||||
let testFailed = false
|
||||
|
||||
function wrapper(fn: () => Promise<unknown>): () => Promise<unknown> {
|
||||
return async () => {
|
||||
try {
|
||||
const result = await fn()
|
||||
testFailed = false
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
testFailed = true
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('start command', () => {
|
||||
let docker: Dockerode
|
||||
let codexClient: Codex, codexHost: Codex
|
||||
const envPrefix = `codex-factory-test-${crypto.randomBytes(4).toString('hex')}`
|
||||
|
||||
beforeAll(() => {
|
||||
docker = new Dockerode()
|
||||
codexClient = new Codex('http://127.0.0.1:8080')
|
||||
codexHost = new Codex('http://127.0.0.1:8081')
|
||||
|
||||
// This will force Codex Factory to create
|
||||
process.env[ENV_ENV_PREFIX_KEY] = envPrefix
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (testFailed) {
|
||||
console.log('List of containers:')
|
||||
const containers = await docker.listContainers()
|
||||
containers.forEach(c => console.log(` - ${c.Names.join(', ')}`))
|
||||
|
||||
await run(['logs', 'client'])
|
||||
}
|
||||
|
||||
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'])
|
||||
|
||||
await expect(findContainer(docker, 'client')).resolves.toBeDefined()
|
||||
await expect(findContainer(docker, 'blockchain')).resolves.toBeDefined()
|
||||
await expect(findContainer(docker, 'host-1')).resolves.toBeDefined()
|
||||
await expect(findContainer(docker, 'host-2')).resolves.toBeDefined()
|
||||
await expect(findContainer(docker, 'host-3')).resolves.toBeDefined()
|
||||
await expect(findContainer(docker, 'host-4')).resolves.toBeDefined()
|
||||
|
||||
expect((await codexClient.debug.info()).data).toHaveProperty('id')
|
||||
}),
|
||||
)
|
||||
|
||||
describe('should start cluster with just few hosts', () => {
|
||||
beforeAll(async () => {
|
||||
await run(['stop', '--rm']) // Cleanup the testing containers
|
||||
})
|
||||
|
||||
it(
|
||||
'',
|
||||
wrapper(async () => {
|
||||
// As spinning the cluster with --detach the command will exit once the cluster is up and running
|
||||
await run(['start', '--hosts', '2'])
|
||||
|
||||
await expect(findContainer(docker, 'client')).resolves.toBeDefined()
|
||||
await expect(findContainer(docker, 'blockchain')).resolves.toBeDefined()
|
||||
await expect(findContainer(docker, 'host-1')).resolves.toBeDefined()
|
||||
await expect(findContainer(docker, 'host-2')).resolves.toBeDefined()
|
||||
await expect(findContainer(docker, 'host-3')).rejects.toHaveProperty('statusCode', 404)
|
||||
await expect(findContainer(docker, 'host-4')).rejects.toHaveProperty('statusCode', 404)
|
||||
|
||||
expect((await codexClient.debug.info()).data).toHaveProperty('id')
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
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'])
|
||||
|
||||
expect(docker.getNetwork(`${envPrefix}-network`)).toBeDefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('should remove containers with --fresh option', () => {
|
||||
let availabilityId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
console.log('(before) Starting up Codex Factory')
|
||||
await run(['start', '--detach'])
|
||||
const result = await codexClient.marketplace.createAvailability({
|
||||
totalCollateral: 1,
|
||||
totalSize: 3000,
|
||||
minPricePerBytePerSecond: 100,
|
||||
duration: 100,
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
throw result.data
|
||||
}
|
||||
|
||||
availabilityId = result.data.id
|
||||
|
||||
console.log('(before) Stopping the Codex Factory')
|
||||
await run(['stop'])
|
||||
})
|
||||
|
||||
it(
|
||||
'',
|
||||
wrapper(async () => {
|
||||
console.log('(test) Starting the Codex Factory')
|
||||
await run(['start', '--fresh', '--detach'])
|
||||
|
||||
console.log('(test) Trying to fetch the data')
|
||||
expect((await codexClient.marketplace.availabilities()).data).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.stringContaining(availabilityId),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1,73 +0,0 @@
|
||||
/* 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'
|
||||
|
||||
describe('stop command', () => {
|
||||
let docker: Dockerode
|
||||
const envPrefix = `codex-factory-test-${crypto.randomBytes(4).toString('hex')}`
|
||||
|
||||
beforeAll(() => {
|
||||
docker = new Dockerode()
|
||||
|
||||
// This will force Codex 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'])
|
||||
})
|
||||
|
||||
it('', async () => {
|
||||
await expect(findContainer(docker, 'client')).resolves.toHaveProperty('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'blockchain')).resolves.toHaveProperty('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-1')).resolves.toHaveProperty('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-2')).resolves.toHaveProperty('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-3')).resolves.toHaveProperty('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-4')).resolves.toHaveProperty('State.Status', 'running')
|
||||
|
||||
await run(['stop'])
|
||||
|
||||
await expect(findContainer(docker, 'client')).resolves.toHaveProperty('State.Status', 'exited')
|
||||
await expect(findContainer(docker, 'blockchain')).resolves.toHaveProperty('State.Status', 'exited')
|
||||
await expect(findContainer(docker, 'host-1')).resolves.toHaveProperty('State.Status', 'exited')
|
||||
await expect(findContainer(docker, 'host-2')).resolves.toHaveProperty('State.Status', 'exited')
|
||||
await expect(findContainer(docker, 'host-3')).resolves.toHaveProperty('State.Status', 'exited')
|
||||
await expect(findContainer(docker, 'host-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'])
|
||||
})
|
||||
|
||||
it('', async () => {
|
||||
await expect(findContainer(docker, 'client')).resolves.toHaveProperty('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'blockchain')).resolves.toHaveProperty('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-1')).resolves.toHaveProperty('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-2')).resolves.toHaveProperty('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-3')).resolves.toHaveProperty('State.Status', 'running')
|
||||
await expect(findContainer(docker, 'host-4')).resolves.toHaveProperty('State.Status', 'running')
|
||||
|
||||
await run(['stop', '--rm'])
|
||||
|
||||
await expect(findContainer(docker, 'client')).rejects.toHaveProperty('statusCode', 404)
|
||||
await expect(findContainer(docker, 'blockchain')).rejects.toHaveProperty('statusCode', 404)
|
||||
await expect(findContainer(docker, 'host-1')).rejects.toHaveProperty('statusCode', 404)
|
||||
await expect(findContainer(docker, 'host-2')).rejects.toHaveProperty('statusCode', 404)
|
||||
await expect(findContainer(docker, 'host-3')).rejects.toHaveProperty('statusCode', 404)
|
||||
await expect(findContainer(docker, 'host-4')).rejects.toHaveProperty('statusCode', 404)
|
||||
})
|
||||
})
|
||||
})
|
||||
9
test/tsconfig.json
Normal file
9
test/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"references": [
|
||||
{"path": "../../codex-factory-new"}
|
||||
]
|
||||
}
|
||||
@ -1,10 +1,18 @@
|
||||
import Dockerode from 'dockerode'
|
||||
import { ENV_ENV_PREFIX_KEY } from '../../src/command/start'
|
||||
import * as Dockerode from 'dockerode'
|
||||
|
||||
import { ENV_ENV_PREFIX_KEY } from '../../src/commands/start'
|
||||
|
||||
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 deleteNetwork(docker: Dockerode, name: string): Promise<void> {
|
||||
const network = (await docker.listNetworks()).find(n => n.Name === name)
|
||||
if (network) {
|
||||
await docker.getNetwork(network.Id).remove()
|
||||
}
|
||||
}
|
||||
|
||||
export async function sleep(ms: number): Promise<void> {
|
||||
return new Promise<void>(resolve => {setTimeout(() => resolve(), ms)})
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,20 +1,15 @@
|
||||
{
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"compilerOptions": {
|
||||
"alwaysStrict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"module": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"typeRoots": ["./src/types", "node_modules/@types"],
|
||||
"target": "es2015",
|
||||
"rootDirs": ["src"],
|
||||
"module": "commonjs",
|
||||
"outDir": "dist",
|
||||
"resolveJsonModule": true,
|
||||
"useDefineForClassFields": true
|
||||
"target": "es2022",
|
||||
"moduleResolution": "node16"
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src",
|
||||
"test",
|
||||
"jest.config.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user