From e5cce547f61aef57866b28923ac98e4eb8846a34 Mon Sep 17 00:00:00 2001 From: Javier Viola Date: Thu, 16 Dec 2021 13:12:14 -0300 Subject: [PATCH] Refactor add authorities/parachains to genesis (#21) * wip * more work in add auths * support add parachain in genesis or register through rpc call --- src/chain-spec.ts | 175 ++++++++++ src/configManager.ts | 15 +- src/networkNode.ts | 8 +- src/orchestrator.ts | 361 ++++----------------- src/paras.ts | 117 +++++++ src/providers/k8s/chain-spec.ts | 75 +++++ src/providers/k8s/dynResourceDefinition.ts | 1 - src/providers/k8s/index.ts | 3 +- src/providers/k8s/kubeClient.ts | 84 +++-- src/types.d.ts | 30 ++ src/utils.ts | 22 +- 11 files changed, 559 insertions(+), 332 deletions(-) create mode 100644 src/chain-spec.ts create mode 100644 src/paras.ts create mode 100644 src/providers/k8s/chain-spec.ts diff --git a/src/chain-spec.ts b/src/chain-spec.ts new file mode 100644 index 000000000..b8cc6c67c --- /dev/null +++ b/src/chain-spec.ts @@ -0,0 +1,175 @@ +import { Keyring } from "@polkadot/api"; +import { cryptoWaitReady } from "@polkadot/util-crypto"; +import { encodeAddress } from "@polkadot/util-crypto"; +import { ChainSpec } from "./types"; +import { readDataFile } from "./utils"; +const fs = require("fs"); + +function nameCase(string: string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +// Get authority keys from within chainSpec data +function getAuthorityKeys(chainSpec: ChainSpec) { + // Check runtime_genesis_config key for rococo compatibility. + const runtimeConfig = + chainSpec.genesis.runtime?.runtime_genesis_config || + chainSpec.genesis.runtime; + if (runtimeConfig && runtimeConfig.session) { + return runtimeConfig.session.keys; + } + + // For retro-compatibility with substrate pre Polkadot 0.9.5 + if (runtimeConfig && runtimeConfig.palletSession) { + return runtimeConfig.palletSession.keys; + } + + console.error(" โš  session not found in runtimeConfig"); + process.exit(1); +} + +// Remove all existing keys from `session.keys` +export function clearAuthorities(spec: string) { + let rawdata = fs.readFileSync(spec); + let chainSpec; + try { + chainSpec = JSON.parse(rawdata); + } catch { + console.error(" โš  failed to parse the chain spec"); + process.exit(1); + } + + let keys = getAuthorityKeys(chainSpec); + keys.length = 0; + + let data = JSON.stringify(chainSpec, null, 2); + fs.writeFileSync(spec, data); + console.log(`\n๐Ÿงน Starting with a fresh authority set...`); +} + +// Add additional authorities to chain spec in `session.keys` +export async function addAuthority(spec: string, name: string) { + await cryptoWaitReady(); + + const sr_keyring = new Keyring({ type: "sr25519" }); + const sr_account = sr_keyring.createFromUri(`//${nameCase(name)}`); + const sr_stash = sr_keyring.createFromUri(`//${nameCase(name)}//stash`); + + const ed_keyring = new Keyring({ type: "ed25519" }); + const ed_account = ed_keyring.createFromUri(`//${nameCase(name)}`); + + const ec_keyring = new Keyring({ type: "ecdsa" }); + const ec_account = ec_keyring.createFromUri(`//${nameCase(name)}`); + + let key = [ + sr_stash.address, + sr_stash.address, + { + grandpa: ed_account.address, + babe: sr_account.address, + im_online: sr_account.address, + parachain_validator: sr_account.address, + authority_discovery: sr_account.address, + para_validator: sr_account.address, + para_assignment: sr_account.address, + beefy: encodeAddress(ec_account.publicKey), + }, + ]; + + let rawdata = fs.readFileSync(spec); + let chainSpec = JSON.parse(rawdata); + + let keys = getAuthorityKeys(chainSpec); + keys.push(key); + + let data = JSON.stringify(chainSpec, null, 2); + fs.writeFileSync(spec, data); + console.log(` ๐Ÿ‘ค Added Genesis Authority ${name} - ${sr_stash.address}`); +} + +// Add parachains to the chain spec at genesis. +export async function addParachainToGenesis( + spec_path: string, + para_id: string, + head: string, + wasm: string, + parachain: boolean = true +) { + let rawdata = fs.readFileSync(spec_path); + let chainSpec = JSON.parse(rawdata); + + // Check runtime_genesis_config key for rococo compatibility. + const runtimeConfig = + chainSpec.genesis.runtime?.runtime_genesis_config || + chainSpec.genesis.runtime; + let paras = undefined; + if (runtimeConfig.paras) { + paras = runtimeConfig.paras.paras; + } + // For retro-compatibility with substrate pre Polkadot 0.9.5 + else if (runtimeConfig.parachainsParas) { + paras = runtimeConfig.parachainsParas.paras; + } + if (paras) { + let new_para = [ + parseInt(para_id), + [ + readDataFile(head), //fs.readFileSync(head).toString(), + readDataFile(wasm), //fs.readFileSync(wasm).toString(), + parachain + ], + ]; + + paras.push(new_para); + + let data = JSON.stringify(chainSpec, null, 2); + fs.writeFileSync(spec_path, data); + console.log(` โœ“ Added Genesis Parachain ${para_id}`); + } else { + console.error(" โš  paras not found in runtimeConfig"); + process.exit(1); + } +} + +// Update the runtime config in the genesis. +// It will try to match keys which exist within the configuration and update the value. +export async function changeGenesisConfig(spec: string, updates: any) { + let rawdata = fs.readFileSync(spec); + let chainSpec = JSON.parse(rawdata); + + console.log(`\nโš™ Updating Relay Chain Genesis Configuration`); + + if (chainSpec.genesis) { + let config = chainSpec.genesis; + findAndReplaceConfig(updates, config); + + let data = JSON.stringify(chainSpec, null, 2); + fs.writeFileSync(spec, data); + } +} + +// Look at the key + values from `obj1` and try to replace them in `obj2`. +function findAndReplaceConfig(obj1: any, obj2: any) { + // Look at keys of obj1 + Object.keys(obj1).forEach((key) => { + // See if obj2 also has this key + if (obj2.hasOwnProperty(key)) { + // If it goes deeper, recurse... + if ( + obj1[key] !== null && + obj1[key] !== undefined && + obj1[key].constructor === Object + ) { + findAndReplaceConfig(obj1[key], obj2[key]); + } else { + obj2[key] = obj1[key]; + console.log( + ` โœ“ Updated Genesis Configuration [ ${key}: ${obj2[key]} ]` + ); + } + } else { + console.error(` โš  Bad Genesis Configuration [ ${key}: ${obj1[key]} ]`); + } + }); +} + diff --git a/src/configManager.ts b/src/configManager.ts index 3214e70b9..10b27316c 100644 --- a/src/configManager.ts +++ b/src/configManager.ts @@ -23,8 +23,11 @@ export const DEFAULT_CHAIN = "rococo-local"; export const DEFAULT_BOOTNODE_PEER_ID = "12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp"; export const DEFAULT_BOOTNODE_DOMAIN = "bootnode"; -export const DEFAULT_CHAIN_SPEC_COMMAND = - "polkadot build-spec --chain {{chainName}} --disable-default-bootnode > /cfg/{{chainName}}-plain.json && polkadot build-spec --chain {{chainName}} --disable-default-bootnode --raw > /cfg/{{chainName}}.json"; +export const DEFAULT_CHAIN_SPEC_PATH = "/cfg/{{chainName}}.json"; +export const DEFAULT_CHAIN_SPEC_RAW_PATH = "/cfg/{{chainName}}-raw.json"; +//export const DEFAULT_CHAIN_SPEC_COMMAND = +// "polkadot build-spec --chain {{chainName}} --disable-default-bootnode > /cfg/{{chainName}}-plain.json && polkadot build-spec --chain {{chainName}} --disable-default-bootnode --raw > /cfg/{{chainName}}.json"; +export const DEFAULT_CHAIN_SPEC_COMMAND = "polkadot build-spec --chain {{chainName}} --disable-default-bootnode"; export const DEFAULT_GENESIS_GENERATE_COMMAND = "/usr/local/bin/adder-collator export-genesis-state > /cfg/genesis-state"; export const DEFAULT_WASM_GENERATE_COMMAND = @@ -44,6 +47,13 @@ export const BAKCCHANNEL_URI_PATTERN = "http://127.0.0.1:{{PORT}}"; export const BAKCCHANNEL_PORT = 3000; export const BAKCCHANNEL_POD_NAME = "backchannel"; +export const ZOMBIE_WRAPPER = "zombie-wrapper.sh"; +// get the path of the zombie wrapper +export const zombieWrapperPath = resolve( + __dirname, + `../scripts/${ZOMBIE_WRAPPER}` +); + export function generateNetworkSpec(config: LaunchConfig): ComputedNetwork { let networkSpec: any = { relaychain: { @@ -191,6 +201,7 @@ export function generateNetworkSpec(config: LaunchConfig): ComputedNetwork { let parachainSetup: Parachain = { id: parachain.id, + addToGenesis: parachain.addToGenesis === undefined ? true : parachain.addToGenesis, // add by default collator: { name: getUniqueName("collator"), command: parachain.collator.command || DEFAULT_COLLATOR_COMMAND, diff --git a/src/networkNode.ts b/src/networkNode.ts index 0b0513843..16e08070e 100644 --- a/src/networkNode.ts +++ b/src/networkNode.ts @@ -41,10 +41,12 @@ export class NetworkNode implements NetworkNodeInterface { async connectApi() { const provider = new WsProvider(this.wsUri); + debug(`Connecting api for ${this.name}...`); this.apiInstance = await ApiPromise.create({ provider, types: this.userDefinedTypes, }); + debug(`Connected to ${this.name}`); } async restart(timeout: number | null = null) { @@ -55,7 +57,7 @@ export class NetworkNode implements NetworkNodeInterface { : `echo restart > /tmp/zombiepipe`; args.push(cmd); - await client._kubectl(args, undefined, true); + await client.kubectl(args, undefined, true); } async pause() { @@ -68,7 +70,7 @@ export class NetworkNode implements NetworkNodeInterface { "-c", "echo pause > /tmp/zombiepipe", ]; - await client._kubectl(args, undefined, true); + await client.kubectl(args, undefined, true); } async resume() { @@ -81,7 +83,7 @@ export class NetworkNode implements NetworkNodeInterface { "-c", "echo pause > /tmp/zombiepipe", ]; - await client._kubectl(args, undefined, true); + await client.kubectl(args, undefined, true); } async isUp(timeout = DEFAULT_INDIVIDUAL_TEST_TIMEOUT): Promise { diff --git a/src/orchestrator.ts b/src/orchestrator.ts index f832cb3e9..dbc805edd 100644 --- a/src/orchestrator.ts +++ b/src/orchestrator.ts @@ -14,29 +14,37 @@ import { TRANSFER_CONTAINER_NAME, WS_URI_PATTERN, METRICS_URI_PATTERN, + DEFAULT_CHAIN_SPEC_PATH, + DEFAULT_CHAIN_SPEC_RAW_PATH, + DEFAULT_CHAIN_SPEC_COMMAND, + zombieWrapperPath, + ZOMBIE_WRAPPER, } from "./configManager"; import { Network } from "./network"; import { NetworkNode } from "./networkNode"; import { startPortForwarding } from "./portForwarder"; -import { ApiPromise, WsProvider } from "@polkadot/api"; +//import { ApiPromise, WsProvider } from "@polkadot/api"; +import { clearAuthorities, addAuthority, changeGenesisConfig, addParachainToGenesis } from "./chain-spec"; import { generateNamespace, sleep, filterConsole, writeLocalJsonFile, loadTypeDef, + createTempNodeDef, } from "./utils"; import tmp from "tmp-promise"; import fs from "fs"; import { resolve } from "path"; +import { generateParachainFiles } from "./paras"; +import { setupChainSpec } from "./providers/k8s"; +import { getChainSpecRaw } from "./providers/k8s/chain-spec"; const debug = require("debug")("zombie"); // For now the only provider is k8s const { genBootnodeDef, genPodDef, initClient } = Providers.Kubernetes; -const ZOMBIE_WRAPPER = "zombie-wrapper.sh"; - // Hide some warning messages that are coming from Polkadot JS API. // TODO: Make configurable. filterConsole([ @@ -48,7 +56,6 @@ export async function start( credentials: string, networkConfig: LaunchConfig, monitor: boolean = false, - withMetrics: boolean = false ) { let network: Network | undefined; let cronInterval = undefined; @@ -57,7 +64,7 @@ export async function start( const networkSpec: ComputedNetwork = generateNetworkSpec(networkConfig); debug(JSON.stringify(networkSpec, null, 4)); - // global timeout + // global timeout to spin the network setTimeout(() => { if (network && !network.launched) { throw new Error( @@ -66,13 +73,9 @@ export async function start( } }, networkSpec.settings.timeout * 1000); - // Create namespace + // set namespace const namespace = `zombie-${generateNamespace()}`; - // Chain name and file name - const chainSpecFileName = `${networkSpec.relaychain.chain}.json`; - const chainName = networkSpec.relaychain.chain; - // get user defined types const userDefinedTypes: any = loadTypeDef(networkSpec.types); @@ -100,163 +103,64 @@ export async function start( // Create MAGIC file to stop temp/init containers fs.openSync(localMagicFilepath, "w"); - const zombieWrapperPath = resolve( - __dirname, - `../scripts/${ZOMBIE_WRAPPER}` - ); + // Define chain name and file name to use. + const chainSpecFileName = `${networkSpec.relaychain.chain}.json`; + const chainName = networkSpec.relaychain.chain; + const chainSpecFullPath = `${tmpDir.path}/${chainSpecFileName}`; // create namespace - const namespaceDef = { - apiVersion: "v1", - kind: "Namespace", - metadata: { - name: namespace, - }, - }; - - writeLocalJsonFile(tmpDir.path, "namespace", namespaceDef); - await client.createResource(namespaceDef); + await client.createNamespace(); // Create bootnode and backchannel services debug(`Creating bootnode and backchannel services`); await client.crateStaticResource("bootnode-service.yaml"); await client.crateStaticResource("backchannel-service.yaml"); await client.crateStaticResource("backchannel-pod.yaml"); - - // create basic infra metrics if needed - // if (withMetrics) await client.staticSetup(); await client.createPodMonitor("pod-monitor.yaml", chainName); - - // setup cleaner if (!monitor) cronInterval = await client.setupCleaner(); + // create or copy chain spec + await setupChainSpec(namespace, networkSpec, chainName, chainSpecFullPath); + // check if we have the chain spec file + if (!fs.existsSync(chainSpecFullPath)) + throw new Error("Can't find chain spec file!"); - if (networkSpec.relaychain.chainSpecCommand) { - let node: Node = { - name: getUniqueName("temp"), - validator: false, - image: networkSpec.relaychain.defaultImage, - fullCommand: - networkSpec.relaychain.chainSpecCommand + - " && " + - WAIT_UNTIL_SCRIPT_SUFIX, // leave the pod runnig until we finish transfer files - chain: networkSpec.relaychain.chain, - bootnodes: [], - args: [], - env: [], - telemetryUrl: "", - overrides: [], - }; - - const podDef = await genPodDef(namespace, node); - debug( - `launching ${podDef.metadata.name} pod with image ${podDef.spec.containers[0].image}` - ); - debug(`command: ${podDef.spec.containers[0].command.join(" ")}`); - writeLocalJsonFile(tmpDir.path, "temp", podDef); - - await client.createResource(podDef, true, false); - await client.wait_transfer_container(podDef.metadata.name); - - for (const override of networkSpec.relaychain.overrides) { - await client.copyFileToPod( - podDef.metadata.name, - override.local_path, - override.remote_path, - TRANSFER_CONTAINER_NAME - ); - } - - await client.copyFileToPod( - podDef.metadata.name, - localMagicFilepath, - FINISH_MAGIC_FILE, - TRANSFER_CONTAINER_NAME - ); - - await client.wait_pod_ready(podDef.metadata.name); - const fileName = `${networkSpec.relaychain.chain}.json`; - debug("copy file from pod"); - - await client.copyFileFromPod( - podDef.metadata.name, - `/cfg/${networkSpec.relaychain.chain}-plain.json`, - `${tmpDir.path}/${networkSpec.relaychain.chain}-plain.json`, - podDef.metadata.name - ); - - await client.copyFileFromPod( - podDef.metadata.name, - `/cfg/${fileName}`, - `${tmpDir.path}/${fileName}`, - podDef.metadata.name - ); + // Chain spec customization logic + clearAuthorities(chainSpecFullPath); + for (const node of networkSpec.relaychain.nodes) { + await addAuthority(chainSpecFullPath, node.name); + } - await client.copyFileToPod( - podDef.metadata.name, - localMagicFilepath, - FINISH_MAGIC_FILE - ); - sleep(300 * 1000); - } else { - if (networkSpec.relaychain.chainSpecPath) { - // copy file to temp to use - fs.copyFileSync( - networkSpec.relaychain.chainSpecPath, - `${tmpDir.path}/${chainSpecFileName}` - ); - } + for(const parachain of networkSpec.parachains) { + const parachainFilesPath = await generateParachainFiles(namespace, tmpDir.path, chainName,parachain); + const stateLocalFilePath = `${parachainFilesPath}/${GENESIS_STATE_FILENAME}`; + const wasmLocalFilePath = `${parachainFilesPath}/${GENESIS_WASM_FILENAME}`; + if(parachain.addToGenesis) await addParachainToGenesis(chainSpecFullPath, parachain.id.toString(), stateLocalFilePath, wasmLocalFilePath); } - // check if we have the chain spec file - if (!fs.existsSync(`${tmpDir.path}/${chainSpecFileName}`)) - throw new Error("Can't find chain spec file!"); + // generate the raw chain spec + await getChainSpecRaw(namespace, networkSpec.relaychain.defaultImage, chainName, chainSpecFullPath); + + // files to include in each node + const filesToCopyToNodes = [ + { + localFilePath: `${tmpDir.path}/${chainSpecFileName}`, + remoteFilePath: `/cfg/${chainSpecFileName}` + }, { + localFilePath: zombieWrapperPath, + remoteFilePath: `/cfg/${ZOMBIE_WRAPPER}` + } + ]; // bootnode // TODO: allow to customize the bootnode const bootnodeSpec = await generateBootnodeSpec(networkSpec); const bootnodeDef = await genBootnodeDef(namespace, bootnodeSpec); - // debug(JSON.stringify(bootnodeDef, null, 4 )); - debug( - `launching ${bootnodeDef.metadata.name} pod with image ${bootnodeDef.spec.containers[0].image}` - ); - debug(`command: ${bootnodeDef.spec.containers[0].command.join(" ")}`); - writeLocalJsonFile(tmpDir.path, "bootnode", bootnodeDef); - await client.createResource(bootnodeDef, true, false); - await client.wait_transfer_container(bootnodeDef.metadata.name); - - await client.copyFileToPod( - bootnodeDef.metadata.name, - `${tmpDir.path}/${chainSpecFileName}`, - `/cfg/${chainSpecFileName}`, - TRANSFER_CONTAINER_NAME - ); - - await client.copyFileToPod( - bootnodeDef.metadata.name, - zombieWrapperPath, - `/cfg/${ZOMBIE_WRAPPER}`, - TRANSFER_CONTAINER_NAME - ); - - await client.copyFileToPod( - bootnodeDef.metadata.name, - localMagicFilepath, - FINISH_MAGIC_FILE, - TRANSFER_CONTAINER_NAME - ); - - await client.wait_pod_ready(bootnodeDef.metadata.name); - - await client.copyFileToPod( - bootnodeDef.metadata.name, - localMagicFilepath, - FINISH_MAGIC_FILE - ); + await client.spawnFromDef(bootnodeDef, filesToCopyToNodes); // make sure the bootnode is up and available over DNS await sleep(5000); @@ -267,11 +171,6 @@ export async function start( bootnodeIdentifier, client ); - // const wsUri = `ws://127.0.0.1:${fwdPort}`; - // const prometheusUri = `http://127.0.0.1:${prometheusPort}/metrics`; - // const provider = new WsProvider(wsUri); - // debug(`creating api connection for ${bootnodeDef.metadata.name}`); - // const api = await ApiPromise.create({ provider, types: userDefinedTypes }); const bootnodeNode: NetworkNode = new NetworkNode( bootnodeDef.metadata.name, @@ -282,6 +181,7 @@ export async function start( network.addNode(bootnodeNode); const bootnodeIP = await client.getBootnodeIP(); + // Create nodes for (const node of networkSpec.relaychain.nodes) { // TODO: k8s don't see pods by name so in here we inject the bootnode ip @@ -291,48 +191,7 @@ export async function start( // create the node and attach to the network object debug(`creating node: ${node.name}`); const podDef = await genPodDef(namespace, node); - - debug( - `launching ${podDef.metadata.name} pod with image ${podDef.spec.containers[0].image}` - ); - debug(`command: ${podDef.spec.containers[0].command.join(" ")}`); - - writeLocalJsonFile(tmpDir.path, node.name, podDef); - await client.createResource(podDef, true, false); - await client.wait_transfer_container(podDef.metadata.name); - - await client.copyFileToPod( - podDef.metadata.name, - `${tmpDir.path}/${chainSpecFileName}`, - `/cfg/${chainSpecFileName}`, - TRANSFER_CONTAINER_NAME - ); - - await client.copyFileToPod( - podDef.metadata.name, - zombieWrapperPath, - `/cfg/${ZOMBIE_WRAPPER}`, - TRANSFER_CONTAINER_NAME - ); - - for (const override of node.overrides) { - await client.copyFileToPod( - podDef.metadata.name, - override.local_path, - override.remote_path, - TRANSFER_CONTAINER_NAME - ); - } - - await client.copyFileToPod( - podDef.metadata.name, - localMagicFilepath, - FINISH_MAGIC_FILE, - TRANSFER_CONTAINER_NAME - ); - - await client.wait_pod_ready(podDef.metadata.name); - debug(`${podDef.metadata.name} pod is ready!`); + await client.spawnFromDef(podDef, filesToCopyToNodes); const nodeIdentifier = `${podDef.kind}/${podDef.metadata.name}`; const fwdPort = await startPortForwarding(9944, nodeIdentifier, client); @@ -361,90 +220,15 @@ export async function start( } for (const parachain of networkSpec.parachains) { - let wasmLocalFilePath, stateLocalFilePath; - // check if we need to create files - if (parachain.genesisStateGenerator || parachain.genesisWasmGenerator) { - let commands = []; - if (parachain.genesisStateGenerator) - commands.push(parachain.genesisStateGenerator); - if (parachain.genesisWasmGenerator) - commands.push(parachain.genesisWasmGenerator); - commands.push(WAIT_UNTIL_SCRIPT_SUFIX); - - let node: Node = { - name: getUniqueName("temp-collator"), - validator: false, - image: parachain.collator.image || DEFAULT_COLLATOR_IMAGE, - fullCommand: commands.join(" && "), - chain: networkSpec.relaychain.chain, - bootnodes: [], - args: [], - env: [], - telemetryUrl: "", - overrides: [], - }; - const podDef = await genPodDef(namespace, node); - - debug( - `launching ${podDef.metadata.name} pod with image ${podDef.spec.containers[0].image}` - ); - debug(`command: ${podDef.spec.containers[0].command.join(" ")}`); - - await client.createResource(podDef, true, false); - await client.wait_transfer_container(podDef.metadata.name); - - await client.copyFileToPod( - podDef.metadata.name, - localMagicFilepath, - FINISH_MAGIC_FILE, - TRANSFER_CONTAINER_NAME - ); - - await client.wait_pod_ready(podDef.metadata.name); - - if (parachain.genesisStateGenerator) { - stateLocalFilePath = `${tmpDir.path}/${GENESIS_STATE_FILENAME}`; - await client.copyFileFromPod( - podDef.metadata.name, - `/cfg/${GENESIS_STATE_FILENAME}`, - stateLocalFilePath - ); - } - - if (parachain.genesisWasmGenerator) { - wasmLocalFilePath = `${tmpDir.path}/${GENESIS_WASM_FILENAME}`; - await client.copyFileFromPod( - podDef.metadata.name, - `/cfg/${GENESIS_WASM_FILENAME}`, - wasmLocalFilePath - ); - } - - // put file to terminate pod - await client.copyFileToPod( - podDef.metadata.name, - localMagicFilepath, - FINISH_MAGIC_FILE + if(!parachain.addToGenesis) { + // register parachain on a running network + await network.registerParachain( + parachain.id, + `${tmpDir.path}/${parachain.id}/${GENESIS_WASM_FILENAME}`, + `${tmpDir.path}/${parachain.id}/${GENESIS_STATE_FILENAME}` ); } - if (!stateLocalFilePath) stateLocalFilePath = parachain.genesisStatePath; - if (!wasmLocalFilePath) wasmLocalFilePath = parachain.genesisWasmPath; - - // CHECK - if (!stateLocalFilePath || !wasmLocalFilePath) - throw new Error("Invalid state or wasm files"); - - // register parachain - await network.registerParachain( - parachain.id, - wasmLocalFilePath, - stateLocalFilePath - ); - - // let finalCommandWithArgs = - // parachain.collator.commandWithArgs || parachain.collator.command; - // create collator let collator: Node = { name: getUniqueName(parachain.collator.name), @@ -461,38 +245,7 @@ export async function start( overrides: [], }; const podDef = await genPodDef(namespace, collator); - - debug( - `launching ${podDef.metadata.name} pod with image ${podDef.spec.containers[0].image}` - ); - debug(`command: ${podDef.spec.containers[0].command.join(" ")}`); - - writeLocalJsonFile(tmpDir.path, parachain.collator.name, podDef); - await client.createResource(podDef, true, false); - await client.wait_transfer_container(podDef.metadata.name); - - await client.copyFileToPod( - podDef.metadata.name, - `${tmpDir.path}/${chainSpecFileName}`, - `/cfg/${chainSpecFileName}`, - TRANSFER_CONTAINER_NAME - ); - - await client.copyFileToPod( - podDef.metadata.name, - zombieWrapperPath, - `/cfg/${ZOMBIE_WRAPPER}`, - TRANSFER_CONTAINER_NAME - ); - - await client.copyFileToPod( - podDef.metadata.name, - localMagicFilepath, - FINISH_MAGIC_FILE, - TRANSFER_CONTAINER_NAME - ); - - await client.wait_pod_ready(podDef.metadata.name); + await client.spawnFromDef(podDef, filesToCopyToNodes); const networkNode: NetworkNode = new NetworkNode( podDef.metadata.name, @@ -535,4 +288,4 @@ export async function test( await network.stop(); } } -} +} \ No newline at end of file diff --git a/src/paras.ts b/src/paras.ts new file mode 100644 index 000000000..d19c3210e --- /dev/null +++ b/src/paras.ts @@ -0,0 +1,117 @@ +import { debug } from "console"; +import { + DEFAULT_COLLATOR_IMAGE, + FINISH_MAGIC_FILE, + GENESIS_STATE_FILENAME, + GENESIS_WASM_FILENAME, + getUniqueName, + TRANSFER_CONTAINER_NAME, + WAIT_UNTIL_SCRIPT_SUFIX, +} from "./configManager"; +import { genPodDef, getClient } from "./providers/k8s"; +import { Node, Parachain } from "./types"; +import fs from "fs"; + +export async function generateParachainFiles( + namespace: string, + tmpDir: string, + chainName: string, + parachain: Parachain +): Promise { + const parachainFilesPath = `${tmpDir}/${parachain.id}`; + const stateLocalFilePath = `${parachainFilesPath}/${GENESIS_STATE_FILENAME}`; + const wasmLocalFilePath = `${parachainFilesPath}/${GENESIS_WASM_FILENAME}`; + const localMagicFilepath = `${tmpDir}/finished.txt`; + const client = getClient(); + + fs.mkdirSync(parachainFilesPath); + + // check if we need to create files + if (parachain.genesisStateGenerator || parachain.genesisWasmGenerator) { + let commands = []; + if (parachain.genesisStateGenerator) + commands.push(parachain.genesisStateGenerator); + if (parachain.genesisWasmGenerator) + commands.push(parachain.genesisWasmGenerator); + commands.push(WAIT_UNTIL_SCRIPT_SUFIX); + + let node: Node = { + name: getUniqueName("temp-collator"), + validator: false, + image: parachain.collator.image || DEFAULT_COLLATOR_IMAGE, + fullCommand: commands.join(" && "), + chain: chainName, + bootnodes: [], + args: [], + env: [], + telemetryUrl: "", + overrides: [], + }; + const podDef = await genPodDef(namespace, node); + + debug( + `launching ${podDef.metadata.name} pod with image ${podDef.spec.containers[0].image}` + ); + debug(`command: ${podDef.spec.containers[0].command.join(" ")}`); + + await client.createResource(podDef, true, false); + await client.wait_transfer_container(podDef.metadata.name); + + await client.copyFileToPod( + podDef.metadata.name, + localMagicFilepath, + FINISH_MAGIC_FILE, + TRANSFER_CONTAINER_NAME + ); + + await client.wait_pod_ready(podDef.metadata.name); + + if (parachain.genesisStateGenerator) { + await client.copyFileFromPod( + podDef.metadata.name, + `/cfg/${GENESIS_STATE_FILENAME}`, + stateLocalFilePath + ); + } + + if (parachain.genesisWasmGenerator) { + await client.copyFileFromPod( + podDef.metadata.name, + `/cfg/${GENESIS_WASM_FILENAME}`, + wasmLocalFilePath + ); + } + + // put file to terminate pod + await client.copyFileToPod( + podDef.metadata.name, + localMagicFilepath, + FINISH_MAGIC_FILE + ); + } + + if (parachain.genesisStatePath) { + // copy file to temp to use + fs.copyFileSync( + parachain.genesisStatePath, + stateLocalFilePath ); + } + // else throw new Error("Invalid state file path"); + + if (parachain.genesisWasmPath) { + // copy file to temp to use + fs.copyFileSync( + parachain.genesisWasmPath, + wasmLocalFilePath ); + } + //else throw new Error("Invalid wasm file path"); + + // register parachain + // await network.registerParachain( + // parachain.id, + // wasmLocalFilePath, + // stateLocalFilePath + // ); + + return parachainFilesPath; +} diff --git a/src/providers/k8s/chain-spec.ts b/src/providers/k8s/chain-spec.ts new file mode 100644 index 000000000..7bb28bd12 --- /dev/null +++ b/src/providers/k8s/chain-spec.ts @@ -0,0 +1,75 @@ +import { debug } from "console"; +import { genPodDef, getClient } from "."; +import { DEFAULT_CHAIN_SPEC_PATH, TRANSFER_CONTAINER_NAME, FINISH_MAGIC_FILE, DEFAULT_CHAIN_SPEC_COMMAND, DEFAULT_CHAIN_SPEC_RAW_PATH } from "../../configManager"; +import { ComputedNetwork } from "../../types"; +import { createTempNodeDef, sleep, writeLocalJsonFile } from "../../utils"; + +import fs from "fs"; + +export async function setupChainSpec(namespace: string, networkSpec: ComputedNetwork, chainName: string, chainFullPath: string): Promise { + // We have two options to get the chain-spec file, neither should use the `raw` file/argument + // 1: User provide the chainSpecCommand (without the --raw option) + // 2: User provide the file (we DON'T expect the raw file) + const client = getClient(); + if (networkSpec.relaychain.chainSpecCommand) { + const { defaultImage, chainSpecCommand } = networkSpec.relaychain; + // set output of command + const fullCommand = `${chainSpecCommand} > ${DEFAULT_CHAIN_SPEC_PATH.replace(/{{chainName}}/ig, chainName)}`; + const node = createTempNodeDef("temp", defaultImage, chainName, fullCommand); + + const podDef = await genPodDef(namespace, node); + const podName = podDef.metadata.name; + await client.spawnFromDef(podDef); + + debug("copy file from pod"); + await client.copyFileFromPod( + podName, + `/cfg/${chainName}.json`, + chainFullPath, + podName + ); + + await client.putLocalMagicFile(podName, podName); + } else { + if (networkSpec.relaychain.chainSpecPath) { + // copy file to temp to use + fs.copyFileSync( + networkSpec.relaychain.chainSpecPath, + chainFullPath + ); + } + } +} + +export async function getChainSpecRaw(namespace: string, image: string, chainName: string, chainFullPath: string): Promise { + // backup plain file + const plainPath = chainFullPath.replace(".json", "-plain.json"); + fs.copyFileSync(chainFullPath, plainPath); + + const remoteChainSpecFullPath = DEFAULT_CHAIN_SPEC_PATH.replace(/{{chainName}}/, chainName); + const remoteChainSpecRawFullPath = DEFAULT_CHAIN_SPEC_RAW_PATH.replace(/{{chainName}}/, chainName); + const chainSpecCommandRaw = DEFAULT_CHAIN_SPEC_COMMAND.replace(/{{chainName}}/ig, remoteChainSpecFullPath); + const fullCommand = `${chainSpecCommandRaw} --raw > ${remoteChainSpecRawFullPath}`; + const node = createTempNodeDef("temp", image, chainName, fullCommand ); + + const podDef = await genPodDef(namespace, node); + const podName = podDef.metadata.name; + + const client = getClient(); + await client.spawnFromDef(podDef,[ + { + localFilePath: chainFullPath, + remoteFilePath: remoteChainSpecFullPath + } + ]); + + debug("copy raw chain spec file from pod"); + await client.copyFileFromPod( + podName, + remoteChainSpecRawFullPath, + chainFullPath, + podName + ); + + await client.putLocalMagicFile(podName, podName); +} \ No newline at end of file diff --git a/src/providers/k8s/dynResourceDefinition.ts b/src/providers/k8s/dynResourceDefinition.ts index 527f8d090..79fb52a30 100644 --- a/src/providers/k8s/dynResourceDefinition.ts +++ b/src/providers/k8s/dynResourceDefinition.ts @@ -4,7 +4,6 @@ import { TRANSFER_CONTAINER_NAME, DEFAULT_COMMAND, } from "../../configManager"; -import { KubeClient } from "./kubeClient"; import { Node } from "../../types"; export async function genBootnodeDef( diff --git a/src/providers/k8s/index.ts b/src/providers/k8s/index.ts index 23f001bb0..989737d9c 100644 --- a/src/providers/k8s/index.ts +++ b/src/providers/k8s/index.ts @@ -1,4 +1,5 @@ import { KubeClient, getClient, initClient } from "./kubeClient"; import { genBootnodeDef, genPodDef } from "./dynResourceDefinition"; +import { setupChainSpec } from "./chain-spec"; -export { KubeClient, genBootnodeDef, genPodDef, getClient, initClient }; +export { KubeClient, genBootnodeDef, genPodDef, getClient, initClient, setupChainSpec }; diff --git a/src/providers/k8s/kubeClient.ts b/src/providers/k8s/kubeClient.ts index dde1ed0cf..fe52e7187 100644 --- a/src/providers/k8s/kubeClient.ts +++ b/src/providers/k8s/kubeClient.ts @@ -1,11 +1,10 @@ import execa from "execa"; -import { stat } from "fs"; import { resolve } from "path"; -import { resourceLimits } from "worker_threads"; -import { TRANSFER_CONTAINER_NAME } from "../../configManager"; -import { addMinutes } from "../../utils"; +import { FINISH_MAGIC_FILE, TRANSFER_CONTAINER_NAME } from "../../configManager"; +import { addMinutes, writeLocalJsonFile } from "../../utils"; const fs = require("fs").promises; import { spawn } from "child_process"; +import { fileMap } from "../../types"; const debug = require("debug")("zombie::kube::client"); export interface KubectlResponse { @@ -39,6 +38,7 @@ export class KubeClient { timeout: number; command: string = "kubectl"; tmpDir: string; + localMagicFilepath: string; constructor(configPath: string, namespace: string, tmpDir: string) { this.configPath = configPath; @@ -46,24 +46,68 @@ export class KubeClient { this.debug = true; this.timeout = 30; // secs this.tmpDir = tmpDir; + this.localMagicFilepath = `${tmpDir}/finished.txt`; } async validateAccess(): Promise { try { - const result = await this._kubectl(["cluster-info"], undefined, false); + const result = await this.kubectl(["cluster-info"], undefined, false); return result.exitCode === 0; } catch (e) { return false; } } + async createNamespace(): Promise { + const namespaceDef = { + apiVersion: "v1", + kind: "Namespace", + metadata: { + name: this.namespace, + }, + }; + + writeLocalJsonFile(this.tmpDir, "namespace", namespaceDef); + await this.createResource(namespaceDef); + } + + async spawnFromDef(podDef: any, filesToCopy: fileMap[] = [] , filesToGet: fileMap[] = []): Promise { + const name = podDef.metadata.name; + writeLocalJsonFile(this.tmpDir, name , podDef); + debug( + `launching ${podDef.metadata.name} pod with image ${podDef.spec.containers[0].image}` + ); + debug(`command: ${podDef.spec.containers[0].command.join(" ")}`); + await this.createResource(podDef, true, false); + await this.wait_transfer_container(name); + + for(const fileMap of filesToCopy) { + const {localFilePath, remoteFilePath} = fileMap; + await client.copyFileToPod(name, localFilePath, remoteFilePath, TRANSFER_CONTAINER_NAME) + } + + await this.putLocalMagicFile(name); + await this.wait_pod_ready(name); + debug(`${name} pod is ready!`); + } + + async putLocalMagicFile(name: string, container?: string) { + const target = container? container : TRANSFER_CONTAINER_NAME; + await client.copyFileToPod( + name, + this.localMagicFilepath, + FINISH_MAGIC_FILE, + target + ); + } + // accept a json def async createResource( resourseDef: any, scoped: boolean = false, waitReady: boolean = false ): Promise { - const pod = await this._kubectl( + await this.kubectl( ["apply", "-f", "-"], JSON.stringify(resourseDef), scoped @@ -78,7 +122,7 @@ export class KubeClient { let t = this.timeout; const args = ["get", kind, name, "-o", "jsonpath={.status}"]; do { - const result = await this._kubectl(args, undefined, true); + const result = await this.kubectl(args, undefined, true); //debug( result.stdout ); const status = JSON.parse(result.stdout); if (["Running", "Succeeded"].includes(status.phase)) return; @@ -101,7 +145,7 @@ export class KubeClient { let t = this.timeout; const args = ["get", "pod", podName, "-o", "jsonpath={.status.phase}"]; do { - const result = await this._kubectl(args, undefined, true); + const result = await this.kubectl(args, undefined, true); //debug( result.stdout ); if (["Running", "Succeeded"].includes(result.stdout)) return; @@ -116,7 +160,7 @@ export class KubeClient { let t = this.timeout; const args = ["get", "pod", podName, "-o", "jsonpath={.status}"]; do { - const result = await this._kubectl(args, undefined, true); + const result = await this.kubectl(args, undefined, true); const status = JSON.parse(result.stdout); // check if we are waiting init container @@ -141,7 +185,7 @@ export class KubeClient { const resourceDef = fileContent .toString("utf-8") .replace(new RegExp("{{namespace}}", "g"), this.namespace); - await this._kubectl(["apply", "-f", "-"], resourceDef); + await this.kubectl(["apply", "-f", "-"], resourceDef); } async createPodMonitor(filename: string, chain: string): Promise { @@ -151,7 +195,7 @@ export class KubeClient { .toString("utf-8") .replace(/{{namespace}}/ig, this.namespace) .replace(/{{chain}}/ig, chain); - await this._kubectl(["apply", "-f", "-"], resourceDef, true); + await this.kubectl(["apply", "-f", "-"], resourceDef, true); } async updateResource( @@ -172,7 +216,7 @@ export class KubeClient { ); } - await this._kubectl(["apply", "-f", "-"], resourceDef); + await this.kubectl(["apply", "-f", "-"], resourceDef); } async copyFileToPod( @@ -183,7 +227,7 @@ export class KubeClient { ) { const args = ["cp", localFilePath, `${identifier}:${podFilePath}`]; if (container) args.push("-c", container); - const result = await this._kubectl(args, undefined, true); + const result = await this.kubectl(args, undefined, true); debug("copyFileToPod", args); } @@ -195,12 +239,12 @@ export class KubeClient { ) { const args = ["cp", `${identifier}:${podFilePath}`, localFilePath]; if (container) args.push("-c", container); - const result = await this._kubectl(args, undefined, true); + const result = await this.kubectl(args, undefined, true); debug(result); } async runningOnMinikube(): Promise { - const result = await this._kubectl([ + const result = await this.kubectl([ "get", "sc", "-o", @@ -210,7 +254,7 @@ export class KubeClient { } async destroyNamespace() { - await this._kubectl( + await this.kubectl( ["delete", "namespace", this.namespace], undefined, false @@ -219,7 +263,7 @@ export class KubeClient { async getBootnodeIP(): Promise { const args = ["get", "pod", "bootnode", "-o", "jsonpath={.status.podIP}"]; - const result = await this._kubectl(args, undefined, true); + const result = await this.kubectl(args, undefined, true); return result.stdout; } @@ -301,7 +345,7 @@ export class KubeClient { "-o", "jsonpath={.status.phase}", ]; - const result = await this._kubectl(args, undefined, false); + const result = await this.kubectl(args, undefined, false); if (result.exitCode !== 0 || result.stdout !== "Active") return false; return true; } @@ -353,12 +397,12 @@ export class KubeClient { async dumpLogs(path: string, podName: string) { const dstFileName = `${path}/logs/${podName}.log`; const args = ["logs", podName, "--namespace", this.namespace]; - const result = await this._kubectl(args, undefined, false); + const result = await this.kubectl(args, undefined, false); await fs.writeFile(dstFileName, result.stdout); } // run kubectl - async _kubectl( + async kubectl( args: string[], resourceDef?: string, scoped: boolean = true diff --git a/src/types.d.ts b/src/types.d.ts index f7f4b4de1..cdc8a0fd2 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -67,6 +67,7 @@ export interface Collator { export interface Parachain { id: number; + addToGenesis: boolean; genesisWasmPath?: string; genesisWasmGenerator?: string; genesisStatePath?: string; @@ -89,6 +90,28 @@ export interface envVars { value: string; } +export interface ChainSpec { + name: string; + id: string; + chainType: string; + bootNodes: string[]; + telemetryEndpoints: null; + protocolId: string; + properties: null; + forkBlocks: null; + badBlocks: null; + consensusEngine: null; + lightSyncState: null; + genesis: { + runtime: any; // this can change depending on the versions + raw: { + top: { + [key: string]: string; + }; + }; + }; +} + // Launch Config ( user provided config ) export interface LaunchConfig { settings?: Settings; @@ -152,6 +175,7 @@ export interface RelayChainConfig { export interface ParachainConfig { id: number; + addToGenesis?: boolean; balance?: number; genesis_wasm_path?: string; genesis_wasm_generator?: string; @@ -166,3 +190,9 @@ export interface ParachainConfig { args?: string[]; }; } + +// name: path +export interface fileMap { + localFilePath: string, + remoteFilePath: string +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 798f69e04..ce70dbce1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,9 @@ import { randomBytes } from "crypto"; import fs from "fs"; import { format } from "util"; -import { LaunchConfig } from "./types"; +import { LaunchConfig, Node } from "./types"; import toml from "toml"; +import { getUniqueName, WAIT_UNTIL_SCRIPT_SUFIX } from "./configManager"; export async function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -134,3 +135,22 @@ export function loadTypeDef(types: string | object): object { return types; } } + +export function createTempNodeDef(name: string, image: string, chain: string, fullCommand: string) { + let node: Node = { + name: getUniqueName("temp"), + image, + fullCommand: fullCommand + " && " + WAIT_UNTIL_SCRIPT_SUFIX, // leave the pod runnig until we finish transfer files + chain, + validator: false, + bootnodes: [], + args: [], + env: [], + telemetryUrl: "", + overrides: [], + }; + + return node; + + +}