Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flux CLI #1070

Merged
merged 20 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ jobs:
run: |
if [[ "${{ steps.publish_docker_js_server.outcome }}" == 'success' ]]; then
echo "Redeploying flux docker js_server."
npx tsx ./dev/scripts/dist/scripts/fluxDeploy.js ProcaptchaJavascriptServer
npx tsx ./dev/flux/dist/index.js deploy --app ProcaptchaJavascriptServer
else
echo "Skipping flux redeploy."
exit 1
Expand Down
95 changes: 95 additions & 0 deletions dev/flux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# @prosopo/flux

This package contains a Command Line Interface (CLI) for interacting with the Flux network.

## CLI Usage

### Authentication

#### Authenticate with Flux Main Site only

Use to generate the authentication for a Flux node for a specific app.This allows you to go straight
to the [RunOnFlux Login](https://cloud.runonflux.io/login.html) and login with the login phrase and signature.

```bash
# authenticate with the network for a specific app
npm run cli auth
{
nodeAPIURL: URL {
href: 'https://api.runonflux.io/',
origin: 'https://api.runonflux.io',
...
},
nodeLoginPhrase: 'asdlkadalkdalskdasldkadlkadlkaldkasdlk',
nodeSignature: 'lkjasdlajsdlkajdlkajdlaskjdlaskjdlaskjdaslkd/9I='
}

```

#### Authenticate with a Node and an App

Use to generate the authentication for a Flux node for a specific app. This allows you to go straight to the node for
the app you are working with and login with the login phrase and signature.

```bash
# authenticate with the network for a specific app
npm run cli auth -- --app <app_name>
{
nodeAPIURL: URL {
# You can now visit this (using port 16126) to login with the login phrase and signature
href: 'http://x.x.x.x:16127/',
origin: 'http://x.x.x.x:16127',
...
},
nodeLoginPhrase: 'asdlkadalkdalskdasldkadlkadlkaldkasdlk',
nodeSignature: 'lkjasdlajsdlkajdlkajdlaskjdlaskjdlaskjdaslkd/9I='
}
```

### Get Dapp Details

#### Get Dapps

Use this command to get a list of all the dapps that are running on the network.

```bash
# get all dapps
npm run cli getDapps
```

#### Get Dapp

Use this command to get the IP addresses of the nodes that are running the app.

```bash
# get details of a specific dapp
npm run cli getDapp -- --app <app_name>
```

### Redeploy

#### Soft redeploy

Restart the app on all nodes that are running the app.

```bash
# redeploy the app
npm run cli redeploy -- --app <app_name>
...
ℹ apiUrl: http://x.x.x.x:16127/id/verifylogin deploy.js 15:00:42
ℹ { status: 'success', deploy.js 15:00:42
data: { message: '<app_name> queried for global soft redeploy' } }
```

#### Hard redeploy

Remove the container and restart the app on all nodes that are running the app.

```bash
# hard redeploy the app
npm run cli redeploy -- --app <app_name> --hard
...
ℹ apiUrl: http://x.x.x.x:16127/id/verifylogin deploy.js 15:00:42
ℹ { status: 'success', deploy.js 15:00:42
data: { message: '<app_name> queried for global hard redeploy' } }
```
4 changes: 4 additions & 0 deletions dev/flux/env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
NODE_ENV=development
PROSOPO_LOG_LEVEL=debug
PROSOPO_ZELCORE_PRIVATE_KEY=
PROSOPO_ZELCORE_PUBLIC_KEY=
51 changes: 51 additions & 0 deletions dev/flux/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@prosopo/flux",
"version": "0.2.41",
"description": "Tools for managing Flux deployment",
"main": "dist/index.js",
"type": "module",
"engines": {
"node": ">=18",
"npm": ">=9"
},
"scripts": {
"clean": "tsc --build --clean",
"build": "tsc --build --verbose",
"test": "NODE_OPTIONS=--max-old-space-size=4096 npx vitest run --config ./src/vite.config.ts",
"cli": "node dist/index.js",
"eslint": "npx eslint . --no-error-on-unmatched-pattern --ignore-path ../../.eslintignore",
"eslint:fix": "npm run eslint -- --fix",
"prettier": "npx prettier . --check --no-error-on-unmatched-pattern --ignore-path ../../.eslintignore",
"prettier:fix": "npm run prettier -- --write",
"lint": "npm run eslint && npm run prettier",
"lint:fix": "npm run eslint:fix && npm run prettier:fix"
},
"author": "Prosopo Limited",
"license": "Apache-2.0",
"dependencies": {
"@noble/curves": "^1.3.0",
"@polkadot/util": "12.6.1",
"@polkadot/util-crypto": "12.6.1",
"@prosopo/cli": "0.2.41",
"@prosopo/common": "0.2.41",
"@prosopo/util": "0.2.41",
"consola": "^3.2.3",
"dotenv": "^16.0.3",
"glob": "^10.0.0",
"qs": "^6.11.2",
"socket.io-client": "^4.7.4",
"varuint-bitcoin": "^1.1.2",
"yargs": "^17.5.1",
"yargs-parser": "^21.0.1"
},
"overrides": {
"@polkadot/keyring": "12.6.1"
},
"devDependencies": {
"@esm-bundle/chai": "^4.3.4-fix.0",
"ts-node": "^10.9.1",
"tslib": "2.6.2",
"typescript": "5.1.6",
"vitest": "^0.34.2"
}
}
43 changes: 43 additions & 0 deletions dev/flux/src/commands/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as z from 'zod'
import { ArgumentsCamelCase, Argv } from 'yargs'
import { LogLevel, Logger, getLogger } from '@prosopo/common'
import { getPrivateKey, getPublicKey } from './process.env.js'
import { main } from '../lib/auth.js'

const fluxAuthArgs = z.object({
app: z.string().optional(),
ip: z.string().optional(),
})

export default (cmdArgs?: { logger?: Logger }) => {
const logger = cmdArgs?.logger || getLogger(LogLevel.enum.info, 'flux.cli.auth')

return {
command: 'auth',
describe: 'Authenticate with a Flux Node',
builder: (yargs: Argv) =>
yargs
.option('app', {
type: 'string' as const,
demandOption: false,
desc: 'Name of the app to authenticate with',
} as const)
.option('ip', {
type: 'string' as const,
demandOption: false,
desc: 'IP address of Flux machine to authenticate with',
} as const),
handler: async (argv: ArgumentsCamelCase) => {
try {
const privateKey = getPrivateKey()
const publicKey = getPublicKey()
const parsedArgs = fluxAuthArgs.parse(argv)
const result = await main(publicKey, privateKey, parsedArgs.app, parsedArgs.ip)
logger.info(result)
} catch (err) {
logger.error(err)
}
},
middlewares: [],
}
}
37 changes: 37 additions & 0 deletions dev/flux/src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as z from 'zod'
import { ArgumentsCamelCase, Argv } from 'yargs'
import { LogLevel, Logger, getLogger } from '@prosopo/common'
import { getPrivateKey, getPublicKey } from './process.env.js'
import { main } from '../lib/deploy.js'

const fluxDeployArgs = z.object({
app: z.string(),
ip: z.string().optional(),
hard: z.boolean().optional(),
})

export default (cmdArgs?: { logger?: Logger }) => {
const logger = cmdArgs?.logger || getLogger(LogLevel.enum.info, 'flux.cli.deploy')

return {
command: 'deploy',
describe: 'Deploy a Flux application',
builder: (yargs: Argv) =>
yargs.option('hard', {
type: 'boolean' as const,
demand: false,
desc: 'Whether to hard redeploy the app',
} as const),
handler: async (argv: ArgumentsCamelCase) => {
try {
const privateKey = getPrivateKey()
const publicKey = getPublicKey()
const parsedArgs = fluxDeployArgs.parse(argv)
await main(publicKey, privateKey, parsedArgs.app, parsedArgs.ip, parsedArgs.hard)
} catch (err) {
logger.error(err)
}
},
middlewares: [],
}
}
35 changes: 35 additions & 0 deletions dev/flux/src/commands/getDapp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as z from 'zod'
import { ArgumentsCamelCase, Argv } from 'yargs'
import { LogLevel, Logger, getLogger } from '@prosopo/common'
import { getAuth, getIndividualFluxAppDetails } from '../lib/auth.js'
import { getPrivateKey, getPublicKey } from './process.env.js'
const fluxGetDappArgs = z.object({
app: z.string(),
})
export default (cmdArgs?: { logger?: Logger }) => {
const logger = cmdArgs?.logger || getLogger(LogLevel.enum.info, 'flux.cli.getDapp')

return {
command: 'getDapp',
describe: 'Get dapp details',
builder: (yargs: Argv) =>
yargs.option('app', {
type: 'string' as const,
demandOption: false,
desc: 'Name of the dapp to get the details of',
} as const),
handler: async (argv: ArgumentsCamelCase) => {
try {
const privateKey = getPrivateKey()
const publicKey = getPublicKey()
const { signature, loginPhrase } = await getAuth(privateKey)
const parsedArgs = fluxGetDappArgs.parse(argv)
const dapp = await getIndividualFluxAppDetails(parsedArgs.app, publicKey, signature, loginPhrase)
logger.info(JSON.stringify(dapp, null, 2))
} catch (err) {
logger.error(err)
}
},
middlewares: [],
}
}
23 changes: 23 additions & 0 deletions dev/flux/src/commands/getDapps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LogLevel, Logger, getLogger } from '@prosopo/common'
import { getPrivateKey, getPublicKey } from './process.env.js'
import { main } from '../lib/getDapps.js'

export default (cmdArgs?: { logger?: Logger }) => {
const logger = cmdArgs?.logger || getLogger(LogLevel.enum.info, 'flux.cli.getDapps')

return {
command: 'getDapps',
describe: 'Get dapp details',
handler: async () => {
try {
const privateKey = getPrivateKey()
const publicKey = getPublicKey()
const dapps = await main(publicKey, privateKey)
logger.info(JSON.stringify(dapps, null, 2))
} catch (err) {
logger.error(err)
}
},
middlewares: [],
}
}
5 changes: 5 additions & 0 deletions dev/flux/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { default as commandDeploy } from './deploy.js'
export { default as commandAuth } from './auth.js'
export { default as commandGetDapps } from './getDapps.js'
export { default as commandGetDapp } from './getDapp.js'
export { default as commandTerminal } from './terminal.js'
23 changes: 23 additions & 0 deletions dev/flux/src/commands/process.env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ProsopoEnvError } from '@prosopo/common'
import { wifToPrivateKey } from '../lib/sep256k1Sign.js'

export const getPrivateKey = () => {
const secret = process.env['PROSOPO_ZELCORE_PRIVATE_KEY']
if (!secret) {
throw new ProsopoEnvError('DEVELOPER.MISSING_ENV_VARIABLE', {
context: { missingEnvVars: ['PROSOPO_ZELCORE_PRIVATE_KEY'] },
})
}
return wifToPrivateKey(secret)
}

export const getPublicKey = () => {
const secret = process.env['PROSOPO_ZELCORE_PUBLIC_KEY']
if (!secret) {
throw new ProsopoEnvError('DEVELOPER.MISSING_ENV_VARIABLE', {
context: { missingEnvVars: ['PROSOPO_ZELCORE_PUBLIC_KEY'] },
})
}

return secret
}
43 changes: 43 additions & 0 deletions dev/flux/src/commands/terminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as z from 'zod'
import { ArgumentsCamelCase, Argv } from 'yargs'
import { LogLevel, Logger, getLogger } from '@prosopo/common'
import { getPrivateKey, getPublicKey } from './process.env.js'
import { main } from '../lib/terminal.js'

const fluxAuthArgs = z.object({
app: z.string(),
ip: z.string().optional(),
})

export default (cmdArgs?: { logger?: Logger }) => {
const logger = cmdArgs?.logger || getLogger(LogLevel.enum.info, 'flux.cli.auth')

return {
command: 'terminal',
describe: 'Start a terminal to a Flux Node',
builder: (yargs: Argv) =>
yargs
.option('app', {
type: 'string' as const,
demandOption: false,
desc: 'Name of the app to authenticate with',
} as const)
.option('ip', {
type: 'string' as const,
demandOption: false,
desc: 'IP address of Flux machine to authenticate with',
} as const),
handler: async (argv: ArgumentsCamelCase) => {
try {
const privateKey = getPrivateKey()
const publicKey = getPublicKey()
const parsedArgs = fluxAuthArgs.parse(argv)
const result = await main(publicKey, privateKey, parsedArgs.app, parsedArgs.ip)
logger.info(result)
} catch (err) {
logger.error(err)
}
},
middlewares: [],
}
}
21 changes: 21 additions & 0 deletions dev/flux/src/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ProsopoApiError } from '@prosopo/common'
export async function streamToJson(stream: ReadableStream<Uint8Array>): Promise<Record<any, any>> {
return await new Response(stream).json()
}

export const errorHandler = async <T>(response: Response) => {
if (!response.ok) {
throw new ProsopoApiError('API.BAD_REQUEST', { context: { error: `HTTP error! status: ${response.status}` } })
}
if (response.body && !response.bodyUsed) {
const data = await streamToJson(response.body)

if (data.status === 'error') {
throw new ProsopoApiError('API.BAD_REQUEST', {
context: { error: `HTTP error! status: ${data.data.message} ` },
})
}
return data as T
}
return {} as T
}
Loading
Loading