diff --git a/docs/quorum/COMMANDS.md b/docs/quorum/COMMANDS.md index ea77323d..c02a58f1 100644 --- a/docs/quorum/COMMANDS.md +++ b/docs/quorum/COMMANDS.md @@ -142,3 +142,34 @@ Description: 匯入現有的 Quorum Network | --help | boolean | Show help | | | | --version | boolean | Show version number | | | | -i, --interactive | boolean | 是否使用 Cathay BDK 互動式問答 | | | + +## Cluster + +### `bdk quorum cluster apply` + +Description: 產生 Quorum Cluster 所需的相關設定檔案並建立網路 + +| Options | Type | Description | Required | Default | +| --------------------- | :-----: | ------------------------------ | :------: | ------- | +| --help | boolean | Show help | | | +| --version | boolean | Show version number | | | + +### `bdk quorum cluster delete` + +Description: 刪除現有的 Quorum Cluster 網路 + +| Options | Type | Description | Required | Default | +| --------------------- | :-----: | ------------------------------ | :------: | ------- | +| --help | boolean | Show help | | | +| --version | boolean | Show version number | | | +| -i, --interactive | boolean | 是否使用 Cathay BDK 互動式問答 | | | + +### `bdk quorum cluster generate` + +Description: 產生 Quorum Cluster 所需的相關設定檔案 + +| Options | Type | Description | Required | Default | +| --------------------- | :-----: | ------------------------------ | :------: | ------- | +| --help | boolean | Show help | | | +| --version | boolean | Show version number | | | +| -i, --interactive | boolean | 是否使用 Cathay BDK 互動式問答 | | | diff --git a/docs/quorum/EXAMPLE.md b/docs/quorum/EXAMPLE.md index a0f85853..71a376a8 100644 --- a/docs/quorum/EXAMPLE.md +++ b/docs/quorum/EXAMPLE.md @@ -7,6 +7,7 @@ - [建立 Blockscout Explorer](#建立-blockscout-explorer) - [加入 Remote 節點](#加入-remote-節點) - [備份還原 Node](#備份還原-node) +- [建立 Cluster](#建立-cluster) ## 確認 BDK 安裝狀態 @@ -138,4 +139,49 @@ bdk quorum backup import -i ```bash # 還原後需透過以下指令,來啟動該備份的節點 bdk quorum network up --all +``` + +## 建立 Cluster + +先確保電腦安裝以下套件 `kubectl`, `helm`, `docker` +```bash +kubectl version +helm version +docker version +``` +該範例以 `minikube` 做為本機建立的範例 + +### Step 1. 建立本地 Cluster + +```bash +minikube start --memory 11384 --cpus 2 +# 確認目前的 cluster 為 minikube +kubectl config current-context +``` + +### Step 2. 建立 K8S 網路 +```bash +bdk quorum cluster apply -i +``` +- `What is your cloud provider?` 選擇 `GCP/local` +- `What is your chain id?` 選擇 81712 +- `How many validator do you want?` 選擇 4 +- `How many member do you want?` 選擇 0 +- `Do you already own a wallet?` false + +這樣你的本地端的 quorum 網路就建立好了,如需連線及可用 `http://localhost:8545` 做連線 +```bash +kubectl port-forward -n quorum svc/goquorum-node-validator-1 8545 +``` + +### Step 3. 刪除 K8S 網路 +```bash +bdk quorum cluster delete +``` +按 'y' 刪除 + +## 產出 helm values 和資料於本地 +如需直接使用 helm repo 來做 helm release 可利用以下 script +```bash +bdk quorum cluster generate -i ``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1e1c45eb..3621147c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4042,9 +4042,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", diff --git a/src/console.ts b/src/console.ts index a7f7a069..75e5ec69 100644 --- a/src/console.ts +++ b/src/console.ts @@ -40,6 +40,14 @@ const npmBuild = spawnSync('npm', ['run', 'build-ts']) console.log(`[+] child process exited with code ${npmBuild.status}`) if (npmBuild.status !== 0) { console.error(`${npmBuild.output[1]}`); console.error('\x1b[31m%s\x1b[0m', npmBuild.stderr); exit(0) } +/** + * copy helm chart file + */ +console.log('[*] exec spawnSync: copy helm chart file') +const cp = spawnSync('cp', ['-r', 'src/quorum/instance/infra/kubernetes/charts', 'dist/quorum/instance/infra/kubernetes']) +console.log(`[+] child process exited with code ${cp.status}`) +if (cp.status !== 0) { console.error('\x1b[31m%s\x1b[0m', cp.stderr); exit(0) } + /** * npm link/ */ diff --git a/src/quorum/command/cluster.ts b/src/quorum/command/cluster.ts new file mode 100644 index 00000000..8758195d --- /dev/null +++ b/src/quorum/command/cluster.ts @@ -0,0 +1,11 @@ +import { Argv } from 'yargs' + +export const command = 'cluster' + +export const desc = '管理 Quorum cluster 的指令' + +export const builder = (yargs: Argv) => { + return yargs.commandDir('cluster').demandCommand() +} + +export const handler = {} diff --git a/src/quorum/command/cluster/apply.ts b/src/quorum/command/cluster/apply.ts new file mode 100644 index 00000000..97870b7a --- /dev/null +++ b/src/quorum/command/cluster/apply.ts @@ -0,0 +1,171 @@ +import { Argv, Arguments } from 'yargs' +import { ethers } from 'ethers' +import config from '../../config' +import Cluster from '../../service/cluster' +import Wallet from '../../../wallet/service/wallet' +import { onCancel } from '../../../util/error' +import { ClusterCreateType } from '../../model/type/kubernetes.type' +import { WalletType } from '../../../wallet/model/type/wallet.type' +import { defaultNetworkConfig } from '../../model/defaultNetworkConfig' +import prompts from 'prompts' +import ora from 'ora' + +export const command = 'apply' + +export const desc = '產生 Quorum Cluster 所需的相關設定檔案並建立網路' + +interface OptType { + interactive: boolean +} + +export const builder = (yargs: Argv) => { + return yargs + .example('bdk quorum cluster apply --interactive', 'Cathay BDK 互動式問答') + .option('interactive', { type: 'boolean', description: '是否使用 Cathay BDK 互動式問答', alias: 'i' }) +} + +export const handler = async (argv: Arguments) => { + const cluster = new Cluster(config) + const wallet = new Wallet() + + const confirm: boolean = await (async () => { + const fileList = cluster.getHelmChartFiles() + if (fileList.length !== 0) { + const confirmDelete = (await prompts({ + type: 'confirm', + name: 'value', + message: '⚠️ Detecting quorum cluster already exists. The following processes will remove all existing files. Continue?', + initial: false, + }, { onCancel })).value + if (confirmDelete) { + const spinner = ora('Quorum Cluster Delete ...').start() + cluster.removeHelmChartFiles() + spinner.succeed('Remove all existing files!') + } + return confirmDelete + } else { + return true + } + })() + + if (confirm) { + // network create + const clusterCreate: ClusterCreateType = await (async () => { + if (argv.interactive) { + const { provider } = await prompts({ + type: 'select', + name: 'provider', + message: 'What is your cloud provider?', + choices: [ + { + title: 'GCP/local', + value: 'local', + }, + { + title: 'AWS', + value: 'aws', + }, + { + title: 'Azure', + value: 'azure', + }, + ], + initial: 0, + }, { onCancel }) + + let region: string | undefined = '' + if (provider === 'aws') { + const { awsRegion } = await prompts({ + type: 'text', + name: 'awsRegion', + message: 'What is your region?', + initial: 'ap-southeast-2', + }, { onCancel }) + region = awsRegion + } + + const { chainId, validatorNumber, memberNumber } = await prompts([ + { + type: 'number', + name: 'chainId', + message: 'What is your chain id?', + min: 0, + initial: 81712, + }, + { + type: 'number', + name: 'validatorNumber', + message: 'How many validator do you want?', + min: 1, + initial: 4, + }, + { + type: 'number', + name: 'memberNumber', + message: 'How many member do you want?', + min: 0, + initial: 0, + }, + ], { onCancel }) + + const { walletOwner } = await prompts({ + type: 'select', + name: 'walletOwner', + message: 'Do you already own a wallet?', + choices: [ + { + title: 'true', + value: true, + }, + { + title: 'false', + value: false, + }, + ], + initial: 1, + }) + + let walletAddress: string + + if (walletOwner) { + const { address } = await prompts({ + type: 'text', + name: 'address', + message: 'What is your wallet address?', + validate: walletAddress => ethers.utils.isAddress(walletAddress) ? true : 'Address not valid.', + }, { onCancel }) + + walletAddress = address + } else { + const { address, privateKey } = wallet.createWalletAddress(WalletType.ETHEREUM) + walletAddress = address + ora().stopAndPersist({ + text: `Your ${WalletType.ETHEREUM} wallet address: 0x${walletAddress}`, + symbol: '🔑', + }) + ora().stopAndPersist({ + text: `Wallet private key: ${privateKey}`, + symbol: '🔑', + }) + } + + const alloc = [{ + account: walletAddress, + amount: '1000000000000000000000000000', + }] + + const isBootNode = false + const bootNodeList: boolean[] = Array(validatorNumber + memberNumber).fill(false) + + return { provider, region, chainId, validatorNumber, memberNumber, alloc, isBootNode, bootNodeList } + } else { + const { address, privateKey } = wallet.createWalletAddress(WalletType.ETHEREUM) + const config = defaultNetworkConfig(address, privateKey) + return { ...config, provider: 'local' } + } + })() + const spinner = ora('Quorum Cluster Apply ...').start() + await cluster.apply(clusterCreate, spinner) + spinner.succeed('Quorum Cluster Apply Successfully!') + } +} diff --git a/src/quorum/command/cluster/delete.ts b/src/quorum/command/cluster/delete.ts new file mode 100644 index 00000000..ed3a448c --- /dev/null +++ b/src/quorum/command/cluster/delete.ts @@ -0,0 +1,49 @@ +import { Argv } from 'yargs' +import config from '../../config' +import Cluster from '../../service/cluster' +import { onCancel } from '../../../util/error' +import prompts from 'prompts' +import ora from 'ora' + +export const command = 'delete' + +export const desc = '刪除現有的 Quorum Cluster 網路' + +interface OptType { + interactive: boolean +} + +export const builder = (yargs: Argv) => { + return yargs + .example('bdk quorum cluster delete', 'Cathay BDK 互動式問答') +} + +export const handler = async () => { + const cluster = new Cluster(config) + + const confirm: boolean = await (async () => { + const fileList = cluster.getHelmChartFiles() + if (fileList.length !== 0) { + const confirmDelete = (await prompts({ + type: 'confirm', + name: 'value', + message: '⚠️ Detecting quorum cluster already exists. The following processes will remove all existing files. Continue?', + initial: false, + }, { onCancel })).value + if (confirmDelete) { + const spinner = ora('Quorum Cluster Create ...').start() + cluster.removeHelmChartFiles() + spinner.succeed('Remove all existing files!') + } + return confirmDelete + } else { + return true + } + })() + + if (confirm) { + const spinner = ora('Deployments Under Namespace Quorum Delete ...').start() + await cluster.delete() + spinner.succeed('Quorum Cluster Delete Successfully!') + } +} diff --git a/src/quorum/command/cluster/generate.ts b/src/quorum/command/cluster/generate.ts new file mode 100644 index 00000000..e7423873 --- /dev/null +++ b/src/quorum/command/cluster/generate.ts @@ -0,0 +1,198 @@ +import { Argv, Arguments } from 'yargs' +import { ethers } from 'ethers' +import config from '../../config' +import Cluster from '../../service/cluster' +import Wallet from '../../../wallet/service/wallet' +import { ClusterCreateType, ClusterGenerateType } from '../../model/type/kubernetes.type' +import { WalletType } from '../../../wallet/model/type/wallet.type' +import { defaultNetworkConfig } from '../../model/defaultNetworkConfig' +import { onCancel } from '../../../util/error' +import prompts from 'prompts' +import ora from 'ora' + +export const command = 'generate' + +export const desc = '產生 Quorum Cluster 所需的相關設定檔案' + +interface OptType { + interactive: boolean +} + +export const builder = (yargs: Argv) => { + return yargs + .example('bdk quorum cluster generate --interactive', 'Cathay BDK 互動式問答') + .option('interactive', { type: 'boolean', description: '是否使用 Cathay BDK 互動式問答', alias: 'i' }) +} + +export const handler = async (argv: Arguments) => { + const cluster = new Cluster(config) + const wallet = new Wallet() + + const confirm: boolean = await (async () => { + const fileList = cluster.getHelmChartFiles() + if (fileList.length !== 0) { + const confirmDelete = (await prompts({ + type: 'confirm', + name: 'value', + message: '⚠️ Detecting quorum cluster already exists. The following processes will remove all existing files. Continue?', + initial: false, + }, { onCancel })).value + if (confirmDelete) { + const spinner = ora('Quorum Cluster Delete ...').start() + cluster.removeHelmChartFiles() + spinner.succeed('Remove all existing files!') + } + return confirmDelete + } else { + return true + } + })() + + if (confirm) { + const clusterGenerate: ClusterGenerateType = await (async () => { + if (argv.interactive) { + return (await prompts([ + { + type: 'select', + name: 'chartPackageModeEnabled', + message: 'What is the connect mode you want?', + choices: [ + { + title: 'package mode (package without helm and k8s)', + value: false, + }, + { + title: 'template mode (template with helm and k8s)', + value: true, + }, + ], + initial: 0, + }, + ], { onCancel })) as ClusterGenerateType + } else { + return { + chartPackageModeEnabled: false, + } + } + })() + // network create + const networkCreate: ClusterCreateType = await (async () => { + if (argv.interactive) { + const { provider } = await prompts({ + type: 'select', + name: 'provider', + message: 'What is your cloud provider?', + choices: [ + { + title: 'GCP/local', + value: 'local', + }, + { + title: 'AWS', + value: 'aws', + }, + { + title: 'Azure', + value: 'azure', + }, + ], + initial: 0, + }, { onCancel }) + + let region: string | undefined = '' + if (provider === 'aws') { + const { awsRegion } = await prompts({ + type: 'text', + name: 'awsRegion', + message: 'What is your region?', + initial: 'ap-southeast-2', + }, { onCancel }) + region = awsRegion + } + + const { chainId, validatorNumber, memberNumber } = await prompts([ + { + type: 'number', + name: 'chainId', + message: 'What is your chain id?', + min: 0, + initial: 81712, + }, + { + type: 'number', + name: 'validatorNumber', + message: 'How many validator do you want?', + min: 1, + initial: 4, + }, + { + type: 'number', + name: 'memberNumber', + message: 'How many member do you want?', + min: 0, + initial: 0, + }, + ], { onCancel }) + + const { walletOwner } = await prompts({ + type: 'select', + name: 'walletOwner', + message: 'Do you already own a wallet?', + choices: [ + { + title: 'true', + value: true, + }, + { + title: 'false', + value: false, + }, + ], + initial: 1, + }) + + let walletAddress: string + + if (walletOwner) { + const { address } = await prompts({ + type: 'text', + name: 'address', + message: 'What is your wallet address?', + validate: walletAddress => ethers.utils.isAddress(walletAddress) ? true : 'Address not valid.', + }, { onCancel }) + + walletAddress = address + } else { + const { address, privateKey } = wallet.createWalletAddress(WalletType.ETHEREUM) + walletAddress = address + ora().stopAndPersist({ + text: `Your ${WalletType.ETHEREUM} wallet address: 0x${walletAddress}`, + symbol: '🔑', + }) + ora().stopAndPersist({ + text: `Wallet private key: ${privateKey}`, + symbol: '🔑', + }) + } + + const alloc = [{ + account: walletAddress, + amount: '1000000000000000000000000000', + }] + + const isBootNode = false + const bootNodeList: boolean[] = Array(validatorNumber + memberNumber).fill(false) + + return { provider, region, chainId, validatorNumber, memberNumber, alloc, isBootNode, bootNodeList } + } else { + const { address, privateKey } = wallet.createWalletAddress(WalletType.ETHEREUM) + const config = defaultNetworkConfig(address, privateKey) + return { ...config, provider: 'local' } + } + })() + + const spinner = ora('Quorum Cluster Generate ...').start() + await cluster.generate(clusterGenerate, networkCreate) + spinner.succeed('Quorum Cluster Generate Successfully!') + } +} diff --git a/src/quorum/instance/Instance.abstract.ts b/src/quorum/instance/Instance.abstract.ts index 4b81ca65..b5817f06 100644 --- a/src/quorum/instance/Instance.abstract.ts +++ b/src/quorum/instance/Instance.abstract.ts @@ -1,6 +1,6 @@ import { Config } from '../config' import BdkFile from './bdkFile' -import { InfraRunner, InfraRunnerResultType } from './infra/InfraRunner.interface' +import { InfraRunner, InfraRunnerResultType, KubernetesInfraRunner } from './infra/InfraRunner.interface' export abstract class AbstractInstance { /** @ignore */ @@ -13,12 +13,19 @@ export abstract class AbstractInstance { protected hostPath: string /** @ignore */ protected dockerPath: string + /** @ignore */ + protected kubernetesInfra: KubernetesInfraRunner|undefined - constructor (config: Config, infra: InfraRunner) { + constructor ( + config: Config, + infra: InfraRunner, + kubernetesInfra?: KubernetesInfraRunner, + ) { this.config = config this.infra = infra this.bdkFile = new BdkFile(config) this.hostPath = config.infraConfig.dockerHostPath this.dockerPath = config.infraConfig.dockerPath + this.kubernetesInfra = kubernetesInfra } } diff --git a/src/quorum/instance/bdkFile.ts b/src/quorum/instance/bdkFile.ts index 1978c52b..030f59b0 100644 --- a/src/quorum/instance/bdkFile.ts +++ b/src/quorum/instance/bdkFile.ts @@ -1,9 +1,11 @@ import ExplorerDockerComposeYaml from '../model/yaml/docker-compose/explorerDockerComposeYaml' import fs from 'fs-extra' +import path from 'path' import { Config } from '../config' import { GenesisJsonType, NetworkInfoItem } from '../model/type/network.type' import ValidatorDockerComposeYaml from '../model/yaml/docker-compose/validatorDockerComposeYaml' import MemberDockerComposeYaml from '../model/yaml/docker-compose/memberDockerCompose' +import { GenesisConfigYaml, ValidatorConfigYaml, MemberConfigYaml } from '../model/yaml/helm-chart' import { PathError } from '../../util/error' export enum InstanceTypeEnum { @@ -15,13 +17,16 @@ export enum InstanceTypeEnum { export default class BdkFile { private config: Config private bdkPath: string + private helmPath: string private backupPath: string private envPath: string private orgPath: string + private thisPath = path.resolve(__dirname) constructor (config: Config, networkName: string = config.networkName) { this.config = config this.bdkPath = `${config.infraConfig.bdkPath}/${networkName}` + this.helmPath = `${this.bdkPath}/helm` this.backupPath = `${config.infraConfig.bdkPath}/backup` this.envPath = `${config.infraConfig.bdkPath}/.env` this.orgPath = '' @@ -267,6 +272,84 @@ export default class BdkFile { fs.writeFileSync(this.getMemberDockerComposeYamlPath(), memberDockerComposeYaml.getYamlString()) } + // helm chart files + public checkHelmChartPath () { + if (!fs.existsSync(this.helmPath)) { + fs.copySync(`${this.thisPath}/infra/kubernetes/charts`, this.helmPath, { recursive: true }) + } + } + + public createYaml (name: string, yaml: string) { + fs.mkdirSync(`${this.helmPath}/kubernetes`, { recursive: true }) + fs.writeFileSync(`${this.helmPath}/kubernetes/${name}.yaml`, yaml) + } + + public getGoQuorumGenesisChartPath (): string { + this.checkHelmChartPath() + return `${this.helmPath}/goquorum-genesis` + } + + public getGoQuorumNodeChartPath (): string { + this.checkHelmChartPath() + return `${this.helmPath}/goquorum-node` + } + + public createChartValueFolder () { + fs.mkdirSync(`${this.helmPath}/values`, { recursive: true }) + } + + public createGoQuorumValues () { + this.checkHelmChartPath() + fs.writeFileSync(`${this.helmPath}/goquorum-values.yaml`, '') + } + + public copyGoQuorumHelmChart () { + this.checkHelmChartPath() + fs.copySync(this.helmPath, './', { recursive: true }) + } + + public createChartTar (tag: string, date: string) { + return fs.createWriteStream(`./${tag}-${date}.tar.gz`) + } + + public getValidatorChartPath (i: number): string { + this.createChartValueFolder() + return `${this.helmPath}/values/validator${i}-values.yaml` + } + + public getMemberChartPath (i: number): string { + this.createChartValueFolder() + return `${this.helmPath}/values/member${i}-values.yaml` + } + + public createGenesisChartValues (genesisYaml: GenesisConfigYaml) { + this.createChartValueFolder() + fs.writeFileSync(`${this.helmPath}/values/genesis-values.yaml`, genesisYaml.getYamlString()) + } + + public createValidatorChartValues (validatorYaml: ValidatorConfigYaml, i: number) { + this.createChartValueFolder() + fs.writeFileSync(`${this.helmPath}/values/validator${i}-values.yaml`, validatorYaml.getYamlString()) + } + + public createMemberChartValues (memberYaml: MemberConfigYaml, i: number) { + fs.writeFileSync(`${this.helmPath}/values/member${i}-values.yaml`, memberYaml.getYamlString()) + } + + public getGenesisChartPath () { + return `${this.helmPath}/values/genesis-values.yaml` + } + + public removeHelmChart () { + fs.rmSync(`${this.helmPath}`, { recursive: true, force: true }) + } + + public getHelmChartValuesFiles () { + this.checkHelmChartPath() + fs.mkdirSync(`${this.helmPath}/values`, { recursive: true }) + return fs.readdirSync(`${this.helmPath}/values`) + } + public checkPathExist (path: string) { if (!fs.existsSync(path)) { throw new PathError(`${path} no exist`) diff --git a/src/quorum/instance/infra/InfraRunner.interface.ts b/src/quorum/instance/infra/InfraRunner.interface.ts index 6f437744..57463a84 100644 --- a/src/quorum/instance/infra/InfraRunner.interface.ts +++ b/src/quorum/instance/infra/InfraRunner.interface.ts @@ -1,5 +1,5 @@ import { DockerRunCommandType } from '../../model/type/docker.type' - +import { K8SRunCommandType, ClusterDeleteType } from '../../model/type/kubernetes.type' /** * Infra return type when direct use docker */ @@ -28,6 +28,15 @@ export interface InfraRunner { restart(dockerComposeFile: string, service: string[]): Promise } +// Kubernetes Methods +export interface KubernetesInfraRunner { + createDeploymentAndService(payload: K8SRunCommandType): Promise + createTemplate(payload: K8SRunCommandType): Promise + wait(job: string, namespace: string): Promise + deleteDeploymentAndService(payload: ClusterDeleteType): Promise + listAllRelease(namespace: string): Promise +} + // Strategy export class InfraStrategy { public static createDockerRunner (infraRunner: InfraRunner) { @@ -37,4 +46,8 @@ export class InfraStrategy { public static createRunner (infraRunner: InfraRunner) { return infraRunner } + + public static createKubernetesRunner (infraRunner: KubernetesInfraRunner) { + return infraRunner + } } diff --git a/src/quorum/instance/infra/agent/.gitkeep b/src/quorum/instance/infra/agent/.gitkeep deleted file mode 100644 index a4de60b0..00000000 --- a/src/quorum/instance/infra/agent/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -TODO Implement diff --git a/src/quorum/instance/infra/kubernetes/.gitkeep b/src/quorum/instance/infra/kubernetes/.gitkeep deleted file mode 100644 index a4de60b0..00000000 --- a/src/quorum/instance/infra/kubernetes/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -TODO Implement diff --git a/src/quorum/instance/infra/kubernetes/charts/bdk-network/Chart.yaml b/src/quorum/instance/infra/kubernetes/charts/bdk-network/Chart.yaml new file mode 100644 index 00000000..5710e92f --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/bdk-network/Chart.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +version: 0.1.0 +appVersion: latest +name: bdk-network +description: Quorum network settings with bdk setting in Kubernetes +keywords: + - ethereum + - hyperledger + - enterprise + - blockchain +home: https://consensys.net/quorum/ +sources: + - https://github.com/consensys/quorum diff --git a/src/quorum/instance/infra/kubernetes/charts/bdk-network/templates/node-service-loadbalance.yaml b/src/quorum/instance/infra/kubernetes/charts/bdk-network/templates/node-service-loadbalance.yaml new file mode 100644 index 00000000..0548d87b --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/bdk-network/templates/node-service-loadbalance.yaml @@ -0,0 +1,31 @@ +{{- if (.Values.bdk.loadBalancer.enabled)}} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-loadbalancer + labels: + app.kubernetes.io/component: service + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/release: {{ .Release.Name }} + app.kubernetes.io/managed-by: helm + app: bdk-network-loadbalancer + namespace: {{ .Release.Namespace }} +spec: + type: LoadBalancer + selector: + app: bdk-network-loadbalancer + ports: + - name: json-rpc + port: {{ .Values.network.port.rpc }} + targetPort: json-rpc + protocol: TCP + - name: ws + port: {{ .Values.network.port.ws }} + targetPort: ws + protocol: TCP + - name: graphql + port: {{ .Values.network.port.graphql }} + targetPort: graphql + protocol: TCP +{{- end }} \ No newline at end of file diff --git a/src/quorum/instance/infra/kubernetes/charts/bdk-network/values.yaml b/src/quorum/instance/infra/kubernetes/charts/bdk-network/values.yaml new file mode 100644 index 00000000..362d54c5 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/bdk-network/values.yaml @@ -0,0 +1,10 @@ +--- +bdk: + loadBalancer: + enabled: true + +network: + port: + rpc: 8545 + ws: 8546 + graphql: 8547 \ No newline at end of file diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/.helmignore b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/.helmignore new file mode 100644 index 00000000..014fa775 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +generated_config/ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/Chart.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/Chart.yaml new file mode 100644 index 00000000..1e8d8d74 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +version: 0.1.0 +appVersion: latest +name: goquorum-genesis +description: Quorum Genesis generator with Helm chart in Kubernetes +keywords: + - ethereum + - quorum + - enterprise + - blockchain + - pegasys + - consensys +home: https://consensys.net/quorum/ +sources: + - https://consensys.net/quorum/ +maintainers: + - name: Joshua Fernandes + email: joshua.fernandes@consensys.net + diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/_helpers.tpl b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/_helpers.tpl new file mode 100644 index 00000000..33ef129c --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/_helpers.tpl @@ -0,0 +1,28 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "goquorum-genesis.name" -}} +{{- default .Chart.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "goquorum-genesis.fullname" -}} +{{- $name := default .Chart.Name -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "goquorum-genesis.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/genesis-job-cleanup.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/genesis-job-cleanup.yaml new file mode 100644 index 00000000..03f5fe57 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/genesis-job-cleanup.yaml @@ -0,0 +1,64 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "goquorum-genesis.name" . }}-cleanup + labels: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + azure.workload.identity/use: "true" +{{- end }} + app.kubernetes.io/name: goquorum-genesis-job-cleanup + app.kubernetes.io/component: genesis-job-cleanup + app.kubernetes.io/part-of: {{ include "goquorum-genesis.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/managed-by: helm + namespace: {{ .Release.Namespace }} + annotations: + helm.sh/hook-weight: "0" + helm.sh/hook: "pre-delete" + helm.sh/hook-delete-policy: "hook-succeeded" +spec: + backoffLimit: 3 + completions: 1 + template: + metadata: + labels: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + azure.workload.identity/use: "true" +{{- end}} + app.kubernetes.io/name: goquorum-genesis-job-cleanup + app.kubernetes.io/component: genesis-job-cleanup + app.kubernetes.io/part-of: {{ include "goquorum-genesis.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/managed-by: helm + spec: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + serviceAccountName: {{ .Values.azure.serviceAccountName }} +{{- else if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + serviceAccountName: {{ .Values.aws.serviceAccountName }} +{{- else }} + serviceAccountName: {{ include "goquorum-genesis.name" . }}-sa +{{- end }} + restartPolicy: "Never" + containers: + - name: delete-genesis + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + securityContext: + runAsUser: 0 + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/bash + - -c + args: + - | + +{{- if .Values.quorumFlags.removeGenesisOnDelete }} + + echo "Deleting genesis configmap in k8s ..." + kubectl delete configmap --namespace {{ .Release.Namespace }} goquorum-genesis + + echo "Deleting node-peers configmap in k8s ..." + kubectl delete configmap --namespace {{ .Release.Namespace }} goquorum-peers + +{{- end}} + diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/genesis-job-init.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/genesis-job-init.yaml new file mode 100644 index 00000000..eb4fa11b --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/genesis-job-init.yaml @@ -0,0 +1,164 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "goquorum-genesis.name" . }}-init + labels: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + azure.workload.identity/use: "true" +{{- end }} + app.kubernetes.io/name: goquorum-genesis-job + app.kubernetes.io/component: genesis-job + app.kubernetes.io/part-of: {{ include "goquorum-genesis.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/managed-by: helm + namespace: {{ .Release.Namespace }} + annotations: + helm.sh/hook-delete-policy: "hook-succeeded" +spec: + backoffLimit: 3 + completions: 1 + template: + metadata: + labels: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + azure.workload.identity/use: "true" +{{- end}} + app.kubernetes.io/name: goquorum-genesis-job + app.kubernetes.io/component: genesis-job + app.kubernetes.io/part-of: {{ include "goquorum-genesis.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/managed-by: helm + spec: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + serviceAccountName: {{ .Values.azure.serviceAccountName }} +{{- else if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + serviceAccountName: {{ .Values.aws.serviceAccountName }} +{{- else }} + serviceAccountName: {{ include "goquorum-genesis.name" . }}-sa +{{- end }} + restartPolicy: "Never" + containers: + - name: generate-genesis + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + securityContext: + runAsUser: 0 + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/bash + - -c + args: + - | + echo "Creating config ..." + +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + + function safeWriteSecret { + key=$1 + fpath=$2 + az keyvault secret show --vault-name {{ .Values.azure.keyvaultName }} --name $key > /dev/null 2>&1 + if [ $? -ne 0 ]; then + az keyvault secret set --vault-name {{ .Values.azure.keyvaultName }} --name $key --file $fpath --encoding utf-8 + else + # if the key exists pull it from keyvault so that when you update the enodes configmap, you have the right value + az keyvault secret show --vault-name {{ .Values.azure.keyvaultName }} --name $key | jq -r '.value' > $fpath + fi + } + + az login --identity --debug + az account set --subscription {{ .Values.azure.subscriptionId }} + +{{- else if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + + function safeWriteSecret { + key=$1 + fpath=$2 + aws secretsmanager describe-secret --secret-id $key > /dev/null 2>&1 + if [ $? -ne 0 ]; then + aws secretsmanager create-secret --name $key --description $key --secret-string file://$fpath + else + # if the key exists pull it from keyvault so that when you update the enodes configmap, you have the right value + aws secretsmanager get-secret-value --secret-id $key | jq -r '.SecretString' > $fpath + fi + } + +{{- else }} + + function safeWriteSecret { + key=$1 + fpath=$2 + kubectl create secret generic ${key}-keys --namespace {{ .Release.Namespace }} --from-file=nodekey=${fpath}/nodekey --from-file=nodekey.pub=${fpath}/nodekey.pub --from-file=enode=${fpath}/nodekey.pub --from-file=accountPrivate.key=${fpath}/accountPrivateKey --from-file=accountPassword=${fpath}/accountPassword --from-file=accountKeystore=${fpath}/accountKeystore --from-file=accountAdddress=${fpath}/accountAddress + } + +{{- end }} + + function safeWriteGenesisConfigmap { + FOLDER_PATH=$1 + kubectl get configmap --namespace {{ .Release.Namespace }} goquorum-genesis + if [ $? -ne 0 ]; then + kubectl create configmap --namespace {{ .Release.Namespace }} goquorum-genesis --from-file=genesis.json=$FOLDER_PATH/goQuorum/genesis.json + fi + } + + function safeWriteQuorumPeersConfigmap { + kubectl get configmap --namespace {{ .Release.Namespace }} goquorum-peers + if [ $? -ne 0 ]; then + kubectl create configmap --namespace {{ .Release.Namespace }} goquorum-peers --from-file=static-nodes.json=/generated-config/static-nodes.json + fi + } + + FOLDER_PATH=$(quorum-genesis-tool --consensus {{ .Values.rawGenesisConfig.genesis.config.algorithm.consensus }} {{ if .Values.rawGenesisConfig.blockchain.nodes.generate }} --validators {{ .Values.rawGenesisConfig.blockchain.nodes.count }} {{ else }} --validators 0 {{ end }} --members 0 --bootnodes 0 --chainID {{ .Values.rawGenesisConfig.genesis.config.chainId }} --blockperiod {{ .Values.rawGenesisConfig.genesis.config.algorithm.blockperiodseconds }} --emptyBlockPeriod {{ .Values.rawGenesisConfig.genesis.config.algorithm.emptyBlockPeriod }} --epochLength {{ .Values.rawGenesisConfig.genesis.config.algorithm.epochlength }} --requestTimeout {{ .Values.rawGenesisConfig.genesis.config.algorithm.requesttimeoutseconds }} --difficulty {{ .Values.rawGenesisConfig.genesis.difficulty }} --gasLimit {{ .Values.rawGenesisConfig.genesis.gasLimit }} --coinbase {{ .Values.rawGenesisConfig.genesis.coinbase }} {{ if .Values.rawGenesisConfig.blockchain.accountPassword }} --accountPassword {{ .Values.rawGenesisConfig.blockchain.accountPassword }} {{ end }} {{ if eq .Values.cluster.cloudNativeServices false }} --quickstartDevAccounts {{ .Values.rawGenesisConfig.genesis.includeQuickStartAccounts }} {{ end }} --outputPath /generated-config | tail -1 | sed -e "s/^Artifacts in folder: //") + + echo $FOLDER_PATH + echo "Creating genesis configmap in k8s ..." + safeWriteGenesisConfigmap $FOLDER_PATH + + # create the static-nodes with proper dns names for the quorum nodes + echo "[" > /generated-config/static-nodes.json + + # 0 index so setting this to the num of validators + echo "Creating validator keys ..." + i=1 + for f in $(find $FOLDER_PATH -type d -iname "validator*" -exec basename {} \;); do + echo $f + + if [ -d $FOLDER_PATH/${f} ]; then + + echo "Creating keys for $f ..." + +{{- if and (ne .Values.cluster.provider "local") (.Values.cluster.cloudNativeServices) }} + + echo "Using cloud native services" + safeWriteSecret goquorum-node-validator-${i}-nodekey $FOLDER_PATH/${f}/nodekey + safeWriteSecret goquorum-node-validator-${i}-nodekeypub $FOLDER_PATH/${f}/nodekey.pub + safeWriteSecret goquorum-node-validator-${i}-enode $FOLDER_PATH/${f}/nodekey.pub + safeWriteSecret goquorum-node-validator-${i}-address $FOLDER_PATH/${f}/address + kubectl create configmap --namespace {{ .Release.Namespace }} goquorum-node-validator-${i}-address --from-file=address=$FOLDER_PATH/${f}/address + + safeWriteSecret goquorum-node-validator-${i}-accountPrivateKey $FOLDER_PATH/${f}/accountPrivateKey + safeWriteSecret goquorum-node-validator-${i}-accountPassword $FOLDER_PATH/${f}/accountPassword + safeWriteSecret goquorum-node-validator-${i}-accountKeystore $FOLDER_PATH/${f}/accountKeystore + safeWriteSecret goquorum-node-validator-${i}-accountAddress $FOLDER_PATH/${f}/accountAddress + +{{- else }} + + echo "Using k8s secrets" + safeWriteSecret goquorum-node-validator-${i} "$FOLDER_PATH/${f}" + kubectl create configmap --namespace {{ .Release.Namespace }} goquorum-node-validator-${i}-address --from-file=address=$FOLDER_PATH/${f}/address + +{{- end }} + + # add to the static-nodes + pubkey=$(cat $FOLDER_PATH/${f}/nodekey.pub ) + echo ",\"enode://$pubkey@goquorum-node-validator-$i-0.goquorum-node-validator-$i.{{ .Release.Namespace }}.svc.cluster.local:30303?discport=0\"" >> /generated-config/static-nodes.json + + i=$((i+1)) + fi + done + + echo "]" >> /generated-config/static-nodes.json + # remove the extra comma to make it valid json + sed -i '0,/,/s///' /generated-config/static-nodes.json + safeWriteQuorumPeersConfigmap + + echo "Completed ..." diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/genesis-service-account.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/genesis-service-account.yaml new file mode 100644 index 00000000..86c5bbf0 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/templates/genesis-service-account.yaml @@ -0,0 +1,47 @@ + +{{- if not .Values.cluster.cloudNativeServices }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "goquorum-genesis.name" . }}-sa + namespace: {{ .Release.Namespace }} + +{{- end }} + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "goquorum-genesis.name" . }}-role + namespace: {{ .Release.Namespace }} +rules: + - apiGroups: [""] + resources: ["secrets", "configmaps"] + verbs: ["create", "get", "list", "update", "delete", "patch" ] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch" ] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "goquorum-genesis.name" . }}-rb + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "goquorum-genesis.name" . }}-role +subjects: + - kind: ServiceAccount + namespace: {{ .Release.Namespace }} +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + name: {{ .Values.azure.serviceAccountName }} +{{- else if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + name: {{ .Values.aws.serviceAccountName }} +{{- else }} + name: {{ include "goquorum-genesis.name" . }}-sa +{{- end}} + + diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/values.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/values.yaml new file mode 100644 index 00000000..3b81146f --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-genesis/values.yaml @@ -0,0 +1,51 @@ +--- + +quorumFlags: + removeGenesisOnDelete: true + +cluster: + provider: local # choose from: local | aws | azure + cloudNativeServices: false # set to true to use Cloud Native Services (SecretsManager and IAM for AWS; KeyVault & Managed Identities for Azure) + +aws: + # the aws cli commands uses the name 'quorum-sa' so only change this if you altered the name + serviceAccountName: quorum-sa + # the region you are deploying to + region: ap-southeast-2 + +azure: + serviceAccountName: quorum-sa + # the clientId of the user assigned managed identity created in the template + identityClientId: azure-clientId + keyvaultName: azure-keyvault + # the tenant ID of the key vault + tenantId: azure-tenantId + # the subscription ID to use - this needs to be set explictly when using multi tenancy + subscriptionId: azure-subscriptionId + +# the raw Genesis config +# rawGenesisConfig.blockchain.nodes set the number of validators/signers +rawGenesisConfig: + genesis: + config: + chainId: 1337 + algorithm: + consensus: qbft # choose from: ibft | qbft | raft | clique + blockperiodseconds: 10 + emptyBlockPeriod: 60 + epochlength: 30000 + requesttimeoutseconds: 20 + nonce: '0x0' + gasLimit: '0x47b760' + difficulty: '0x1' + coinbase: '0x0000000000000000000000000000000000000000' + blockchain: + nodes: # refers to validators/signers + generate: true + count: 4 + accountPassword: 'password' + +image: + repository: consensys/quorum-k8s-hooks + tag: qgt-0.2.12 + pullPolicy: IfNotPresent diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/.helmignore b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/.helmignore new file mode 100644 index 00000000..014fa775 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +generated_config/ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/Chart.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/Chart.yaml new file mode 100644 index 00000000..69c3b8b7 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/Chart.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +version: 0.1.0 +appVersion: v22.4.0 +name: goquorum-node +description: Quorum node for a POA network using IBFT for consensys +keywords: + - ethereum + - hyperledger + - enterprise + - blockchain + - pegasys + - consensys +home: https://consensys.net/quorum/ +sources: + - https://github.com/consensys/quorum +maintainers: + - name: Joshua Fernandes + email: joshua.fernandes@consensys.net \ No newline at end of file diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/_helpers.tpl b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/_helpers.tpl new file mode 100644 index 00000000..1330ec4f --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/_helpers.tpl @@ -0,0 +1,30 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "goquorum-node.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "goquorum-node.fullname" -}} +{{- $name := default .Chart.Name -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" $name .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "goquorum-node.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/aws-secret-provider-class.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/aws-secret-provider-class.yaml new file mode 100644 index 00000000..c97d9cf0 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/aws-secret-provider-class.yaml @@ -0,0 +1,57 @@ +{{- if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + +--- +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: {{ include "goquorum-node.fullname" . }}-aws-secret-provider + namespace: {{ .Release.Namespace }} +spec: + provider: aws + parameters: + objects: | + - objectName: {{ include "goquorum-node.fullname" . }}-nodekey + objectAlias: nodekey + objectType: secretsmanager + objectVersion: "" + - objectName: {{ include "goquorum-node.fullname" . }}-nodekeypub + objectAlias: nodekey.pub + objectType: secretsmanager + objectVersion: "" + - objectName: {{ include "goquorum-node.fullname" . }}-enode + objectAlias: enode + objectType: secretsmanager + objectVersion: "" + - objectName: {{ include "goquorum-node.fullname" . }}-accountKeystore + objectAlias: accountKeystore + objectType: secretsmanager + objectVersion: "" + - objectName: {{ include "goquorum-node.fullname" . }}-accountPrivateKey + objectAlias: accountPrivateKey + objectType: secretsmanager + objectVersion: "" + - objectName: {{ include "goquorum-node.fullname" . }}-accountPassword + objectAlias: accountPassword + objectType: secretsmanager + objectVersion: "" + - objectName: {{ include "goquorum-node.fullname" . }}-accountAddress + objectAlias: accountAddress + objectType: secretsmanager + objectVersion: "" + + {{- if .Values.quorumFlags.privacy }} + - objectName: {{ include "goquorum-node.fullname" . }}-tmkey + objectAlias: tm.key + objectType: secretsmanager + objectVersion: "" + - objectName: {{ include "goquorum-node.fullname" . }}-tmkeypub + objectAlias: tm.pub + objectType: secretsmanager + objectVersion: "" + - objectName: {{ include "goquorum-node.fullname" . }}-tmpassword + objectAlias: tm.password + objectType: secretsmanager + objectVersion: "" + {{- end }} + +{{- end }} diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/azure-secret-provider-class.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/azure-secret-provider-class.yaml new file mode 100644 index 00000000..cd127e17 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/azure-secret-provider-class.yaml @@ -0,0 +1,74 @@ +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + +--- +apiVersion: secrets-store.csi.x-k8s.io/v1alpha1 +kind: SecretProviderClass +metadata: + name: {{ include "goquorum-node.fullname" . }}-azure-secret-provider + namespace: {{ .Release.Namespace }} +spec: + provider: azure + parameters: + usePodIdentity: "false" + useVMManagedIdentity: "false" + keyvaultName: "{{ .Values.azure.keyvaultName }}" + tenantId: "{{ .Values.azure.tenantId }}" + cloudName: "AzurePublicCloud" + objects: | + array: + - | + objectName: {{ include "goquorum-node.fullname" . }}-nodekey + objectAlias: nodekey + objectType: secret + objectVersion: "" + - | + objectName: {{ include "goquorum-node.fullname" . }}-nodekeypub + objectAlias: nodekey.pub + objectType: secret + objectVersion: "" + - | + objectName: {{ include "goquorum-node.fullname" . }}-enode + objectAlias: enode + objectType: secret + objectVersion: "" + - | + objectName: {{ include "goquorum-node.fullname" . }}-accountKeystore + objectAlias: accountKeystore + objectType: secret + objectVersion: "" + - | + objectName: {{ include "goquorum-node.fullname" . }}-accountPrivateKey + objectAlias: accountPrivateKey + objectType: secret + objectVersion: "" + - | + objectName: {{ include "goquorum-node.fullname" . }}-accountPassword + objectAlias: accountPassword + objectType: secret + objectVersion: "" + - | + objectName: {{ include "goquorum-node.fullname" . }}-accountAddress + objectAlias: accountAddress + objectType: secret + objectVersion: "" + + {{- if .Values.quorumFlags.privacy }} + - | + objectName: {{ include "goquorum-node.fullname" . }}-tmkey + objectAlias: tm.key + objectType: secret + objectVersion: "" + - | + objectName: {{ include "goquorum-node.fullname" . }}-tmkeypub + objectAlias: tm.pub + objectType: secret + objectVersion: "" + - | + objectName: {{ include "goquorum-node.fullname" . }}-tmpassword + objectAlias: tm.password + objectType: secret + objectVersion: "" + {{- end }} + +{{- end }} + diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-hooks-pre-delete.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-hooks-pre-delete.yaml new file mode 100644 index 00000000..87732c89 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-hooks-pre-delete.yaml @@ -0,0 +1,153 @@ + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "goquorum-node.fullname" . }}-pre-delete-hook + namespace: {{ .Release.Namespace }} + annotations: + helm.sh/hook: pre-delete + helm.sh/hook-weight: "0" + helm.sh/hook-delete-policy: "hook-succeeded" + labels: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + azure.workload.identity/use: "true" +{{- end }} + app.kubernetes.io/name: pre-delete-hook + app.kubernetes.io/component: job + app.kubernetes.io/part-of: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/release: {{ .Release.Name }} + app.kubernetes.io/managed-by: helm +spec: + backoffLimit: 3 + completions: 1 + template: + metadata: + labels: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + azure.workload.identity/use: "true" +{{- end}} + app.kubernetes.io/name: pre-delete-hook + app.kubernetes.io/release: {{ .Release.Name }} + spec: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + serviceAccountName: {{ .Values.azure.serviceAccountName }} +{{- else if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + serviceAccountName: {{ .Values.aws.serviceAccountName }} +{{- else }} + serviceAccountName: {{ include "goquorum-node.fullname" . }}-hooks-sa +{{- end }} + restartPolicy: "OnFailure" + containers: + - name: {{ template "goquorum-node.fullname" . }}-node-pre-delete-hook + image: "{{ .Values.image.hooks.repository }}:{{ .Values.image.hooks.tag }}" + imagePullPolicy: {{ .Values.image.hooks.pullPolicy }} + command: + - /bin/bash + - -c + args: + - | + + echo "{{ template "goquorum-node.fullname" . }} Pre Delete hook ..." + +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + + function deleteSecret { + key=$1 + fpath=$2 + az keyvault secret show --vault-name {{ .Values.azure.keyvaultName }} --name $key > /dev/null 2>&1 + if [ $? -eq 0 ]; then + az keyvault secret delete --vault-name {{ .Values.azure.keyvaultName }} --name $key + fi + } + + az login --identity --debug + az account set --subscription {{ .Values.azure.subscriptionId }} + +{{- else if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + + function deleteSecret { + key=$1 + aws secretsmanager describe-secret --secret-id $key > /dev/null 2>&1 + if [ $? -eq 0 ]; then + aws secretsmanager delete-secret --secret-id $key --recovery-window-in-days 7 + fi + } + +{{- else }} + # provider: local + function deleteSecret { + key=$1 + kubectl delete secret ${key} --namespace {{ .Release.Namespace }} + } + +{{- end }} + + function delete_node_from_tessera_peers_configmap { + kubectl -n {{ .Release.Namespace }} get configmap tessera-peers -o json + # if there is no configmap, do nothing + if [ $? -ne 0 ]; then + echo "No tessera-peers found, nothing to do..." + # delete the one + else + echo "tessera-peers found, deleting {{ template "goquorum-node.fullname" . }}..." + echo $(kubectl -n {{ .Release.Namespace }} get configmap tessera-peers -o jsonpath='{.data.tesseraPeers}' ) > /tmp/tessera-peers.raw + cat /tmp/tessera-peers.raw | jq --arg NEEDLE "{{ template "goquorum-node.fullname" . }}" 'del(.[] | select( .url | contains($NEEDLE) ))' > /tmp/tessera-peers + kubectl -n {{ .Release.Namespace }} create configmap tessera-peers --from-file=tesseraPeers=/tmp/tessera-peers -o yaml --dry-run=client | kubectl replace -f - + fi + } + + function delete_node_from_quorum_peers_configmap { + kubectl -n {{ .Release.Namespace }} get configmap goquorum-peers -o json + # if there is no configmap, do nothing + if [ $? -ne 0 ]; then + echo "No peers found, nothing to do..." + # delete the one + else + echo "goquorum-peers found, deleting {{ template "goquorum-node.fullname" . }}..." + echo $(kubectl -n {{ .Release.Namespace }} get configmap goquorum-peers -o jsonpath='{.data.static-nodes\.json}' ) > /tmp/static-nodes.json.raw + cat /tmp/static-nodes.json.raw | jq --arg NEEDLE "{{ template "goquorum-node.fullname" . }}" 'del(.[] | select( . | contains($NEEDLE) ))' > /tmp/static-nodes.json + kubectl -n {{ .Release.Namespace }} create configmap goquorum-peers --from-file=static-nodes.json=/tmp/static-nodes.json -o yaml --dry-run=client | kubectl replace -f - + + echo "Deleting node address configmap... " + kubectl delete configmap {{ template "goquorum-node.fullname" . }}-address --namespace {{ .Release.Namespace }} + fi + } + + delete_node_from_quorum_peers_configmap + delete_node_from_tessera_peers_configmap + + +{{- if .Values.quorumFlags.removeKeysOnDelete }} + +{{- if and (ne .Values.cluster.provider "local") (.Values.cluster.cloudNativeServices) }} + + deleteSecret {{ template "goquorum-node.fullname" . }}-nodekey + deleteSecret {{ template "goquorum-node.fullname" . }}-nodekeypub + deleteSecret {{ template "goquorum-node.fullname" . }}-enode + deleteSecret {{ template "goquorum-node.fullname" . }}-address + deleteSecret {{ template "goquorum-node.fullname" . }}-accountPrivateKey + deleteSecret {{ template "goquorum-node.fullname" . }}-accountPassword + deleteSecret {{ template "goquorum-node.fullname" . }}-accountKeystore + deleteSecret {{ template "goquorum-node.fullname" . }}-accountAddress +{{- if .Values.quorumFlags.privacy }} + deleteSecret {{ template "goquorum-node.fullname" . }}-tmkey + deleteSecret {{ template "goquorum-node.fullname" . }}-tmkeypub + deleteSecret {{ template "goquorum-node.fullname" . }}-tmpassword +{{- end }} + +{{- else }} + # provider: local + deleteSecret {{ template "goquorum-node.fullname" . }}-keys +{{- if .Values.quorumFlags.privacy }} + deleteSecret {{ template "goquorum-node.fullname" . }}-tessera-keys +{{- end }} + +{{- end }} + +{{- end }} + #.Values.quorumFlags.removeKeysOnDelete + + echo "Completed" + diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-hooks-pre-install.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-hooks-pre-install.yaml new file mode 100644 index 00000000..7bb30c1b --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-hooks-pre-install.yaml @@ -0,0 +1,188 @@ + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "goquorum-node.fullname" . }}-pre-install-hook + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": "hook-succeeded" + labels: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + azure.workload.identity/use: "true" +{{- end }} + app.kubernetes.io/name: pre-install-hook + app.kubernetes.io/component: job + app.kubernetes.io/part-of: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/release: {{ .Release.Name }} + app.kubernetes.io/managed-by: helm +spec: + backoffLimit: 1 + completions: 1 + template: + metadata: + labels: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + azure.workload.identity/use: "true" +{{- end}} + app.kubernetes.io/name: pre-install-hook + app.kubernetes.io/release: {{ .Release.Name }} + spec: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + serviceAccountName: {{ .Values.azure.serviceAccountName }} +{{- else if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + serviceAccountName: {{ .Values.aws.serviceAccountName }} +{{- else }} + serviceAccountName: {{ include "goquorum-node.fullname" . }}-hooks-sa +{{- end }} + restartPolicy: "OnFailure" + containers: + - name: {{ template "goquorum-node.fullname" . }}-quorum-pre-start-hook + image: "{{ .Values.image.hooks.repository }}:{{ .Values.image.hooks.tag }}" + imagePullPolicy: {{ .Values.image.hooks.pullPolicy }} + securityContext: + runAsUser: 0 + command: + - /bin/bash + - -c + args: + - | + + echo "{{ template "goquorum-node.fullname" . }} Pre Install hook ..." + +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + + function safeWriteSecret { + key=$1 + fpath=$2 + az keyvault secret show --vault-name {{ .Values.azure.keyvaultName }} --name $key > /dev/null 2>&1 + if [ $? -ne 0 ]; then + az keyvault secret set --vault-name {{ .Values.azure.keyvaultName }} --name $key --file $fpath --encoding utf-8 + else + # if the key exists pull it from keyvault so that when you update the enodes configmap, you have the right value + az keyvault secret show --vault-name {{ .Values.azure.keyvaultName }} --name $key | jq -r '.value' > $fpath + fi + } + + az login --identity --debug + az account set --subscription {{ .Values.azure.subscriptionId }} + +{{- else if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + + function safeWriteSecret { + key=$1 + fpath=$2 + aws secretsmanager describe-secret --secret-id $key > /dev/null 2>&1 + if [ $? -ne 0 ]; then + aws secretsmanager create-secret --name $key --description $key --secret-string file://$fpath + else + # if the key exists pull it from keyvault so that when you update the enodes configmap, you have the right value + aws secretsmanager get-secret-value --secret-id $key | jq -r '.SecretString' > $fpath + fi + } + +{{- else }} + + # provider: local + function safeWriteSecret { + key=$1 + fpath=$2 + kubectl get secret ${key}-keys --namespace {{ .Release.Namespace }} -o json > /dev/null 2>&1 + if [ $? -ne 0 ]; then + kubectl create secret generic ${key}-keys --namespace {{ .Release.Namespace }} --from-file=nodekey=${fpath}/nodekey --from-file=nodekey.pub=${fpath}/nodekey.pub --from-file=enode=${fpath}/nodekey.pub --from-file=accountPrivate.key=${fpath}/accountPrivateKey --from-file=accountPassword=${fpath}/accountPassword --from-file=accountKeystore=${fpath}/accountKeystore --from-file=accountAdddress=${fpath}/accountAddress + else + # if the key exists pull it from secrets so that when you update the enodes configmap, you have the right value + mkdir -p $fpath + kubectl get secrets --namespace {{ .Release.Namespace }} ${key}-keys -o json | jq '.data.enode' | tr -d '"' | base64 --decode > $fpath/nodekey.pub + fi + } + +{{- end }} + + function update_quorum_peers_configmap { + kubectl get configmap --namespace {{ .Release.Namespace }} goquorum-peers -o json + if [ $? -ne 0 ]; then + echo "[]" > /tmp/static-nodes.json.raw + else + echo $(kubectl get configmap --namespace {{ .Release.Namespace }} goquorum-peers -o jsonpath='{.data.static-nodes\.json}' ) > /tmp/static-nodes.json.raw + fi + + # update the entries + echo "updating goquorum-peers..." + pubkey=$(cat /tmp/enode ) + NEEDLE="enode://$pubkey@{{ template "goquorum-node.fullname" . }}-0.{{ template "goquorum-node.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:30303?discport=0" + cat /tmp/static-nodes.json.raw | jq --arg NEEDLE "$NEEDLE" '. += [ $NEEDLE ] | unique ' > /tmp/static-nodes.json + kubectl create configmap --namespace {{ .Release.Namespace }} goquorum-peers --from-file=static-nodes.json=/tmp/static-nodes.json -o yaml --dry-run=client | kubectl replace -f - + } + + function update_tessera_peers_configmap { + kubectl -n {{ .Release.Namespace }} get configmap tessera-peers -o json + # first time a tx node is deployed and there is no configmap + if [ $? -ne 0 ]; then + echo "No tessera-peers found, creating a new one..." + echo "[{ \"url\": \"http://{{ template "goquorum-node.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:9000\" }]" > /tmp/tessera-peers + kubectl --namespace {{ .Release.Namespace }} create configmap tessera-peers --from-file=tesseraPeers=/tmp/tessera-peers + + # update the entries + else + echo "Tessera-peers found, updating existing..." + echo $(kubectl -n {{ .Release.Namespace }} get configmap tessera-peers -o jsonpath='{.data.tesseraPeers}' ) > /tmp/tessera-peers.raw + NEEDLE="http://{{ template "goquorum-node.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:9000" + cat /tmp/tessera-peers.raw | jq --arg NEEDLE "$NEEDLE" '. += [{"url": $NEEDLE}] | unique ' > /tmp/tessera-peers + kubectl -n {{ .Release.Namespace }} create configmap tessera-peers --from-file=tesseraPeers=/tmp/tessera-peers -o yaml --dry-run=client | kubectl replace -f - + fi + } + + + + echo "{{ template "goquorum-node.fullname" . }} hook ..." + echo "Nodekey generation ..." + FOLDER_PATH=$(quorum-genesis-tool --validators 0 --members 1 --bootnodes 0 {{ if .Values.node.goquorum.account.password }} --accountPassword {{ .Values.node.goquorum.account.password }} {{ end }} --outputPath /generated-config | tail -1 | sed -e "s/^Artifacts in folder: //") + echo "Creating {{ template "goquorum-node.fullname" . }} secrets in k8s ..." + +{{- if .Values.cluster.cloudNativeServices }} + safeWriteSecret {{ template "goquorum-node.fullname" . }}-nodekey $FOLDER_PATH/member0/nodekey + safeWriteSecret {{ template "goquorum-node.fullname" . }}-nodekeypub $FOLDER_PATH/member0/nodekey.pub + safeWriteSecret {{ template "goquorum-node.fullname" . }}-enode $FOLDER_PATH/member0/nodekey.pub + safeWriteSecret {{ template "goquorum-node.fullname" . }}-address $FOLDER_PATH/member0/address + echo "Creating {{ template "goquorum-node.fullname" . }} configmap address in k8s ..." + kubectl create configmap {{ template "goquorum-node.fullname" . }}-address --from-file=address=$FOLDER_PATH/member0/address + safeWriteSecret {{ template "goquorum-node.fullname" . }}-accountPrivateKey $FOLDER_PATH/member0/accountPrivateKey + safeWriteSecret {{ template "goquorum-node.fullname" . }}-accountPassword $FOLDER_PATH/member0/accountPassword + safeWriteSecret {{ template "goquorum-node.fullname" . }}-accountKeystore $FOLDER_PATH/member0/accountKeystore + safeWriteSecret {{ template "goquorum-node.fullname" . }}-accountAddress $FOLDER_PATH/member0/accountAddress +{{- else }} + safeWriteSecret {{ template "goquorum-node.fullname" . }} $FOLDER_PATH/member0 +{{- end }} + + cat $FOLDER_PATH/member0/nodekey.pub > /tmp/enode + update_quorum_peers_configmap + kubectl create configmap {{ template "goquorum-node.fullname" . }}-address --from-file=address=$FOLDER_PATH/member0/address + echo "Quorum Completed" + +{{- if .Values.quorumFlags.privacy }} + + FOLDER_PATH=$(quorum-genesis-tool --validators 0 --members 1 --bootnodes 0 --tesseraEnabled true --tesseraPassword {{ .Values.node.tessera.password }} --outputPath /tmp/tessera | tail -1 | sed -e "s/^Artifacts in folder: //") + if [ ! -f "$FOLDER_PATH/member0/passwordFile.txt" ]; then + echo "" > $FOLDER_PATH/member0/passwordFile.txt + fi + echo "Creating {{ template "goquorum-node.fullname" . }}-tessera-keys secrets in k8s ..." + +{{- if .Values.cluster.cloudNativeServices }} + safeWriteSecret {{ template "goquorum-node.fullname" . }}-tmkey $FOLDER_PATH/member0/tessera.key + safeWriteSecret {{ template "goquorum-node.fullname" . }}-tmkeypub $FOLDER_PATH/member0/tessera.pub + safeWriteSecret {{ template "goquorum-node.fullname" . }}-tmpassword $FOLDER_PATH/member0/passwordFile.txt +{{- else }} + kubectl get secret ${key}-tessera-keys --namespace {{ .Release.Namespace }} -o json > /dev/null 2>&1 + if [ $? -ne 0 ]; then + kubectl create secret generic {{ template "goquorum-node.fullname" . }}-tessera-keys --namespace {{ .Release.Namespace }} --from-file=tm.key=$FOLDER_PATH/member0/tessera.key --from-file=tm.pub=$FOLDER_PATH/member0/tessera.pub --from-file=tm.password=$FOLDER_PATH/member0/passwordFile.txt + fi +{{- end }} + update_tessera_peers_configmap + +{{- end }} + echo "Tessera Completed" + diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-hooks-service-account.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-hooks-service-account.yaml new file mode 100644 index 00000000..7a29e451 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-hooks-service-account.yaml @@ -0,0 +1,54 @@ + +{{- if not .Values.cluster.cloudNativeServices }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "goquorum-node.fullname" . }}-hooks-sa + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook-delete-policy": before-hook-creation + "helm.sh/hook": "pre-install,pre-delete,post-delete" + +{{- end}} + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "goquorum-node.fullname" . }}-hooks-role + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook-delete-policy": before-hook-creation + "helm.sh/hook": "pre-install,pre-delete,post-delete" +rules: + - apiGroups: [""] + resources: ["secrets", "configmaps"] + verbs: ["create", "get", "list", "update", "delete", "patch"] + - apiGroups: [""] + resources: ["services"] + verbs: ["get", "list"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "goquorum-node.fullname" . }}-hooks-rb + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook-delete-policy": before-hook-creation + "helm.sh/hook": "pre-install,pre-delete,post-delete" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "goquorum-node.fullname" . }}-hooks-role +subjects: + - kind: ServiceAccount + namespace: {{ .Release.Namespace }} + {{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + name: {{ .Values.azure.serviceAccountName }} + {{- else if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + name: {{ .Values.aws.serviceAccountName }} + {{- else }} + name: {{ include "goquorum-node.fullname" . }}-hooks-sa + {{- end}} \ No newline at end of file diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-service-account.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-service-account.yaml new file mode 100644 index 00000000..43e88779 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-service-account.yaml @@ -0,0 +1,44 @@ +{{- if not .Values.cluster.cloudNativeServices }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "goquorum-node.fullname" . }}-sa + namespace: {{ .Release.Namespace }} + +{{- end}} + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "goquorum-node.fullname" . }}-role + namespace: {{ .Release.Namespace }} +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get"] + - apiGroups: [""] + resources: ["services"] + verbs: ["get", "list"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "goquorum-node.fullname" . }}-rb + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "goquorum-node.fullname" . }}-role +subjects: + - kind: ServiceAccount + namespace: {{ .Release.Namespace }} + {{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + name: {{ .Values.azure.serviceAccountName }} + {{- else if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + name: {{ .Values.aws.serviceAccountName }} + {{- else }} + name: {{ include "goquorum-node.fullname" . }}-sa + {{- end}} \ No newline at end of file diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-service.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-service.yaml new file mode 100644 index 00000000..ebfb731f --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-service.yaml @@ -0,0 +1,73 @@ + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "goquorum-node.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/component: service + app.kubernetes.io/part-of: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/release: {{ .Release.Name }} + app.kubernetes.io/managed-by: helm + namespace: {{ .Release.Namespace }} +spec: +{{- if (.Values.bdk.migrate)}} + type: NodePort +{{- else}} + type: ClusterIP +{{- end}} + selector: + app.kubernetes.io/part-of: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/release: {{ .Release.Name }} + app.kubernetes.io/component: goquorum + ports: +{{- if not (.Values.bdk.loadBalancer.enabled)}} + - name: json-rpc + port: {{ .Values.node.goquorum.rpc.port }} + targetPort: json-rpc + protocol: TCP + - name: ws + port: {{ .Values.node.goquorum.ws.port }} + targetPort: ws + protocol: TCP + - name: graphql + port: {{ .Values.node.goquorum.graphql.port }} + targetPort: graphql + protocol: TCP +{{- end}} + - name: rlpx + port: {{ .Values.node.goquorum.p2p.port }} + targetPort: rlpx + protocol: TCP +{{- if (.Values.bdk.migrate)}} + nodePort: {{ .Values.bdk.nodePort }} +{{- end}} + - name: discovery + port: {{ .Values.node.goquorum.p2p.port }} + targetPort: discovery + protocol: UDP +{{- if (.Values.bdk.migrate)}} + nodePort: {{ .Values.bdk.nodePort }} +{{- end}} + - name: metrics + port: {{ .Values.node.goquorum.metrics.pprofport }} + targetPort: metrics + protocol: TCP + +{{- if .Values.quorumFlags.privacy }} + - name: tessera + port: {{ .Values.node.tessera.port }} + targetPort: tessera + protocol: TCP + - name: tessera-tp + port: {{ .Values.node.tessera.tpport }} + targetPort: tessera-tp + protocol: TCP + - name: tessera-q2t + port: {{ .Values.node.tessera.q2tport }} + targetPort: tessera-q2t + protocol: TCP +{{- end }} diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-servicemonitor.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-servicemonitor.yaml new file mode 100644 index 00000000..72675697 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-servicemonitor.yaml @@ -0,0 +1,33 @@ + +{{- if and .Values.node.goquorum.metrics.enabled .Values.node.goquorum.metrics.serviceMonitorEnabled }} + +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "goquorum-node.fullname" . }}-servicemonitor + labels: + release: monitoring + app.kubernetes.io/name: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/part-of: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/namespace: {{ template "goquorum-node.chart" . }} + app.kubernetes.io/release: monitoring + namespace: {{ .Release.Namespace }} +spec: + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/part-of: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/release: {{ .Release.Name }} + endpoints: + - port: metrics + interval: 15s + path: /debug/metrics/prometheus + scheme: http + honorLabels: true +{{- end }} + diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-statefulset.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-statefulset.yaml new file mode 100644 index 00000000..b47d4d68 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-statefulset.yaml @@ -0,0 +1,358 @@ + +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ template "goquorum-node.fullname" . }} + labels: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + azure.workload.identity/use: "true" +{{- end }} + app.kubernetes.io/name: goquorum-statefulset + app.kubernetes.io/component: goquorum + app.kubernetes.io/part-of: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/release: {{ .Release.Name }} + app.kubernetes.io/managed-by: helm + {{- range $labelName, $labelValue := .Values.node.goquorum.customLabels }} + {{ $labelName }}: {{ $labelValue }} + {{- end }} + {{- if (.Values.bdk.loadBalancer.enabled) }} + app: bdk-network-loadbalancer + {{- end}} + namespace: {{ .Release.Namespace }} +spec: + replicas: 1 + podManagementPolicy: OrderedReady + updateStrategy: + type: RollingUpdate + selector: + matchLabels: + app.kubernetes.io/name: goquorum-statefulset + app.kubernetes.io/component: goquorum + app.kubernetes.io/part-of: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/release: {{ .Release.Name }} + app.kubernetes.io/managed-by: helm + serviceName: {{ include "goquorum-node.fullname" . }} + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: [ "ReadWriteOnce" ] + storageClassName: {{ include "goquorum-node.fullname" . }}-storage + resources: + requests: + storage: "{{ .Values.storage.pvcSizeLimit }}" + template: + metadata: + labels: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + azure.workload.identity/use: "true" +{{- end }} + app.kubernetes.io/name: goquorum-statefulset + app.kubernetes.io/component: goquorum + app.kubernetes.io/part-of: {{ include "goquorum-node.fullname" . }} + app.kubernetes.io/namespace: {{ .Release.Namespace }} + app.kubernetes.io/release: {{ .Release.Name }} + app.kubernetes.io/managed-by: helm + {{- if (.Values.bdk.loadBalancer.enabled) }} + app: bdk-network-loadbalancer + {{- end}} + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: {{ .Values.node.goquorum.metrics.pprofport | quote}} + prometheus.io/path: "/debug/metrics/prometheus" + spec: +{{- if and (eq .Values.cluster.provider "azure") (.Values.cluster.cloudNativeServices) }} + serviceAccountName: {{ .Values.azure.serviceAccountName }} +{{- else if and (eq .Values.cluster.provider "aws") (.Values.cluster.cloudNativeServices) }} + serviceAccountName: {{ .Values.aws.serviceAccountName }} +{{- else }} + serviceAccountName: {{ include "goquorum-node.fullname" . }}-sa +{{- end }} + containers: + +{{- if .Values.quorumFlags.privacy }} + - name: {{ include "goquorum-node.fullname" . }}-tessera + image: {{ .Values.image.tessera.repository }}:{{ .Values.image.tessera.tag }} + imagePullPolicy: {{ .Values.image.tessera.imagePullPolicy }} + resources: + requests: + cpu: "{{ .Values.node.tessera.resources.cpuRequest }}" + memory: "{{ .Values.node.tessera.resources.memRequest }}" + limits: + cpu: "{{ .Values.node.tessera.resources.cpuLimit }}" + memory: "{{ .Values.node.tessera.resources.memLimit }}" + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: TESSERA_CONFIG_TYPE + value: "-09" + + volumeMounts: +{{- if and (ne .Values.cluster.provider "local") (.Values.cluster.cloudNativeServices) }} + - name: secrets-store + mountPath: {{ .Values.node.tessera.keysPath }} +{{- else }} + - name: tessera-keys + mountPath: {{ .Values.node.tessera.keysPath }} + readOnly: true +{{- end}} + - name: data + mountPath: {{ .Values.node.tessera.dataPath }} + - name: tessera-peers + mountPath: /config/tessera-peers + ports: + - name: tessera + containerPort: {{ .Values.node.tessera.port }} + protocol: TCP + - name: tessera-tp + containerPort: {{ .Values.node.tessera.tpport }} + protocol: TCP + - name: tessera-q2t + containerPort: {{ .Values.node.tessera.q2tport }} + protocol: TCP + command: + - /bin/sh + - -c + args: + - | + exec + + cp {{ .Values.node.tessera.keysPath }}/tm.* {{ .Values.node.tessera.dataPath }}/ ; + + cat < {{ .Values.node.tessera.dataPath }}/tessera-config-09.json + { + "useWhiteList": false, + "jdbc": { + "username": "sa", + "password": "", + "url": "jdbc:h2:{{ .Values.node.tessera.dataPath }}/tm/db;MODE=Oracle;TRACE_LEVEL_SYSTEM_OUT=0", + "autoCreateTables": true + }, + "serverConfigs":[ + { + "app":"ThirdParty", + "enabled": true, + "serverAddress": "http://{{ include "goquorum-node.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.node.tessera.tpport }}", + "communicationType" : "REST" + }, + { + "app":"Q2T", + "enabled": true, + "serverAddress": "http://{{ include "goquorum-node.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.node.tessera.q2tport }}", + "sslConfig": { + "tls": "OFF" + }, + "communicationType" : "REST" + }, + { + "app":"P2P", + "enabled": true, + "serverAddress": "http://{{ include "goquorum-node.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.node.tessera.port }}", + "sslConfig": { + "tls": "OFF" + }, + "communicationType" : "REST" + } + ], + "peer": $$(cat /config/tessera-peers/tesseraPeers), + "keys": { + {{ if .Values.node.tessera.password }} + "passwordFile": "{{ .Values.node.tessera.passwordPath }}", + {{ end }} + "keyData": [ + { + "privateKeyPath": "/keys/tm.key", + "publicKeyPath": "/keys/tm.pub" + } + ] + }, + "alwaysSendTo": [] + } + EOF + + cat {{ .Values.node.tessera.dataPath }}/tessera-config-09.json + /tessera/bin/tessera -configfile {{ .Values.node.tessera.dataPath }}/tessera-config-09.json + +{{- end }} + + - name: {{ include "goquorum-node.fullname" . }}-quorum + image: {{ .Values.image.goquorum.repository }}:{{ .Values.image.goquorum.tag }} + imagePullPolicy: {{ .Values.image.goquorum.imagePullPolicy }} + resources: + requests: + cpu: "{{ .Values.node.goquorum.resources.cpuRequest }}" + memory: "{{ .Values.node.goquorum.resources.memRequest }}" + limits: + cpu: "{{ .Values.node.goquorum.resources.cpuLimit }}" + memory: "{{ .Values.node.goquorum.resources.memLimit }}" + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: QUORUM_NETWORK_ID + value: "{{ .Values.node.goquorum.networkId }}" + - name: QUORUM_CONSENSUS + value: istanbul +{{- if .Values.quorumFlags.privacy }} + - name: QUORUM_PTM_URL + value: "http://{{ include "goquorum-node.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.node.tessera.q2tport }}" +{{ else }} + - name: PRIVATE_CONFIG + value: "ignore" +{{- end }} + volumeMounts: +{{- if and (ne .Values.cluster.provider "local") (.Values.cluster.cloudNativeServices) }} + - name: secrets-store + mountPath: {{ .Values.node.goquorum.keysPath }} +{{- else}} + - name: quorum-keys + mountPath: {{ .Values.node.goquorum.keysPath }} + readOnly: true +{{- if .Values.quorumFlags.privacy }} + - name: tessera-keys + mountPath: {{ .Values.node.goquorum.privacy.pubkeysPath }} + readOnly: true +{{- end}} +{{- end}} + - name: genesis + mountPath: /etc/genesis + readOnly: true + - name: static-nodes + mountPath: /config/static + readOnly: true + - name: data + mountPath: {{ .Values.node.goquorum.dataPath }} + ports: + - name: json-rpc + containerPort: {{ .Values.node.goquorum.rpc.port }} + protocol: TCP + - name: ws + containerPort: {{ .Values.node.goquorum.ws.port }} + protocol: TCP + - name: graphql + containerPort: {{ .Values.node.goquorum.graphql.port }} + protocol: TCP + - name: rlpx + containerPort: {{ .Values.node.goquorum.p2p.port }} + protocol: TCP + - name: discovery + containerPort: {{ .Values.node.goquorum.p2p.port }} + protocol: UDP + - name: metrics + containerPort: {{ .Values.node.goquorum.metrics.pprofport }} + protocol: TCP + command: + - /bin/sh + - -c + args: + - | + exec + + {{- if .Values.quorumFlags.privacy }} + until $(curl --output /dev/null --silent --head --fail http://localhost:9000/upcheck); do echo 'waiting for transaction manager to start...'; sleep 5; done; + echo 'transaction manager is up'; + {{- end }} + + geth --datadir={{ .Values.node.goquorum.dataPath }} init {{ .Values.node.goquorum.genesisFilePath }} + cp {{ .Values.node.goquorum.keysPath }}/nodekey {{ .Values.node.goquorum.dataPath }}/geth/nodekey + cp /config/static/static-nodes.json {{ .Values.node.goquorum.dataPath }}/geth/static-nodes.json + mkdir -p {{ .Values.node.goquorum.dataPath }}/keystore/ + cp {{ .Values.node.goquorum.keysPath }}/accountKeystore {{ .Values.node.goquorum.dataPath }}/keystore/key; +{{- if .Values.bdk.migrate }} + geth \ + --datadir {{ .Values.node.goquorum.dataPath }} --networkid 81712 \ + --nodiscover --verbosity 3 \ + --syncmode full --nousb --mine --miner.threads 1 --miner.gasprice 0 \ + --nat extip:$POD_IP \ + --emitcheckpoints \ + --metrics --pprof --pprof.addr {{ .Values.node.goquorum.metrics.pprofaddr | quote }} --pprof.port {{ .Values.node.goquorum.metrics.pprofport }} \ + --port {{ .Values.node.goquorum.p2p.port }} \ +{{- else }} + geth \ + --datadir {{ .Values.node.goquorum.dataPath }} \ + --nodiscover --ipcdisable \ + --nat extip:$POD_IP \ + --verbosity {{ .Values.node.goquorum.log.verbosity }} \ + --istanbul.blockperiod {{ .Values.node.goquorum.miner.blockPeriod }} --mine --miner.threads {{ .Values.node.goquorum.miner.threads }} --miner.gasprice 0 --emitcheckpoints \ + --syncmode full --nousb \ + --metrics --pprof --pprof.addr {{ .Values.node.goquorum.metrics.pprofaddr | quote }} --pprof.port {{ .Values.node.goquorum.metrics.pprofport }} \ + --networkid {{ .Values.node.goquorum.networkId }} \ + --port {{ .Values.node.goquorum.p2p.port }} \ +{{- end }} + {{- if .Values.node.goquorum.rpc.enabled }} + --http --http.addr {{ .Values.node.goquorum.rpc.addr }} --http.port {{ .Values.node.goquorum.rpc.port }} --http.corsdomain {{ .Values.node.goquorum.rpc.corsDomain | quote }} --http.vhosts {{ .Values.node.goquorum.rpc.vHosts | quote }} --http.api {{ .Values.node.goquorum.rpc.api | quote }} \ + {{- end }} + {{- if .Values.node.goquorum.ws.enabled }} + --ws --ws.addr {{ .Values.node.goquorum.ws.addr }} --ws.port {{ .Values.node.goquorum.ws.port }} --ws.origins {{ .Values.node.goquorum.ws.origins | quote }} --ws.api {{ .Values.node.goquorum.ws.api | quote }} \ + {{- end }} + {{- if .Values.quorumFlags.privacy }} + --ptm.timeout 5 --ptm.url {{ .Values.node.goquorum.privacy.url }} --ptm.http.writebuffersize 4096 --ptm.http.readbuffersize 4096 --ptm.tls.mode off \ + {{- end }} + {{- if hasKey .Values.node.goquorum.account "unlock" }} + --unlock {{ .Values.node.goquorum.account.unlock }} --allow-insecure-unlock --password {{ .Values.node.goquorum.account.passwordPath }} \ + {{- end }} + + livenessProbe: + httpGet: + path: / + port: 8545 + httpHeaders: + - name: Content-Type + value: application/json + initialDelaySeconds: 180 + periodSeconds: 60 + + volumes: + - name: genesis + configMap: + name: goquorum-genesis + items: + - key: genesis.json + path: genesis.json + - name: static-nodes + configMap: + name: goquorum-peers + items: + - key: static-nodes.json + path: static-nodes.json + +{{- if and (ne .Values.cluster.provider "local") (.Values.cluster.cloudNativeServices) }} + - name: secrets-store + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: {{ include "goquorum-node.fullname" . }}-azure-secret-provider +{{- else }} + - name: quorum-keys + secret: + secretName: {{ include "goquorum-node.fullname" . }}-keys +{{- if .Values.quorumFlags.privacy }} + - name: tessera-keys + secret: + secretName: {{ include "goquorum-node.fullname" . }}-tessera-keys +{{- end }} +{{- end }} +{{- if .Values.quorumFlags.privacy }} + - name: tessera-peers + configMap: + name: tessera-peers + items: + - key: tesseraPeers + path: tesseraPeers +{{- end }} + diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-storage.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-storage.yaml new file mode 100644 index 00000000..90d0fa17 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/templates/node-storage.yaml @@ -0,0 +1,73 @@ + +{{- if eq .Values.cluster.provider "azure" }} + +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: {{ include "goquorum-node.fullname" . }}-storage + namespace: {{ .Release.Namespace }} +provisioner: kubernetes.io/azure-file +reclaimPolicy: "{{.Values.storage.reclaimPolicy }}" +allowVolumeExpansion: true +mountOptions: + - dir_mode=0755 + - file_mode=0755 + - uid=0 + - gid=0 + - mfsymlinks +parameters: + skuName: Standard_LRS + +{{- else if eq .Values.cluster.provider "aws" }} + +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: {{ include "goquorum-node.fullname" . }}-storage + namespace: {{ .Release.Namespace }} +provisioner: {{ .Values.storage.aws.provisioner }} +reclaimPolicy: "{{.Values.storage.reclaimPolicy }}" +allowVolumeExpansion: true +parameters: + type: {{ .Values.storage.aws.parameters.type }} + fsType: {{ .Values.storage.aws.parameters.fsType }} + +# --- +# apiVersion: storage.k8s.io/v1 +# kind: StorageClass +# metadata: +# name: {{ include "goquorum-node.fullname" . }}-storage +# namespace: {{ .Release.Namespace }} +# provisioner: efs.csi.aws.com +# reclaimPolicy: "{{.Values.storage.reclaimPolicy }}" +# parameters: +# provisioningMode: efs-ap +# fileSystemId: #your_file_system_id +# directoryPerms: "700" +# gidRangeStart: "1000" # optional +# gidRangeEnd: "2000" # optional +# basePath: "/dynamic_provisioning" # optional + +{{- else }} + +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: {{ include "goquorum-node.fullname" . }}-storage + namespace: {{ .Release.Namespace }} + labels: + type: local +spec: + storageClassName: {{ include "goquorum-node.fullname" . }}-storage + capacity: + storage: "{{ .Values.storage.sizeLimit }}" + accessModes: + - ReadWriteOnce + hostPath: + path: "/tmp/{{ include "goquorum-node.fullname" . }}" + +{{- end }} + diff --git a/src/quorum/instance/infra/kubernetes/charts/goquorum-node/values.yaml b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/values.yaml new file mode 100644 index 00000000..d4bd0a5a --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/charts/goquorum-node/values.yaml @@ -0,0 +1,130 @@ +--- + +quorumFlags: + privacy: false + removeKeysOnDelete: false + +cluster: + provider: local # choose from: local | aws | azure + cloudNativeServices: false # set to true to use Cloud Native Services (SecretsManager and IAM for AWS; KeyVault & Managed Identities for Azure) + reclaimPolicy: Delete # set to either Retain or Delete + +aws: + # the aws cli commands uses the name 'quorum-sa' so only change this if you altered the name + serviceAccountName: quorum-sa + # the region you are deploying to + region: ap-southeast-2 + +azure: + serviceAccountName: quorum-sa + # the clientId of the user assigned managed identity created in the template + identityClientId: azure-clientId + keyvaultName: azure-keyvault + # the tenant ID of the key vault + tenantId: azure-tenantId + # the subscription ID to use - this needs to be set explictly when using multi tenancy + subscriptionId: azure-subscriptionId + +storage: + sizeLimit: "20Gi" + pvcSizeLimit: "20Gi" + # NOTE: when you set this to Retain, the volume WILL persist after the chart is delete and you need to manually delete it + reclaimPolicy: "Delete" # choose from: Delete | Retain + aws: + provisioner: kubernetes.io/aws-ebs + parameters: + type: gp3 + fsType: ext4 + +node: + goquorum: + resources: + cpuLimit: 1 + cpuRequest: 0.1 + memLimit: "2G" + memRequest: "1G" + # privKey: + # pubKey: + dataPath: "/data/quorum" + keysPath: "/keys" + genesisFilePath: "/etc/genesis/genesis.json" + customLabels: {} + networkId: 10 + replicaCount: 1 + account: + unlock: 0 + password: 'password' + passwordPath: "/keys/accountPassword" + log: + verbosity: 5 + miner: + threads: 1 + blockPeriod: 5 + p2p: + enabled: true + addr: "0.0.0.0" + port: 30303 + rpc: + enabled: true + addr: "0.0.0.0" + port: 8545 + corsDomain: "*" + vHosts: "*" + api: "admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul" + authenticationEnabled: false + ws: + enabled: true + addr: "0.0.0.0" + port: 8546 + api: "admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul" + origins: "*" + authenticationEnabled: false + graphql: + enabled: true + addr: "0.0.0.0" + port: 8547 + corsDomain: "*" + vHosts: "*" + metrics: + enabled: true + pprofaddr: "0.0.0.0" + pprofport: 9545 + serviceMonitorEnabled: false + privacy: + url: "http://localhost:9101" + pubkeysPath: "/tessera" + pubkeyFile: "/tessera/tm.pub" + + tessera: + resources: + cpuLimit: 0.7 + cpuRequest: 0.5 + memLimit: "2G" + memRequest: "1G" + tmkey: "" + tmpub: "" + password: "password" + passwordPath: "/keys/tm.password" + dataPath: "/data/tessera" + keysPath: "/keys" + port: 9000 + tpport: 9080 + q2tport: 9101 + +image: + goquorum: + repository: quorumengineering/quorum + tag: 23.4.0 + tessera: + repository: quorumengineering/tessera + tag: 22.1.7 + hooks: + repository: consensys/quorum-k8s-hooks + tag: qgt-0.2.12 + pullPolicy: IfNotPresent + +bdk: + migrate: false + nodePort: 30303 + loadBalancer: + enabled: false \ No newline at end of file diff --git a/src/quorum/instance/infra/kubernetes/runner.ts b/src/quorum/instance/infra/kubernetes/runner.ts new file mode 100644 index 00000000..a6e2d369 --- /dev/null +++ b/src/quorum/instance/infra/kubernetes/runner.ts @@ -0,0 +1,134 @@ +import { logger } from '../../../../util/logger' +import { spawn } from 'child_process' +import config from '../../../config' +import { DockerResultType, KubernetesInfraRunner } from '../InfraRunner.interface' +import { ClusterDeleteType, K8SRunCommandType } from '../../../model/type/kubernetes.type' + +export class Runner implements KubernetesInfraRunner { + public createDeploymentAndService = async (payload: K8SRunCommandType): Promise => { + await this.checkAndCreateNamespace(payload.namespace) + const helmOutput = await this.runHelm( + ['install', + payload.name, + payload.helmChart, + '--namespace', payload.namespace, + '--values', payload.values]) + return { stdout: helmOutput } + } + + public createTemplate = async (payload: K8SRunCommandType): Promise => { + const helmOutput = await this.runHelm( + ['template', + payload.name, + payload.helmChart, + '--namespace', payload.namespace, + '--values', payload.values]) + return { stdout: helmOutput } + } + + public wait = async (job: string, namespace: string, timeoutSecond = 300): Promise => { + const k8sOutput = await this.runKubectl([ + 'wait', + '--for=condition=complete', + job, + '-n', + namespace, + `--timeout=${timeoutSecond.toString()}s`]) + return { stdout: k8sOutput } + } + + public listAllRelease = async (namespace: string): Promise => { + const helmOutput = await this.runHelm(['list', '--short', '--namespace', namespace]) + return { stdout: helmOutput } + } + + public deleteDeploymentAndService = async (payload: ClusterDeleteType): Promise => { + await this.runHelm( + ['uninstall', + payload.name, + '--namespace', payload.namespace]) + return { stdout: '' } + } + + public deleteAll = async (payload: K8SRunCommandType): Promise => { + await this.runKubectl(['delete', 'all', '--all', '-n', payload.namespace]) + return { stdout: '' } + } + + public chooseCluster = async (payload: K8SRunCommandType): Promise => { + await console.log('chooseCluster') + await this.runKubectl(['config', 'use-context', payload.name]) + return { stdout: '' } + } + + public clusterList = async (): Promise => { + await this.runKubectl(['config', 'get-contexts']) + return { stdout: '' } + } + + private async checkAndCreateNamespace (namespace: string): Promise { + try { + await this.runKubectl(['get', 'namespaces', namespace]) + } catch (e: any) { + if (e.message.includes('NotFound')) { + await this.runKubectl(['create', 'namespace', namespace]) + } + } + } + + private checkInstall (name: string): Promise { + return new Promise((resolve) => { + const spawnReturn = spawn('helm', ['list', '-n', name]) + spawnReturn.stdout.on('data', (data) => { + resolve(data.toString()) + }) + spawnReturn.on('error', (error) => { + throw new Error(`[x] command [helm]: ${error.message}`) + }) + }) + } + + private runKubectl (args: Array): Promise { + return new Promise((resolve, reject) => { + logger.debug(`run spawnSync: kubectl ${args.join(' ')}`) + const spawnReturn = spawn('kubectl', [...args], { env: { ...process.env, UID: `${config.UID}`, GID: `${config.GID}` } }) + let output = '' + spawnReturn.stdout.on('data', (data) => { + output += data + }) + + spawnReturn.stderr.on('data', (data) => { + output += data + }) + spawnReturn.stdout.on('close', () => { + if (output.includes('Error')) reject(new Error(`[x] command [kubectl ${args.join(' ')}]: ${output}`)) + resolve(`kubectl ${args.join(' ')} ${output} OK`) + }) + spawnReturn.on('error', (error) => { + throw new Error(`[x] command [kubectl]: ${error.message}`) + }) + }) + } + + private runHelm (args: Array): Promise { + return new Promise((resolve, reject) => { + logger.debug(`run spawnSync: helm ${args.join(' ')}`) + const spawnReturn = spawn('helm', [...args], { env: { ...process.env, UID: `${config.UID}`, GID: `${config.GID}` } }) + let output = '' + spawnReturn.stdout.on('data', (data) => { + output += data + }) + + spawnReturn.stderr.on('data', (data) => { + output += data + }) + spawnReturn.stdout.on('close', () => { + resolve(`helm ${args.join(' ')} OK\n${output}`) + if (output.includes('Error')) reject(new Error(`[x] command [helm ${args.join(' ')}]: ${output}`)) + }) + spawnReturn.on('error', (error) => { + throw new Error(`[x] command [helm]: ${error.message}`) + }) + }) + } +} diff --git a/src/quorum/instance/kubernetesCluster.ts b/src/quorum/instance/kubernetesCluster.ts new file mode 100644 index 00000000..22ae7fb6 --- /dev/null +++ b/src/quorum/instance/kubernetesCluster.ts @@ -0,0 +1,40 @@ +import { AbstractInstance } from './Instance.abstract' +import { logger } from '../../util' +import { K8SRunCommandType, ClusterDeleteType } from '../model/type/kubernetes.type' + +export default class KubernetesInstance extends AbstractInstance { + public async install (payload: K8SRunCommandType) { + logger.debug('Kubernetes instance install') + if (this.kubernetesInfra !== undefined) { + return await this.kubernetesInfra.createDeploymentAndService(payload) + } + } + + public async template (payload: K8SRunCommandType) { + logger.debug('Kubernetes instance template') + if (this.kubernetesInfra !== undefined) { + return await this.kubernetesInfra.createTemplate(payload) + } + } + + public async delete (payload: ClusterDeleteType) { + logger.debug('Kubernetes instance delete') + if (this.kubernetesInfra !== undefined) { + return await this.kubernetesInfra.deleteDeploymentAndService(payload) + } + } + + public async listAllRelease (namespace: string) { + logger.debug('Kubernetes instance listAllRelease') + if (this.kubernetesInfra !== undefined) { + return await this.kubernetesInfra.listAllRelease(namespace) + } + } + + public async wait (job: string, namespace: string) { + logger.debug('Kubernetes instance wait') + if (this.kubernetesInfra !== undefined) { + return await this.kubernetesInfra.wait(job, namespace) + } + } +} diff --git a/src/quorum/model/type/kubernetes.type.ts b/src/quorum/model/type/kubernetes.type.ts new file mode 100644 index 00000000..33a04784 --- /dev/null +++ b/src/quorum/model/type/kubernetes.type.ts @@ -0,0 +1,25 @@ +import { NetworkCreateType } from './network.type' +export interface K8SRunCommandType { + helmChart: string + name: string + namespace: string + values: string + version?: string + ignoreError?: boolean +} + +export interface ClusterDeleteType { + name: string + namespace: string +} +export interface ClusterCreateType extends NetworkCreateType { + provider: string + region?: string +} + +/** + * @requires chartPackageModeEnabled - package without helm and k8s + */ +export interface ClusterGenerateType { + chartPackageModeEnabled: boolean +} diff --git a/src/quorum/model/yaml/helm-chart/blockscoutYaml.ts b/src/quorum/model/yaml/helm-chart/blockscoutYaml.ts new file mode 100644 index 00000000..2f0de87e --- /dev/null +++ b/src/quorum/model/yaml/helm-chart/blockscoutYaml.ts @@ -0,0 +1,49 @@ +import HelmChartYaml from './helmChartYaml' + +class BlockscoutConfigYaml extends HelmChartYaml { + public setImage () { + this.setService('image', { + blockscout: { + repository: 'consensys/blockscout', + tag: 'v4.0.0-beta', + pullPolicy: 'IfNotPresent', + }, + }) + } + + public setPostgresql () { + this.setService('postgresql', { + postgresqlDatabase: 'postgres', + postgresqlUsername: 'postgres', + postgresqlPassword: 'postgres', + initdbUser: 'postgres', + initdbPassword: 'postgres', + enabled: true, + }) + } + + public setBlockscout () { + this.setService('blockscout', { + resources: { + cpuLimit: 0.7, + cpuRequest: 0.5, + memLimit: '2G', + memRequest: '1G', + }, + port: 4000, + database_url: 'ecto://postgres:postgres@blockscout-postgresql/postgres?ssl=false', + postgres_password: 'postgres', + postgres_user: 'postgres', + network: 'quorum', + subnetwork: 'consensys', + chain_id: 1337, + coin: 'eth', + ethereum_jsonrpc_variant: 'geth', + ethereum_jsonrpc_transport: 'http', + ethereum_jsonrpc_endpoint: 'goquorum-node-member-1', // service name to be used + secret_key_base: 'VTIB3uHDNbvrY0+60ZWgUoUBKDn9ppLR8MI4CpRz4/qLyEFs54ktJfaNT6Z221', // change me please + }) + } +} + +export default BlockscoutConfigYaml diff --git a/src/quorum/model/yaml/helm-chart/genesisYaml.ts b/src/quorum/model/yaml/helm-chart/genesisYaml.ts new file mode 100644 index 00000000..f2d46618 --- /dev/null +++ b/src/quorum/model/yaml/helm-chart/genesisYaml.ts @@ -0,0 +1,41 @@ +import HelmChartYaml from './helmChartYaml' + +class GenesisConfigYaml extends HelmChartYaml { + public setGenesis (chainID: number, nodeCount: number) { + this.setQuorumFlags({ + privacy: false, + removeKeysOnDelete: false, + isBootnode: false, + usesBootnodes: false, + }) + + const genesisConfig = { + genesis: { + config: { + chainId: chainID, + algorithm: { + consensus: 'qbft', + blockperiodseconds: 1, + emptyBlockPeriod: 3600, + epochlength: 30000, + requesttimeoutseconds: 60, + }, + gasLimit: '0xE0000000', + difficulty: '0x1', + coinbase: '0x0000000000000000000000000000000000000000', + includeQuickStartAccounts: false, + }, + }, + blockchain: { + nodes: { + generate: true, + count: nodeCount, + }, + accountPassword: 'password', + }, + } + this.setService('rawGenesisConfig', genesisConfig) + } +} + +export default GenesisConfigYaml diff --git a/src/quorum/model/yaml/helm-chart/helmChartYaml.ts b/src/quorum/model/yaml/helm-chart/helmChartYaml.ts new file mode 100644 index 00000000..1d73b8cc --- /dev/null +++ b/src/quorum/model/yaml/helm-chart/helmChartYaml.ts @@ -0,0 +1,109 @@ +import BdkYaml from '../bdkYaml' + +interface ClusterInterface { + provider: string + cloudNativeServices: boolean + reclaimPolicy?: string +} + +interface quorumFlags { + privacy: boolean + removeKeysOnDelete: boolean + isBootnode: boolean + usesBootnodes: boolean +} + +interface AwsInterface { + serviceAccountName: string + region: string +} + +interface AzureInterface { + serviceAccountName: string + identityClientId: string + keyvaultName: string + tenantId: string + subscriptionId: string +} + +interface NodeInterface { + goquorum: { + metrics: { + serviceMonitorEnabled: boolean + } + resources: { + cpuLimit: number + cpuRequest: number + memLimit: string + memRequest: string + } + account?: { + password: string + } + } + tessera?: { + password: string + } +} +export interface HelmChartYamlInterface { + quorumFlags: quorumFlags + cluster?: ClusterInterface + aws?: AwsInterface + azure?: AzureInterface + node?: NodeInterface + [key: string]: any +} + +class HelmChartYaml extends BdkYaml { + constructor (value?: HelmChartYamlInterface) { + super(value) + if (!value) { + this.value.cluster = { + provider: 'local', + cloudNativeServices: false, + } + } + } + + public setProvider (provider: string, region: string = 'ap-southeast-2') { + const clusterConfig = { + provider: provider, + cloudNativeServices: (provider !== 'local'), + } + this.setCluster(clusterConfig) + + const providers: { [key: string]: any } = { + aws: { + serviceAccountName: 'quorum-sa', + region: region, + }, + azure: { + serviceAccountName: 'quorum-sa', + identityClientId: 'azure-clientId', + keyvaultName: 'azure-keyvault', + tenantId: 'azure-tenantId', + subscriptionId: 'azure-subscriptionId', + }, + } + + this.setService(provider, providers[provider]) + } + + protected setQuorumFlags (quorumFlags: quorumFlags) { + this.value.quorumFlags = quorumFlags + } + + protected setCluster (cluster: ClusterInterface) { + this.value.cluster = cluster + } + + protected setNode (node: NodeInterface) { + this.value.node = node + } + + protected setService (name: string, service: any) { + this.value[name] = service + } +} + +export default HelmChartYaml diff --git a/src/quorum/model/yaml/helm-chart/index.ts b/src/quorum/model/yaml/helm-chart/index.ts new file mode 100644 index 00000000..336d2e4b --- /dev/null +++ b/src/quorum/model/yaml/helm-chart/index.ts @@ -0,0 +1,4 @@ +export { default as GenesisConfigYaml } from './genesisYaml' +export { default as ValidatorConfigYaml } from './validatorYaml' +export { default as MemberConfigYaml } from './memberYaml' +export { default as BlockscoutConfigYaml } from './blockscoutYaml' diff --git a/src/quorum/model/yaml/helm-chart/memberYaml.ts b/src/quorum/model/yaml/helm-chart/memberYaml.ts new file mode 100644 index 00000000..094fa4e2 --- /dev/null +++ b/src/quorum/model/yaml/helm-chart/memberYaml.ts @@ -0,0 +1,32 @@ +import HelmChartYaml from './helmChartYaml' + +class MemberConfigYaml extends HelmChartYaml { + public setQuorumConfigs (metrics = false) { + this.setQuorumFlags({ + privacy: false, + removeKeysOnDelete: false, + isBootnode: false, + usesBootnodes: false, + }) + + this.setNode({ + goquorum: { + metrics: { + // default value in helm is true + serviceMonitorEnabled: metrics, + }, + resources: { + cpuLimit: 1, + cpuRequest: 0.1, + memLimit: '2G', + memRequest: '0.5G', + }, + account: { + password: 'password', + }, + }, + }) + } +} + +export default MemberConfigYaml diff --git a/src/quorum/model/yaml/helm-chart/validatorYaml.ts b/src/quorum/model/yaml/helm-chart/validatorYaml.ts new file mode 100644 index 00000000..dfa44d70 --- /dev/null +++ b/src/quorum/model/yaml/helm-chart/validatorYaml.ts @@ -0,0 +1,29 @@ +import HelmChartYaml from './helmChartYaml' + +class ValidatorConfigYaml extends HelmChartYaml { + public setQuorumConfigs (metrics = false) { + this.setQuorumFlags({ + privacy: false, + removeKeysOnDelete: false, + isBootnode: false, + usesBootnodes: false, + }) + + this.setNode({ + goquorum: { + metrics: { + // default value in helm is true + serviceMonitorEnabled: metrics, + }, + resources: { + cpuLimit: 1, + cpuRequest: 0.1, + memLimit: '2G', + memRequest: '0.5G', + }, + }, + }) + } +} + +export default ValidatorConfigYaml diff --git a/src/quorum/service/Service.abstract.ts b/src/quorum/service/Service.abstract.ts index 15b22899..a7c82aa5 100644 --- a/src/quorum/service/Service.abstract.ts +++ b/src/quorum/service/Service.abstract.ts @@ -1,8 +1,8 @@ import { Config } from '../config' import BdkFile from '../instance/bdkFile' -import { DockerResultType, InfraStrategy, InfraRunnerResultType, InfraRunner } from '../instance/infra/InfraRunner.interface' +import { DockerResultType, InfraStrategy, InfraRunnerResultType, InfraRunner, KubernetesInfraRunner } from '../instance/infra/InfraRunner.interface' import { Runner as DockerRunner } from '../instance/infra/docker/runner' - +import { Runner as KubernetesRunner } from '../instance/infra/kubernetes/runner' export interface ParserType { [method: string]: (dockerResult: DockerResultType, options?: any) => any } @@ -14,8 +14,14 @@ export abstract class AbstractService { protected bdkFile: BdkFile /** @ignore */ protected infra: InfraRunner + /** @ignore */ + protected kubernetesInfra: KubernetesInfraRunner - constructor (config: Config, infra?: InfraRunner) { + constructor ( + config: Config, + infra?: InfraRunner, + kubernetesInfra?: KubernetesInfraRunner, + ) { this.config = config this.bdkFile = new BdkFile(config) @@ -24,6 +30,12 @@ export abstract class AbstractService { } else { this.infra = InfraStrategy.createRunner(infra) } + + if (kubernetesInfra === undefined) { + this.kubernetesInfra = InfraStrategy.createKubernetesRunner(new KubernetesRunner()) + } else { + this.kubernetesInfra = InfraStrategy.createKubernetesRunner(kubernetesInfra) + } } static readonly parser: ParserType diff --git a/src/quorum/service/cluster.ts b/src/quorum/service/cluster.ts new file mode 100644 index 00000000..957fcb28 --- /dev/null +++ b/src/quorum/service/cluster.ts @@ -0,0 +1,173 @@ +import tar from 'tar' +import { Ora } from 'ora' +import { tarDateFormat } from '../../util' +import { AbstractService } from './Service.abstract' +import KubernetesInstance from '../instance/kubernetesCluster' +import { ClusterCreateType, ClusterGenerateType } from '../model/type/kubernetes.type' +import { GenesisConfigYaml, ValidatorConfigYaml, MemberConfigYaml } from '../model/yaml/helm-chart' +import { DockerResultType } from '../instance/infra/InfraRunner.interface' +export default class Cluster extends AbstractService { + /** + * @description Use helm create quorum template + */ + public async apply (networkCreateConfig: ClusterCreateType, spinner: Ora): Promise { + const { provider, region, chainId, validatorNumber, memberNumber } = networkCreateConfig + // create genesis and account + const k8s = new KubernetesInstance(this.config, this.infra, this.kubernetesInfra) + this.bdkFile.checkHelmChartPath() + const genesisYaml = new GenesisConfigYaml() + genesisYaml.setProvider(provider, region) + genesisYaml.setGenesis(chainId, validatorNumber) + + this.bdkFile.createGenesisChartValues(genesisYaml) + // custom namespace + spinner.start('Helm install genesis chart') + const genesisOutput = await k8s.install({ + helmChart: this.bdkFile.getGoQuorumGenesisChartPath(), + name: 'genesis', + namespace: 'quorum', + values: this.bdkFile.getGenesisChartPath(), + }) as DockerResultType + await k8s.wait('job.batch/goquorum-genesis-init', 'quorum') + spinner.succeed(`Helm install genesis chart ${genesisOutput.stdout}`) + // create network + const validatorYaml = new ValidatorConfigYaml() + validatorYaml.setProvider(provider, region) + validatorYaml.setQuorumConfigs() + + for (let i = 0; i < validatorNumber; i += 1) { + this.bdkFile.createValidatorChartValues(validatorYaml, i) + } + + const memberYaml = new MemberConfigYaml() + memberYaml.setProvider(provider, region) + memberYaml.setQuorumConfigs() + for (let i = 0; i < memberNumber; i += 1) { + this.bdkFile.createMemberChartValues(memberYaml, i) + } + for (let i = 0; i < validatorNumber; i += 1) { + spinner.start(`Helm install validator chart ${i + 1}`) + const validatorOutput = await k8s.install({ + helmChart: this.bdkFile.getGoQuorumNodeChartPath(), + name: `validator-${i + 1}`, + namespace: 'quorum', + values: this.bdkFile.getValidatorChartPath(i), + }) as DockerResultType + spinner.succeed(`Helm install validator chart ${i + 1} ${validatorOutput.stdout}`) + } + for (let i = 0; i < memberNumber; i += 1) { + spinner.start(`Helm install member chart ${i + 1}`) + const memberOutput = await k8s.install({ + helmChart: this.bdkFile.getGoQuorumNodeChartPath(), + name: `member-${i + 1}`, + namespace: 'quorum', + values: this.bdkFile.getMemberChartPath(i), + }) as DockerResultType + spinner.succeed(`Helm install member chart ${i + 1} ${memberOutput.stdout}`) + } + } + + /** + * @description Use helm create quorum template + */ + public async generate ( + clusterGenerateConfig: ClusterGenerateType, + networkCreateConfig: ClusterCreateType, + ): Promise { + const { provider, region, chainId, validatorNumber, memberNumber } = networkCreateConfig + this.bdkFile.checkHelmChartPath() + // create genesis and account + const genesisYaml = new GenesisConfigYaml() + genesisYaml.setProvider(provider, region) + genesisYaml.setGenesis(chainId, validatorNumber) + + this.bdkFile.createGenesisChartValues(genesisYaml) + + const validatorYaml = new ValidatorConfigYaml() + validatorYaml.setProvider(provider, region) + validatorYaml.setQuorumConfigs() + + for (let i = 0; i < validatorNumber; i += 1) { + this.bdkFile.createValidatorChartValues(validatorYaml, i) + } + + const memberYaml = new MemberConfigYaml() + memberYaml.setProvider(provider, region) + memberYaml.setQuorumConfigs() + for (let i = 0; i < memberNumber; i += 1) { + this.bdkFile.createMemberChartValues(memberYaml, i) + } + + if (clusterGenerateConfig.chartPackageModeEnabled) { + const k8s = new KubernetesInstance(this.config, this.infra, this.kubernetesInfra) + const genesisOutput = await k8s.template({ + helmChart: this.bdkFile.getGoQuorumGenesisChartPath(), + name: 'genesis', + namespace: 'quorum', + values: this.bdkFile.getGenesisChartPath(), + }) as DockerResultType + this.bdkFile.createYaml('genesis', genesisOutput.stdout) + + for (let i = 0; i < validatorNumber; i += 1) { + const validatorOutput = await k8s.template({ + helmChart: this.bdkFile.getGoQuorumNodeChartPath(), + name: `validator-${i + 1}`, + namespace: 'quorum', + values: this.bdkFile.getValidatorChartPath(i), + }) as DockerResultType + this.bdkFile.createYaml(`validator-${i + 1}`, validatorOutput.stdout) + } + for (let i = 0; i < memberNumber; i += 1) { + const memberOutput = await k8s.template({ + helmChart: this.bdkFile.getGoQuorumNodeChartPath(), + name: `member-${i + 1}`, + namespace: 'quorum', + values: this.bdkFile.getMemberChartPath(i), + }) as DockerResultType + this.bdkFile.createYaml(`member-${i + 1}`, memberOutput.stdout) + } + } + this.exportChartTar() + } + + /** + * @description Delete all quorum deployment and service + */ + public async delete (): Promise { + const k8s = new KubernetesInstance(this.config, this.infra, this.kubernetesInfra) + const releases = await this.getAllHelmRelease() + await Promise.all(releases.map(async (release: string) => { + await k8s.delete({ name: release, namespace: 'quorum' }) + })) + } + + private async getAllHelmRelease () { + const k8s = new KubernetesInstance(this.config, this.infra, this.kubernetesInfra) + const releases = await k8s.listAllRelease('quorum') as DockerResultType + return releases.stdout.split('\n').slice(1) + } + + public getHelmChartFiles (): string[] { + return this.bdkFile.getHelmChartValuesFiles() + } + + public removeHelmChartFiles (): void { + this.bdkFile.removeHelmChart() + } + + private exportChartTar () { + const bdkPath = this.bdkFile.getBdkPath() + const createOpts = { + gzip: true, + cwd: bdkPath, + sync: true, + } + try { + tar + .c(createOpts, ['helm']) + .pipe(this.bdkFile.createChartTar('chart', tarDateFormat(new Date()))) + } catch (e: any) { + throw new Error(`[x] tar error: ${e.message}`) + } + } +}