diff --git a/.gitignore b/.gitignore index a14702c..385a9fc 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store +Chart.lock \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 5fe7817..33d286c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,6 +25,7 @@ "editor.defaultFormatter": "biomejs.biome" }, "typescript.tsdk": "node_modules/typescript/lib", + "prettier.enable": false, "editor.formatOnSave": true, "editor.formatOnPaste": true, "emmet.showExpandedAbbreviation": "never", diff --git a/README.md b/README.md index ced1781..6fe3e69 100644 --- a/README.md +++ b/README.md @@ -9,30 +9,15 @@ The helm chart to run this on Kubernetes / OpenShift can be found [here](./chart ## CLI usage ``` -Usage: network-bootstrapper [options] +Usage: network-bootstrapper [options] [command] -Generate node identities, configure consensus, and emit a Besu genesis. +Utilities for configuring Besu-based networks. Options: - -v, --validators Number of validator nodes to generate. - (default: 4) - -r, --rpc-nodes Number of RPC nodes to generate. (default: 2) - -a, --allocations Path to a genesis allocations JSON file. - (default: none) - -o, --outputType Output target (screen, file, kubernetes). - (default: "screen") - --consensus Consensus algorithm (IBFTv2, QBFT). (default: - QBFT) - --chain-id Chain ID for the genesis config. (default: - random between 40000 and 50000) - --seconds-per-block Block time in seconds. (default: 2) - --gas-limit Block gas limit in decimal form. (default: - 9007199254740991) - --gas-price Base gas price (wei). (default: 0) - --evm-stack-size EVM stack size limit. (default: 2048) - --contract-size-limit Contract size limit in bytes. (default: - 2147483647) - --accept-defaults Accept default values for all prompts when CLI - flags are omitted. (default: disabled) - -h, --help display help for command + -h, --help display help for command + +Commands: + generate [options] Generate node identities, configure consensus, and emit a + Besu genesis. + help [command] display help for command ``` diff --git a/biome.jsonc b/biome.jsonc index 1e2f179..80c3f80 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,6 +1,9 @@ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "extends": ["ultracite"], + "files": { + "includes": ["**", "!**/*.yaml"] + }, "linter": { "rules": { "suspicious": { diff --git a/charts/network-bootstrapper/README.md b/charts/network-bootstrapper/README.md deleted file mode 100644 index 64a48d1..0000000 --- a/charts/network-bootstrapper/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# network-bootstrapper - -![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) - -A Helm chart for Kubernetes - -## Maintainers - -| Name | Email | Url | -| ---- | ------ | --- | -| SettleMint | | | - -## Values - -| Key | Type | Default | Description | -|-----|------|---------|-------------| -| affinity | object | `{}` | Affinity and anti-affinity rules influencing pod placement. | -| fullnameOverride | string | `""` | Fully qualified name override for resources created by this release. | -| image | object | `{"pullPolicy":"IfNotPresent","repository":"ghcr.io/settlemint/network-bootstrapper","tag":""}` | Container image settings for the network bootstrapper workload. See https://kubernetes.io/docs/concepts/containers/images/ for background. | -| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy controlling when Kubernetes re-fetches the image layer manifest. | -| image.repository | string | `"ghcr.io/settlemint/network-bootstrapper"` | OCI repository hosting the network bootstrapper image. | -| image.tag | string | `""` | Image tag override. Defaults to the chart's `.appVersion` when left empty. | -| imagePullSecrets | list | `[]` | Image pull secrets enabling access to private registries. See https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ for usage. | -| nameOverride | string | `""` | Short name override applied to chart-scoped resource names. | -| nodeSelector | object | `{}` | Node selector constraints for scheduling the bootstrapper pod. | -| podAnnotations | object | `{}` | Pod-level annotations merged onto the generated pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/. | -| podLabels | object | `{}` | Pod-level labels applied to the pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/. | -| podSecurityContext | object | `{}` | Pod-level security context applied to all containers in the pod. | -| rbac | object | `{"create":true}` | RBAC resources granting ConfigMap access for Kubernetes output workflows. | -| rbac.create | bool | `true` | Whether to create Role and RoleBinding objects targeting the service account. | -| resources | object | `{}` | Resource requests and limits for the bootstrapper container. | -| securityContext | object | `{}` | Container security context applied to the bootstrapper container. | -| serviceAccount | object | `{"annotations":{},"automount":true,"create":true,"name":""}` | Service account configuration for the bootstrapper pod. See https://kubernetes.io/docs/concepts/security/service-accounts/ for details. | -| serviceAccount.annotations | object | `{}` | Additional metadata annotations applied to the service account object. | -| serviceAccount.automount | bool | `true` | Automatically mount the service account token into the pod. | -| serviceAccount.create | bool | `true` | Whether to create a service account automatically. | -| serviceAccount.name | string | `""` | Existing service account name to use instead of one generated by the chart. If unset and `serviceAccount.create` is true, a name is derived from the chart fullname. | -| settings | object | `{"allocations":null,"chainId":null,"consensus":null,"contractSizeLimit":null,"evmStackSize":null,"gasLimit":null,"gasPrice":null,"outputType":"kubernetes","rpcNodes":null,"secondsPerBlock":null,"validators":null}` | Network bootstrapper CLI settings translated into command-line flags. | -| settings.allocations | string | `nil` | Filesystem path, accessible to the job, pointing to a JSON file with initial account allocations. Omit to skip pre-funded accounts. | -| settings.chainId | int | `nil` | Explicit chain ID applied to the genesis configuration. Defaults to a random value in the 40000-50000 range when omitted. | -| settings.consensus | string | `nil` | Consensus engine to configure for the network (IBFTv2 or QBFT). Default: "QBFT". | -| settings.contractSizeLimit | int | `nil` | Contract size limit in bytes enforced by the EVM. Default: 2147483647. | -| settings.evmStackSize | int | `nil` | Maximum EVM stack size allowed for contract execution. Default: 2048. | -| settings.gasLimit | int | `nil` | Genesis block gas limit value expressed in decimal. Default: 9007199254740991. | -| settings.gasPrice | int | `nil` | Base gas price in wei applied to the chain. Default: 0. | -| settings.outputType | string | `"kubernetes"` | Destination for generated artefacts: `screen` (stdout), `file` (write to volume), or `kubernetes` (persist as Kubernetes secrets/configmaps). Default: "screen". | -| settings.rpcNodes | int | `nil` | Number of RPC node definitions included in the output topology. Default: 2. | -| settings.secondsPerBlock | int | `nil` | Target block time in seconds encoded into genesis. Default: 2. | -| settings.validators | int | `nil` | Number of validator node definitions the bootstrapper generates. Default: 4. | -| tolerations | list | `[]` | Kubernetes tolerations assigned to the bootstrapper pod. | -| volumeMounts | list | `[]` | Additional volume mounts added to the bootstrapper container. | -| volumes | list | `[]` | Additional volumes injected into the deployment pod spec. | diff --git a/charts/network-bootstrapper/values.yaml b/charts/network-bootstrapper/values.yaml deleted file mode 100644 index 3d01ab0..0000000 --- a/charts/network-bootstrapper/values.yaml +++ /dev/null @@ -1,113 +0,0 @@ -# -- Container image settings for the network bootstrapper workload. See https://kubernetes.io/docs/concepts/containers/images/ for background. -image: - # -- (string) OCI repository hosting the network bootstrapper image. - repository: ghcr.io/settlemint/network-bootstrapper - # -- (string) Image pull policy controlling when Kubernetes re-fetches the image layer manifest. - pullPolicy: IfNotPresent - # -- (string) Image tag override. Defaults to the chart's `.appVersion` when left empty. - tag: "" - -# -- (list) Image pull secrets enabling access to private registries. See https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ for usage. -imagePullSecrets: [] -# -- (string) Short name override applied to chart-scoped resource names. -nameOverride: "" -# -- (string) Fully qualified name override for resources created by this release. -fullnameOverride: "" - -# -- (object) Service account configuration for the bootstrapper pod. See https://kubernetes.io/docs/concepts/security/service-accounts/ for details. -serviceAccount: - # -- (bool) Whether to create a service account automatically. - create: true - # -- (bool) Automatically mount the service account token into the pod. - automount: true - # -- (object) Additional metadata annotations applied to the service account object. - annotations: {} - # -- (string) Existing service account name to use instead of one generated by the chart. - # If unset and `serviceAccount.create` is true, a name is derived from the chart fullname. - name: "" - -# -- (object) RBAC resources granting ConfigMap access for Kubernetes output workflows. -rbac: - # -- (bool) Whether to create Role and RoleBinding objects targeting the service account. - create: true - -# -- (object) Pod-level annotations merged onto the generated pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/. -podAnnotations: {} -# -- (object) Pod-level labels applied to the pod template metadata. See https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/. -podLabels: {} - -# -- (object) Pod-level security context applied to all containers in the pod. -podSecurityContext: - {} - # fsGroup: 2000 - -# -- (object) Container security context applied to the bootstrapper container. -securityContext: - {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -# -- (object) Resource requests and limits for the bootstrapper container. -resources: - {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -# -- (list) Additional volumes injected into the deployment pod spec. -volumes: [] -# - name: foo -# secret: -# secretName: mysecret -# optional: false - -# -- (list) Additional volume mounts added to the bootstrapper container. -volumeMounts: [] -# - name: foo -# mountPath: "/etc/foo" -# readOnly: true - -# -- (object) Node selector constraints for scheduling the bootstrapper pod. -nodeSelector: {} - -# -- (list) Kubernetes tolerations assigned to the bootstrapper pod. -tolerations: [] - -# -- (object) Affinity and anti-affinity rules influencing pod placement. -affinity: {} - -# -- (object) Network bootstrapper CLI settings translated into command-line flags. -settings: - # -- (int) Number of validator node definitions the bootstrapper generates. Default: 4. - validators: - # -- (int) Number of RPC node definitions included in the output topology. Default: 2. - rpcNodes: - # -- (string) Filesystem path, accessible to the job, pointing to a JSON file with initial account allocations. Omit to skip pre-funded accounts. - allocations: - # -- (string) Destination for generated artefacts: `screen` (stdout), `file` (write to volume), or `kubernetes` (persist as Kubernetes secrets/configmaps). Default: "screen". - outputType: kubernetes - # -- (string) Consensus engine to configure for the network (IBFTv2 or QBFT). Default: "QBFT". - consensus: - # -- (int) Explicit chain ID applied to the genesis configuration. Defaults to a random value in the 40000-50000 range when omitted. - chainId: - # -- (int) Target block time in seconds encoded into genesis. Default: 2. - secondsPerBlock: - # -- (int) Genesis block gas limit value expressed in decimal. Default: 9007199254740991. - gasLimit: - # -- (int) Base gas price in wei applied to the chain. Default: 0. - gasPrice: - # -- (int) Maximum EVM stack size allowed for contract execution. Default: 2048. - evmStackSize: - # -- (int) Contract size limit in bytes enforced by the EVM. Default: 2147483647. - contractSizeLimit: diff --git a/charts/network-bootstrapper/.helmignore b/charts/network/.helmignore similarity index 100% rename from charts/network-bootstrapper/.helmignore rename to charts/network/.helmignore diff --git a/charts/network/Chart.yaml b/charts/network/Chart.yaml new file mode 100644 index 0000000..838d134 --- /dev/null +++ b/charts/network/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: network +description: A Helm chart for a blockchain network on Kubernetes +type: application +version: 0.1.0 +appVersion: "0.1.0" +maintainers: + - name: SettleMint + email: support@settlemint.com + url: https://settlemint.com +dependencies: + - name: network-bootstrapper + version: "*" + - name: network-nodes + version: "*" diff --git a/charts/network/README.md b/charts/network/README.md new file mode 100644 index 0000000..d9f1063 --- /dev/null +++ b/charts/network/README.md @@ -0,0 +1,19 @@ +# network + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) + +A Helm chart for a blockchain network on Kubernetes + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| SettleMint | | | + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| | network-bootstrapper | * | +| | network-nodes | * | + diff --git a/charts/network/charts/network-bootstrapper/.helmignore b/charts/network/charts/network-bootstrapper/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/network/charts/network-bootstrapper/.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 +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/network-bootstrapper/Chart.yaml b/charts/network/charts/network-bootstrapper/Chart.yaml similarity index 100% rename from charts/network-bootstrapper/Chart.yaml rename to charts/network/charts/network-bootstrapper/Chart.yaml diff --git a/charts/network/charts/network-bootstrapper/README.md b/charts/network/charts/network-bootstrapper/README.md new file mode 100644 index 0000000..67d1bfe --- /dev/null +++ b/charts/network/charts/network-bootstrapper/README.md @@ -0,0 +1,54 @@ +# network-bootstrapper + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) + +A Helm chart for Kubernetes + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| SettleMint | | | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| fullnameOverride | string | `"bootstrapper"` | Override for the fully qualified resource name generated by helpers. | +| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy controlling when Kubernetes fetches updated image layers. | +| image.repository | string | `"ghcr.io/settlemint/network-bootstrapper"` | OCI registry path hosting the network bootstrapper image. | +| image.tag | string | `""` | Image tag override; leave empty to inherit the chart appVersion. | +| imagePullSecrets | list | `[]` | Image pull secrets enabling access to private registries. | +| nameOverride | string | `""` | Override for the short release name used by name templates. | +| nodeSelector | object | `{}` | | +| podAnnotations | object | `{}` | | +| podLabels | object | `{}` | | +| podSecurityContext | object | `{}` | | +| rbac.create | bool | `true` | Render Role and RoleBinding resources tied to the ServiceAccount. | +| resources | object | `{}` | | +| securityContext | object | `{}` | | +| serviceAccount.annotations | object | `{}` | Metadata annotations applied to the ServiceAccount. | +| serviceAccount.automount | bool | `true` | Mount the ServiceAccount token into the bootstrapper pod. | +| serviceAccount.create | bool | `true` | Create a ServiceAccount resource for the release automatically. | +| serviceAccount.name | string | `""` | Existing ServiceAccount name to reuse instead of creating one. | +| settings.allocations | string | `nil` | Path to a JSON allocations file providing pre-funded accounts. | +| settings.chainId | int | `nil` | Explicit chain ID to encode into the genesis file. | +| settings.clusterDomain | string | `"svc.cluster.local"` | Kubernetes DNS suffix appended to generated static node hostnames when unset by the CLI. | +| settings.consensus | string | `nil` | Consensus engine to configure (QBFT or IBFTv2). | +| settings.contractSizeLimit | int | `nil` | Maximum smart-contract bytecode size accepted by the EVM. | +| settings.defaultStaticNodeDiscoveryPort | int | `30303` | Default UDP discovery port used in generated enode URIs when no override is provided. | +| settings.defaultStaticNodePort | int | `30303` | Default TCP P2P port used in generated enode URIs when no override is provided. | +| settings.evmStackSize | int | `nil` | Maximum EVM stack size permitted for contract execution. | +| settings.gasLimit | int | `nil` | Genesis block gas limit expressed as a decimal number. | +| settings.gasPrice | int | `nil` | Base gas price in wei applied across the network. | +| settings.outputType | string | `"kubernetes"` | Output target for generated artefacts: screen, file, or kubernetes. | +| settings.secondsPerBlock | int | `nil` | Block interval in seconds for the genesis configuration. | +| settings.staticNodeDiscoveryPort | int | `nil` | UDP discovery port embedded in static node enode URIs. | +| settings.staticNodeDomain | string | `nil` | DNS suffix appended to generated static node hostnames. | +| settings.staticNodeNamespace | string | `nil` | Namespace component inserted between service name and domain in static node hostnames. | +| settings.staticNodePort | int | `nil` | TCP P2P port embedded in static node enode URIs. | +| settings.validators | int | `nil` | Number of validator identities to generate (default 4). | +| tolerations | list | `[]` | Tolerations allowing the bootstrapper pod onto tainted nodes. | +| volumeMounts | list | `[]` | Extra volume mounts applied to the bootstrapper container. | +| volumes | list | `[]` | Additional volumes attached to the bootstrapper pod. | diff --git a/charts/network-bootstrapper/templates/NOTES.txt b/charts/network/charts/network-bootstrapper/templates/NOTES.txt similarity index 100% rename from charts/network-bootstrapper/templates/NOTES.txt rename to charts/network/charts/network-bootstrapper/templates/NOTES.txt diff --git a/charts/network-bootstrapper/templates/_helpers.tpl b/charts/network/charts/network-bootstrapper/templates/_helpers.tpl similarity index 100% rename from charts/network-bootstrapper/templates/_helpers.tpl rename to charts/network/charts/network-bootstrapper/templates/_helpers.tpl diff --git a/charts/network-bootstrapper/templates/job.yaml b/charts/network/charts/network-bootstrapper/templates/job.yaml similarity index 75% rename from charts/network-bootstrapper/templates/job.yaml rename to charts/network/charts/network-bootstrapper/templates/job.yaml index 19486f0..e2bf78b 100644 --- a/charts/network-bootstrapper/templates/job.yaml +++ b/charts/network/charts/network-bootstrapper/templates/job.yaml @@ -38,12 +38,22 @@ spec: image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} args: + - generate + {{- $clusterDomain := default "svc.cluster.local" .Values.settings.clusterDomain }} + {{- $defaultStaticPort := default 30303 .Values.settings.defaultStaticNodePort }} + {{- $defaultStaticDiscovery := default 30303 .Values.settings.defaultStaticNodeDiscoveryPort }} + {{- $resolvedStaticDomain := default $clusterDomain .Values.settings.staticNodeDomain }} + {{- $resolvedStaticPort := default $defaultStaticPort .Values.settings.staticNodePort }} + {{- $resolvedStaticDiscovery := default $defaultStaticDiscovery .Values.settings.staticNodeDiscoveryPort }} {{- with .Values.settings.validators }} - --validators={{ . }} {{- end }} - {{- with .Values.settings.rpcNodes }} - - --rpc-nodes={{ . }} + - --static-node-namespace={{ default .Release.Namespace .Values.settings.staticNodeNamespace }} + {{- if $resolvedStaticDomain }} + - --static-node-domain={{ $resolvedStaticDomain }} {{- end }} + - --static-node-port={{ $resolvedStaticPort }} + - --static-node-discovery-port={{ $resolvedStaticDiscovery }} {{- with .Values.settings.allocations }} - --allocations={{ . }} {{- end }} diff --git a/charts/network-bootstrapper/templates/role.yaml b/charts/network/charts/network-bootstrapper/templates/role.yaml similarity index 100% rename from charts/network-bootstrapper/templates/role.yaml rename to charts/network/charts/network-bootstrapper/templates/role.yaml diff --git a/charts/network-bootstrapper/templates/rolebinding.yaml b/charts/network/charts/network-bootstrapper/templates/rolebinding.yaml similarity index 100% rename from charts/network-bootstrapper/templates/rolebinding.yaml rename to charts/network/charts/network-bootstrapper/templates/rolebinding.yaml diff --git a/charts/network-bootstrapper/templates/serviceaccount.yaml b/charts/network/charts/network-bootstrapper/templates/serviceaccount.yaml similarity index 100% rename from charts/network-bootstrapper/templates/serviceaccount.yaml rename to charts/network/charts/network-bootstrapper/templates/serviceaccount.yaml diff --git a/charts/network/charts/network-bootstrapper/values.yaml b/charts/network/charts/network-bootstrapper/values.yaml new file mode 100644 index 0000000..65bd8ff --- /dev/null +++ b/charts/network/charts/network-bootstrapper/values.yaml @@ -0,0 +1,123 @@ +# Default configuration values for the network-bootstrapper subchart. Each key is annotated for helm-docs output. +image: + # -- (string) OCI registry path hosting the network bootstrapper image. + repository: ghcr.io/settlemint/network-bootstrapper + # -- (string) Image pull policy controlling when Kubernetes fetches updated image layers. + pullPolicy: IfNotPresent + # -- (string) Image tag override; leave empty to inherit the chart appVersion. + tag: "" + +# -- (list) Image pull secrets enabling access to private registries. +imagePullSecrets: [] + +# -- (string) Override for the short release name used by name templates. +nameOverride: "" + +# -- (string) Override for the fully qualified resource name generated by helpers. +fullnameOverride: "bootstrapper" + +# ServiceAccount configuration associated with the bootstrapper job. +serviceAccount: + # -- (bool) Create a ServiceAccount resource for the release automatically. + create: true + # -- (bool) Mount the ServiceAccount token into the bootstrapper pod. + automount: true + # -- (object) Metadata annotations applied to the ServiceAccount. + annotations: {} + # -- (string) Existing ServiceAccount name to reuse instead of creating one. + name: "" + +# RBAC settings that provision Role and RoleBinding for Kubernetes output. +rbac: + # -- (bool) Render Role and RoleBinding resources tied to the ServiceAccount. + create: true + +# Additional annotations merged onto the bootstrapper pod template. +podAnnotations: {} + +# Extra labels merged onto the bootstrapper pod template. +podLabels: {} + +# Pod-level security context applied to all containers in the pod. +podSecurityContext: + {} + # fsGroup: 2000 + +# Container security context applied to the bootstrapper container. +securityContext: + {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# CPU and memory requests or limits for the bootstrapper container. +resources: + {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# -- (list) Additional volumes attached to the bootstrapper pod. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# -- (list) Extra volume mounts applied to the bootstrapper container. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +# Node selector constraints for scheduling the bootstrapper pod. +nodeSelector: {} + +# -- (list) Tolerations allowing the bootstrapper pod onto tainted nodes. +tolerations: [] + +# Affinity and anti-affinity rules influencing pod placement. +affinity: {} + +# Settings passed to the network-bootstrapper CLI executed by the job. +settings: + # -- (string) Kubernetes DNS suffix appended to generated static node hostnames when unset by the CLI. + clusterDomain: svc.cluster.local + # -- (int) Default TCP P2P port used in generated enode URIs when no override is provided. + defaultStaticNodePort: 30303 + # -- (int) Default UDP discovery port used in generated enode URIs when no override is provided. + defaultStaticNodeDiscoveryPort: 30303 + # -- (int) Number of validator identities to generate (default 4). + validators: + # -- (string) DNS suffix appended to generated static node hostnames. + staticNodeDomain: + # -- (string) Namespace component inserted between service name and domain in static node hostnames. + staticNodeNamespace: + # -- (int) TCP P2P port embedded in static node enode URIs. + staticNodePort: + # -- (int) UDP discovery port embedded in static node enode URIs. + staticNodeDiscoveryPort: + # -- (string) Path to a JSON allocations file providing pre-funded accounts. + allocations: + # -- (string) Output target for generated artefacts: screen, file, or kubernetes. + outputType: kubernetes + # -- (string) Consensus engine to configure (QBFT or IBFTv2). + consensus: + # -- (int) Explicit chain ID to encode into the genesis file. + chainId: + # -- (int) Block interval in seconds for the genesis configuration. + secondsPerBlock: + # -- (int) Genesis block gas limit expressed as a decimal number. + gasLimit: + # -- (int) Base gas price in wei applied across the network. + gasPrice: + # -- (int) Maximum EVM stack size permitted for contract execution. + evmStackSize: + # -- (int) Maximum smart-contract bytecode size accepted by the EVM. + contractSizeLimit: diff --git a/charts/network/charts/network-nodes/.helmignore b/charts/network/charts/network-nodes/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/network/charts/network-nodes/.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 +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/network/charts/network-nodes/Chart.yaml b/charts/network/charts/network-nodes/Chart.yaml new file mode 100644 index 0000000..b34678a --- /dev/null +++ b/charts/network/charts/network-nodes/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: network-nodes +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "0.1.0" +maintainers: + - name: SettleMint + email: support@settlemint.com + url: https://settlemint.com diff --git a/charts/network/charts/network-nodes/README.md b/charts/network/charts/network-nodes/README.md new file mode 100644 index 0000000..2d33913 --- /dev/null +++ b/charts/network/charts/network-nodes/README.md @@ -0,0 +1,140 @@ +# network-nodes + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) + +A Helm chart for Kubernetes + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| SettleMint | | | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| config.bonsaiLimitTrieLogsEnabled | bool | `false` | Emit Bonsai limit trie logs for debugging state transitions. | +| config.cacheLastBlocks | int | `1024` | Number of recent blocks cached in memory. | +| config.dataStorageFormat | string | `"FOREST"` | Ledger storage backend (FOREST or BONSAI). | +| config.graphql.corsOrigins | list | `["all"]` | Allowed CORS origins for GraphQL requests. | +| config.graphql.enabled | bool | `true` | Enable the GraphQL API server. | +| config.graphql.host | string | `"0.0.0.0"` | Network interface for the GraphQL server. | +| config.hostAllowlist | list | `["*"]` | Hostnames allowed to access the RPC interfaces; `*` permits all. | +| config.http.api | list | `["DEBUG","ETH","ADMIN","WEB3","IBFT","NET","TRACE","QBFT","PERM","TXPOOL","PLUGINS"]` | Enabled JSON-RPC API namespaces exposed over HTTP. | +| config.http.authenticationEnabled | bool | `false` | Enable JWT authentication for HTTP JSON-RPC requests. | +| config.http.corsOrigins | list | `["all"]` | Allowed CORS origins for HTTP JSON-RPC requests. | +| config.http.enabled | bool | `true` | Enable the HTTP JSON-RPC listener. | +| config.http.host | string | `"0.0.0.0"` | Network interface for the HTTP JSON-RPC listener. | +| config.http.maxActiveConnections | int | `2000` | Maximum concurrent HTTP JSON-RPC connections. | +| config.http.maxBatchSize | int | `512` | Maximum number of batched JSON-RPC calls per request. | +| config.http.maxRequestContentLength | int | `524288000` | Maximum HTTP request body size in bytes. | +| config.logging | string | `"INFO"` | Log verbosity level for Besu components. | +| config.metrics.categories | list | `["BLOCKCHAIN","ETHEREUM","EXECUTORS","JVM","NETWORK","PEERS","PROCESS","PRUNER","RPC","SYNCHRONIZER","TRANSACTION_POOL"]` | Metrics categories exposed to Prometheus. | +| config.metrics.enabled | bool | `true` | Enable the Prometheus metrics endpoint. | +| config.metrics.host | string | `"0.0.0.0"` | Network interface for the metrics endpoint. | +| config.minGasPrice | int | `0` | Minimum gas price accepted for transactions (wei). | +| config.p2p.discoveryEnabled | bool | `true` | Enable the discovery protocol for automatic peer finding. | +| config.p2p.enabled | bool | `true` | Enable the devp2p networking subsystem. | +| config.p2p.interface | string | `"0.0.0.0"` | Network interface the P2P server binds to. | +| config.p2p.maxPeers | int | `25` | Maximum simultaneous peer connections. | +| config.p2p.staticNodesFile | string | `"/etc/besu/static-nodes.json"` | Path to the static-nodes.json file providing fixed peers. | +| config.privateKeyFilename | string | `"privateKey"` | Filename containing each node's private key within mounted secrets. | +| config.randomPeerPriorityEnabled | bool | `true` | Randomise peer priority to avoid deterministic ordering. | +| config.receiptCompactionEnabled | bool | `true` | Enable receipt compaction to reduce disk usage. | +| config.remoteConnectionsLimitEnabled | bool | `false` | Enable limits on concurrent remote JSON-RPC connections. | +| config.revertReasonEnabled | bool | `true` | Include revert reasons in RPC responses when failures occur. | +| config.rpc.txFeecap | int | `0` | Maximum fee per gas accepted for transactions submitted via RPC (0 disables the cap). | +| config.sync.minPeers | int | `1` | Minimum number of peers required before synchronisation starts. | +| config.sync.mode | string | `"FULL"` | Synchronisation mode (FULL, FAST, SNAP, etc.). | +| config.txPool.enableSaveRestore | bool | `true` | Persist the transaction pool to disk between restarts. | +| config.txPool.limitByAccountPercentage | int | `1` | Maximum percentage of the pool allowed per account. | +| config.txPool.maxSize | int | `100000` | Maximum transactions retained in the pool. | +| config.txPool.noLocalPriority | bool | `true` | Disable preferential treatment of locally submitted transactions. | +| config.txPool.type | string | `"SEQUENCED"` | Transaction ordering strategy (e.g., SEQUENCED). | +| config.ws.api | list | `["DEBUG","ETH","ADMIN","WEB3","IBFT","NET","TRACE","QBFT","PERM","TXPOOL","PLUGINS"]` | Enabled JSON-RPC API namespaces exposed over WebSockets. | +| config.ws.authenticationEnabled | bool | `false` | Enable JWT authentication for WebSocket requests. | +| config.ws.enabled | bool | `true` | Enable the WebSocket JSON-RPC listener. | +| config.ws.host | string | `"0.0.0.0"` | Network interface for the WebSocket listener. | +| config.ws.maxActiveConnections | int | `2000` | Maximum concurrent WebSocket connections. | +| config.ws.maxFrameSize | int | `2097152` | Maximum WebSocket frame size in bytes. | +| fullnameOverride | string | `"besu-node"` | Override for the fully qualified release name used in resource naming. | +| httpRoute.annotations | object | `{}` | | +| httpRoute.enabled | bool | `false` | Enable rendering of an HTTPRoute resource. | +| httpRoute.hostnames | list | `["chart-example.local"]` | HTTP hostnames matched by the route. | +| httpRoute.parentRefs | list | `[{"name":"gateway","sectionName":"http"}]` | Gateway references that should accept this route. | +| httpRoute.rules | list | `[{"matches":[{"path":{"type":"PathPrefix","value":"/headers"}}]}]` | Rules containing matches and optional filters evaluated by the Gateway. | +| httpRoute.rules[0].matches | list | `[{"path":{"type":"PathPrefix","value":"/headers"}}]` | Match conditions evaluated for each request. | +| httpRoute.rules[0].matches[0].path.type | string | `"PathPrefix"` | Path match type (Exact, PathPrefix, or RegularExpression). | +| httpRoute.rules[0].matches[0].path.value | string | `"/headers"` | Path value used when evaluating the request URL. | +| image.pullPolicy | string | `"IfNotPresent"` | Kubernetes image pull policy for Besu containers. | +| image.repository | string | `"docker.io/hyperledger/besu"` | OCI image repository hosting Hyperledger Besu. | +| image.tag | string | `"25.8.0"` | Specific Besu image tag to deploy. | +| imagePullSecrets | list | `[]` | Image pull secrets granting registry access for the Besu image. | +| ingress.annotations | object | `{}` | | +| ingress.className | string | `""` | ingressClassName assigned to the Ingress for controller selection. | +| ingress.enabled | bool | `false` | Enable creation of an Ingress resource. | +| ingress.hosts | list | `[{"host":"chart-example.local","paths":[{"path":"/","pathType":"ImplementationSpecific"}]}]` | Hostname and path routing rules for the Ingress. | +| ingress.tls | list | `[]` | TLS configuration for Ingress hosts. | +| livenessProbe.failureThreshold | int | `3` | Consecutive failures required before the container is restarted. | +| livenessProbe.httpGet.path | string | `"/liveness"` | HTTP path used for liveness probing. | +| livenessProbe.httpGet.port | string|int | `"json-rpc"` | Target container port serving the liveness endpoint. | +| livenessProbe.initialDelaySeconds | int | `30` | Seconds to wait before starting liveness checks. | +| livenessProbe.periodSeconds | int | `10` | Frequency of liveness checks in seconds. | +| livenessProbe.timeoutSeconds | int | `2` | Timeout in seconds before marking the probe as failed. | +| nameOverride | string | `""` | Override for the short chart name used in resource naming. | +| nodeSelector | object | `{}` | | +| openShiftRoute.alternateBackends | list | `[]` | Additional backend references to balance traffic across services. | +| openShiftRoute.annotations | object | `{}` | | +| openShiftRoute.enabled | bool | `false` | Enable creation of an OpenShift Route resource. | +| openShiftRoute.host | string | `""` | Desired external hostname for the Route; leave empty for automatic assignment. | +| openShiftRoute.path | string | `""` | URL path prefix handled by the Route. | +| openShiftRoute.port.targetPort | string | `"http"` | Named service port exposed through the Route. | +| openShiftRoute.tls | object|null | `nil` | TLS termination settings; set to null to disable TLS. | +| openShiftRoute.to.weight | int | `100` | Backend weight determining traffic distribution. | +| openShiftRoute.wildcardPolicy | string | `""` | Wildcard policy controlling subdomain routing (None or Subdomain). | +| persistence.accessModes | list | `["ReadWriteOnce"]` | Requested access modes for the PersistentVolumeClaim. | +| persistence.annotations | object | `{}` | | +| persistence.enabled | bool | `false` | Enable persistent volume claims for ledger data. | +| persistence.existingClaim | string | `""` | Name of an existing PersistentVolumeClaim to reuse instead of creating new PVCs. | +| persistence.mountPath | string | `"/data"` | Container path where the persistent volume is mounted. | +| persistence.readOnly | bool | `false` | Mount the volume read-only when true. | +| persistence.retention.whenDeleted | string | `"Retain"` | Behaviour of PVCs when the Helm release is deleted. | +| persistence.retention.whenScaled | string | `"Delete"` | Behaviour of PVCs when the StatefulSet scales down. | +| persistence.selector | object | `{}` | | +| persistence.size | string | `"20Gi"` | Requested storage capacity for each PersistentVolumeClaim. | +| persistence.storageClass | string | `""` | StorageClass name used for provisioning volumes; empty selects the cluster default. | +| persistence.subPath | string | `""` | Subdirectory of the volume to mount instead of the root. | +| persistence.volumeMode | string | `""` | Volume mode (Filesystem or Block) requested for the PVC. | +| persistence.volumeName | string | `"data"` | Logical name for the volume when using volumeClaimTemplates. | +| podAnnotations."prometheus.io/path" | string | `"/metrics"` | HTTP path exposing Prometheus-formatted metrics. | +| podAnnotations."prometheus.io/port" | string | `"9545"` | Container port value used by Prometheus to scrape metrics. | +| podAnnotations."prometheus.io/scheme" | string | `"http"` | HTTP scheme (http or https) used for metrics scraping. | +| podAnnotations."prometheus.io/scrape" | string | `"true"` | Enables Prometheus scraping of the Besu metrics endpoint. | +| podLabels | object | `{}` | | +| podSecurityContext | object | `{}` | | +| readinessProbe.failureThreshold | int | `3` | Consecutive failures required before the pod is considered unready. | +| readinessProbe.httpGet.path | string | `"/readiness?minPeers=0&maxBlocksBehind=100"` | HTTP path used for readiness probing, including peer/sync thresholds. | +| readinessProbe.httpGet.port | string|int | `"json-rpc"` | Target container port serving the readiness endpoint. | +| readinessProbe.initialDelaySeconds | int | `15` | Seconds to wait before starting readiness checks. | +| readinessProbe.periodSeconds | int | `10` | Frequency of readiness checks in seconds. | +| readinessProbe.timeoutSeconds | int | `2` | Timeout in seconds before marking the probe as failed. | +| resources | object | `{}` | | +| rpcReplicaCount | int | `2` | Number of RPC node replicas provisioned via StatefulSet. | +| securityContext | object | `{}` | | +| service.ports.discovery | int | `30303` | Discovery UDP port used by devp2p. | +| service.ports.graphql | int | `8547` | GraphQL API port. | +| service.ports.metrics | int | `9545` | Prometheus metrics port. | +| service.ports.rlpx | int | `30303` | RLPx TCP peer-to-peer port. | +| service.ports.rpc | int | `8545` | HTTP JSON-RPC port. | +| service.ports.ws | int | `8546` | WebSocket JSON-RPC port. | +| service.type | string | `"ClusterIP"` | Service type controlling how endpoints are published (ClusterIP, NodePort, etc.). | +| serviceAccount.annotations | object | `{}` | Metadata annotations applied to the ServiceAccount. | +| serviceAccount.automount | bool | `true` | Mount the ServiceAccount token volume into pods. | +| serviceAccount.create | bool | `true` | Create a ServiceAccount resource automatically for the release. | +| serviceAccount.name | string | `""` | Existing ServiceAccount name to reuse when creation is disabled. | +| tolerations | list | `[]` | Tolerations allowing pods to run on nodes with matching taints. | +| validatorReplicaCount | int | `4` | Number of validator node replicas participating in consensus. | +| volumeMounts | list | `[]` | Additional volume mounts applied to Besu containers. | +| volumes | list | `[]` | Extra volumes attached to Besu pods for custom configuration or secrets. | diff --git a/charts/network/charts/network-nodes/templates/NOTES.txt b/charts/network/charts/network-nodes/templates/NOTES.txt new file mode 100644 index 0000000..e0fbe58 --- /dev/null +++ b/charts/network/charts/network-nodes/templates/NOTES.txt @@ -0,0 +1,30 @@ +{{- $namespace := .Release.Namespace -}} +{{- $fullname := include "nodes.fullname" . -}} +Thank you for deploying the {{ .Chart.Name }} chart! + +This release creates two StatefulSets (validators and RPC nodes) plus matching Services to expose Besu endpoints. + +Pods and Services +----------------- +List the pods created by this release: + + kubectl -n {{ $namespace }} get pods -l app.kubernetes.io/instance={{ .Release.Name }} + +Validators publish peer-to-peer interfaces via `svc/{{ $fullname }}` while RPC nodes expose JSON-RPC via `svc/{{ $fullname }}-rpc`. + +Accessing RPC Endpoints +----------------------- +Port-forward the RPC service to access JSON-RPC locally: + + kubectl -n {{ $namespace }} port-forward svc/{{ $fullname }}-rpc 8545:{{ .Values.service.ports.rpc }} + +Optional: forward WebSockets or GraphQL by selecting the corresponding service port names (`ws`, `graphql`). + +Observability +------------- +Metrics are available on the `metrics` port of each service. Example scrape using `curl`: + + kubectl -n {{ $namespace }} port-forward svc/{{ $fullname }} 9545:{{ .Values.service.ports.metrics }} + curl -s http://127.0.0.1:9545/metrics + +When finished, cancel any port-forward sessions with Ctrl+C. diff --git a/charts/network/charts/network-nodes/templates/_helpers.tpl b/charts/network/charts/network-nodes/templates/_helpers.tpl new file mode 100644 index 0000000..1113259 --- /dev/null +++ b/charts/network/charts/network-nodes/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nodes.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 "nodes.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nodes.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nodes.labels" -}} +helm.sh/chart: {{ include "nodes.chart" . }} +{{ include "nodes.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nodes.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nodes.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "nodes.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "nodes.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/network/charts/network-nodes/templates/configmap.yaml b/charts/network/charts/network-nodes/templates/configmap.yaml new file mode 100644 index 0000000..1369dce --- /dev/null +++ b/charts/network/charts/network-nodes/templates/configmap.yaml @@ -0,0 +1,63 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "nodes.fullname" . }}-config + labels: + {{- include "nodes.labels" . | nindent 4 }} +{{- $persistence := .Values.persistence | default (dict) }} +{{- $persistenceEnabled := default false (get $persistence "enabled") }} +{{- $mountPath := default "" (get $persistence "mountPath") }} +{{- $privateKeyFilename := default "privateKey" .Values.config.privateKeyFilename }} +data: + config.toml: |- + data-path="/data" + genesis-file="/etc/besu/genesis.json" + logging={{ .Values.config.logging | quote }} + node-private-key-file={{ printf "/data/%s" $privateKeyFilename | quote }} + data-storage-format={{ .Values.config.dataStorageFormat | quote }} + bonsai-limit-trie-logs-enabled={{ .Values.config.bonsaiLimitTrieLogsEnabled }} + cache-last-blocks={{ .Values.config.cacheLastBlocks | int }} + min-gas-price={{ .Values.config.minGasPrice | int }} + tx-pool={{ .Values.config.txPool.type | quote }} + tx-pool-max-size={{ .Values.config.txPool.maxSize | int }} + tx-pool-no-local-priority={{ .Values.config.txPool.noLocalPriority }} + tx-pool-limit-by-account-percentage={{ .Values.config.txPool.limitByAccountPercentage | int }} + tx-pool-enable-save-restore={{ .Values.config.txPool.enableSaveRestore }} + p2p-enabled={{ .Values.config.p2p.enabled }} + discovery-enabled={{ .Values.config.p2p.discoveryEnabled }} + static-nodes-file={{ .Values.config.p2p.staticNodesFile | quote }} + p2p-interface={{ .Values.config.p2p.interface | quote }} + p2p-port={{ .Values.service.ports.rlpx | int }} + max-peers={{ .Values.config.p2p.maxPeers | int }} + rpc-tx-feecap={{ .Values.config.rpc.txFeecap | int }} + sync-min-peers={{ .Values.config.sync.minPeers | int }} + sync-mode={{ .Values.config.sync.mode | quote }} + remote-connections-limit-enabled={{ .Values.config.remoteConnectionsLimitEnabled }} + random-peer-priority-enabled={{ .Values.config.randomPeerPriorityEnabled }} + receipt-compaction-enabled={{ .Values.config.receiptCompactionEnabled }} + host-allowlist={{ .Values.config.hostAllowlist | toJson }} + rpc-http-enabled={{ .Values.config.http.enabled }} + rpc-http-host={{ .Values.config.http.host | quote }} + rpc-http-port={{ .Values.service.ports.rpc | int }} + rpc-http-api={{ .Values.config.http.api | toJson }} + rpc-http-cors-origins={{ .Values.config.http.corsOrigins | toJson }} + rpc-http-authentication-enabled={{ .Values.config.http.authenticationEnabled }} + revert-reason-enabled={{ .Values.config.revertReasonEnabled }} + rpc-http-max-active-connections={{ .Values.config.http.maxActiveConnections | int }} + rpc-ws-max-active-connections={{ .Values.config.ws.maxActiveConnections | int }} + rpc-http-max-request-content-length={{ .Values.config.http.maxRequestContentLength | int }} + rpc-ws-max-frame-size={{ .Values.config.ws.maxFrameSize | int }} + rpc-http-max-batch-size={{ .Values.config.http.maxBatchSize | int }} + graphql-http-enabled={{ .Values.config.graphql.enabled }} + graphql-http-host={{ .Values.config.graphql.host | quote }} + graphql-http-port={{ .Values.service.ports.graphql | int }} + graphql-http-cors-origins={{ .Values.config.graphql.corsOrigins | toJson }} + rpc-ws-enabled={{ .Values.config.ws.enabled }} + rpc-ws-host={{ .Values.config.ws.host | quote }} + rpc-ws-port={{ .Values.service.ports.ws | int }} + rpc-ws-api={{ .Values.config.ws.api | toJson }} + rpc-ws-authentication-enabled={{ .Values.config.ws.authenticationEnabled }} + metrics-enabled={{ .Values.config.metrics.enabled }} + metrics-host={{ .Values.config.metrics.host | quote }} + metrics-port={{ .Values.service.ports.metrics | int }} + metrics-category={{ .Values.config.metrics.categories | toJson }} diff --git a/charts/network/charts/network-nodes/templates/httproute.yaml b/charts/network/charts/network-nodes/templates/httproute.yaml new file mode 100644 index 0000000..5957f1d --- /dev/null +++ b/charts/network/charts/network-nodes/templates/httproute.yaml @@ -0,0 +1,38 @@ +{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include "nodes.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + labels: + {{- include "nodes.labels" . | nindent 4 }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- with .Values.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.httpRoute.rules }} + {{- with .matches }} + - matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + weight: 1 + {{- end }} +{{- end }} diff --git a/charts/network/charts/network-nodes/templates/ingress.yaml b/charts/network/charts/network-nodes/templates/ingress.yaml new file mode 100644 index 0000000..56f3f44 --- /dev/null +++ b/charts/network/charts/network-nodes/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "nodes.fullname" . }} + labels: + {{- include "nodes.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "nodes.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/network/charts/network-nodes/templates/route.yaml b/charts/network/charts/network-nodes/templates/route.yaml new file mode 100644 index 0000000..969c71d --- /dev/null +++ b/charts/network/charts/network-nodes/templates/route.yaml @@ -0,0 +1,44 @@ +{{- /* OpenShift Route for exposing the node service when enabled. */ -}} +{{- if .Values.openShiftRoute.enabled -}} +{{- $fullName := include "nodes.fullname" . -}} +{{- $routeValues := .Values.openShiftRoute -}} +{{- $targetPort := default "http" (dig "port" "targetPort" $routeValues) -}} +{{- $weight := default 100 (dig "to" "weight" $routeValues) -}} +{{- $wildcard := default "None" $routeValues.wildcardPolicy -}} +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: {{ $fullName }} + labels: + {{- include "nodes.labels" . | nindent 4 }} + {{- with $routeValues.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + to: + kind: Service + name: {{ $fullName }} + {{- if ne $weight 100 }} + weight: {{ $weight }} + {{- end }} + {{- with $routeValues.alternateBackends }} + alternateBackends: + {{- toYaml . | nindent 4 }} + {{- end }} + port: + targetPort: {{ $targetPort }} + {{- with $routeValues.host }} + host: {{ . | quote }} + {{- end }} + {{- with $routeValues.path }} + path: {{ . | quote }} + {{- end }} + {{- if $wildcard }} + wildcardPolicy: {{ $wildcard }} + {{- end }} + {{- with $routeValues.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/network/charts/network-nodes/templates/service-rpc.yaml b/charts/network/charts/network-nodes/templates/service-rpc.yaml new file mode 100644 index 0000000..7f1c145 --- /dev/null +++ b/charts/network/charts/network-nodes/templates/service-rpc.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nodes.fullname" . }}-rpc + labels: + {{- include "nodes.labels" . | nindent 4 }} + app.kubernetes.io/component: rpc +spec: + type: {{ .Values.service.type }} + ports: + - name: json-rpc + port: {{ .Values.service.ports.rpc }} + targetPort: json-rpc + protocol: TCP + - name: ws + port: {{ .Values.service.ports.ws }} + targetPort: ws + protocol: TCP + - name: graphql + port: {{ .Values.service.ports.graphql }} + targetPort: graphql + protocol: TCP + - name: rlpx + port: {{ .Values.service.ports.rlpx }} + targetPort: rlpx + protocol: TCP + - name: discovery + port: {{ .Values.service.ports.discovery }} + targetPort: discovery + protocol: UDP + - name: metrics + port: {{ .Values.service.ports.metrics }} + targetPort: metrics + protocol: TCP + selector: + {{- include "nodes.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: rpc diff --git a/charts/network/charts/network-nodes/templates/service.yaml b/charts/network/charts/network-nodes/templates/service.yaml new file mode 100644 index 0000000..20e1a74 --- /dev/null +++ b/charts/network/charts/network-nodes/templates/service.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nodes.fullname" . }} + labels: + {{- include "nodes.labels" . | nindent 4 }} + app.kubernetes.io/component: validator +spec: + type: {{ .Values.service.type }} + ports: + - name: json-rpc + port: {{ .Values.service.ports.rpc }} + targetPort: json-rpc + protocol: TCP + - name: ws + port: {{ .Values.service.ports.ws }} + targetPort: ws + protocol: TCP + - name: graphql + port: {{ .Values.service.ports.graphql }} + targetPort: graphql + protocol: TCP + - name: rlpx + port: {{ .Values.service.ports.rlpx }} + targetPort: rlpx + protocol: TCP + - name: discovery + port: {{ .Values.service.ports.discovery }} + targetPort: discovery + protocol: UDP + - name: metrics + port: {{ .Values.service.ports.metrics }} + targetPort: metrics + protocol: TCP + selector: + {{- include "nodes.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: validator diff --git a/charts/network/charts/network-nodes/templates/serviceaccount.yaml b/charts/network/charts/network-nodes/templates/serviceaccount.yaml new file mode 100644 index 0000000..46bb324 --- /dev/null +++ b/charts/network/charts/network-nodes/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "nodes.serviceAccountName" . }} + labels: + {{- include "nodes.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/network/charts/network-nodes/templates/statefulset-rpc.yaml b/charts/network/charts/network-nodes/templates/statefulset-rpc.yaml new file mode 100644 index 0000000..287cd84 --- /dev/null +++ b/charts/network/charts/network-nodes/templates/statefulset-rpc.yaml @@ -0,0 +1,214 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "nodes.fullname" . }}-rpc + labels: + {{- include "nodes.labels" . | nindent 4 }} + app.kubernetes.io/component: rpc +spec: + {{- $root := . }} + {{- $persistence := .Values.persistence | default (dict) }} + {{- $persistenceEnabled := default false (get $persistence "enabled") }} + {{- $existingClaim := default "" (get $persistence "existingClaim") }} + {{- $volumeName := default "data" (get $persistence "volumeName") }} + {{- $mountPath := default "" (get $persistence "mountPath") }} + {{- $subPath := get $persistence "subPath" }} + {{- $readOnly := default false (get $persistence "readOnly") }} + {{- $storageClass := get $persistence "storageClass" }} + {{- $size := default "" (get $persistence "size") }} + {{- $accessModes := default (list "ReadWriteOnce") (get $persistence "accessModes") }} + {{- $volumeMode := get $persistence "volumeMode" }} + {{- $claimAnnotations := default (dict) (get $persistence "annotations") }} + {{- $claimSelector := default (dict) (get $persistence "selector") }} + {{- $retention := default (dict) (get $persistence "retention") }} + {{- $whenDeleted := default "" (get $retention "whenDeleted") }} + {{- $whenScaled := default "" (get $retention "whenScaled") }} + {{- $useExistingClaim := and $persistenceEnabled (ne $existingClaim "") }} + {{- $useClaimTemplate := and $persistenceEnabled (not $useExistingClaim) }} + {{- $privateKeyFilename := default "privateKey" .Values.config.privateKeyFilename }} + {{- $shouldMountPersistence := $persistenceEnabled }} + {{- $rpcReplicaBudget := .Values.rpcReplicaCount | int }} + podManagementPolicy: Parallel + replicas: {{ .Values.rpcReplicaCount }} + serviceName: {{ include "nodes.fullname" . }}-rpc + {{- if and $useClaimTemplate (or (ne $whenDeleted "") (ne $whenScaled "")) }} + persistentVolumeClaimRetentionPolicy: + {{- if ne $whenDeleted "" }} + whenDeleted: {{ $whenDeleted }} + {{- end }} + {{- if ne $whenScaled "" }} + whenScaled: {{ $whenScaled }} + {{- end }} + {{- end }} + selector: + matchLabels: + {{- include "nodes.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: rpc + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "nodes.labels" . | nindent 8 }} + app.kubernetes.io/component: rpc + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "nodes.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/sh + - -c + # Advertise the pod IP so peers see a reachable address. + args: + - | + exec + /opt/besu/bin/besu \ + --config-file=/etc/besu/config.toml \ + --Xdns-enabled=true \ + --Xdns-update-enabled=true \ + --min-gas-price={{ .Values.config.minGasPrice }} \ + --p2p-host=${POD_IP} + ports: + - name: json-rpc + containerPort: {{ .Values.service.ports.rpc }} + protocol: TCP + - name: ws + containerPort: {{ .Values.service.ports.ws }} + protocol: TCP + - name: graphql + containerPort: {{ .Values.service.ports.graphql }} + protocol: TCP + - name: rlpx + containerPort: {{ .Values.service.ports.rlpx }} + protocol: TCP + - name: discovery + containerPort: {{ .Values.service.ports.discovery }} + protocol: UDP + - name: metrics + containerPort: {{ .Values.service.ports.metrics }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- $extraMounts := .Values.volumeMounts | default (list) }} + volumeMounts: + - name: besu-config + mountPath: /etc/besu/config.toml + subPath: config.toml + readOnly: true + - name: besu-genesis + mountPath: /etc/besu/genesis.json + subPath: genesis.json + readOnly: true + - name: besu-static-nodes + mountPath: /etc/besu/static-nodes.json + subPath: static-nodes.json + readOnly: true + {{- if $shouldMountPersistence }} + - name: {{ $volumeName }} + mountPath: /data + {{- if $subPath }} + subPath: {{ $subPath }} + {{- end }} + {{- if $readOnly }} + readOnly: true + {{- end }} + {{- end }} + {{- with $extraMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- $extraVolumes := .Values.volumes | default (list) }} + volumes: + - name: besu-config + configMap: + name: {{ include "nodes.fullname" . }}-config + - name: besu-genesis + configMap: + name: besu-genesis + - name: besu-static-nodes + configMap: + name: besu-static-nodes + {{- if $useExistingClaim }} + - name: {{ $volumeName }} + persistentVolumeClaim: + claimName: {{ $existingClaim }} + {{- end }} + {{- with $extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if $useClaimTemplate }} + volumeClaimTemplates: + - metadata: + name: {{ $volumeName }} + {{- if gt (len $claimAnnotations) 0 }} + annotations: + {{- toYaml $claimAnnotations | nindent 10 }} + {{- end }} + spec: + accessModes: + {{- range $accessModes }} + - {{ . }} + {{- end }} + {{- if $storageClass }} + storageClassName: {{ $storageClass | quote }} + {{- end }} + {{- if $volumeMode }} + volumeMode: {{ $volumeMode }} + {{- end }} + resources: + requests: + storage: {{ required "persistence.size must be set when persistence.enabled is true and persistence.existingClaim is not defined" $size }} + {{- if gt (len $claimSelector) 0 }} + selector: + {{- toYaml $claimSelector | nindent 10 }} + {{- end }} + {{- end }} diff --git a/charts/network/charts/network-nodes/templates/statefulset-validator.yaml b/charts/network/charts/network-nodes/templates/statefulset-validator.yaml new file mode 100644 index 0000000..a7b371a --- /dev/null +++ b/charts/network/charts/network-nodes/templates/statefulset-validator.yaml @@ -0,0 +1,231 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "nodes.fullname" . }}-validator + labels: + {{- include "nodes.labels" . | nindent 4 }} + app.kubernetes.io/component: validator +spec: + {{- $root := . }} + {{- $persistence := .Values.persistence | default (dict) }} + {{- $persistenceEnabled := default false (get $persistence "enabled") }} + {{- $existingClaim := default "" (get $persistence "existingClaim") }} + {{- $volumeName := default "data" (get $persistence "volumeName") }} + {{- $mountPath := default "" (get $persistence "mountPath") }} + {{- $subPath := get $persistence "subPath" }} + {{- $readOnly := default false (get $persistence "readOnly") }} + {{- $storageClass := get $persistence "storageClass" }} + {{- $size := default "" (get $persistence "size") }} + {{- $accessModes := default (list "ReadWriteOnce") (get $persistence "accessModes") }} + {{- $volumeMode := get $persistence "volumeMode" }} + {{- $claimAnnotations := default (dict) (get $persistence "annotations") }} + {{- $claimSelector := default (dict) (get $persistence "selector") }} + {{- $retention := default (dict) (get $persistence "retention") }} + {{- $whenDeleted := default "" (get $retention "whenDeleted") }} + {{- $whenScaled := default "" (get $retention "whenScaled") }} + {{- $useExistingClaim := and $persistenceEnabled (ne $existingClaim "") }} + {{- $useClaimTemplate := and $persistenceEnabled (not $useExistingClaim) }} + {{- $privateKeyFilename := default "privateKey" .Values.config.privateKeyFilename }} + {{- $shouldMountPersistence := $persistenceEnabled }} + {{- $validatorReplicaBudget := .Values.validatorReplicaCount | int }} + replicas: {{ .Values.validatorReplicaCount }} + serviceName: {{ include "nodes.fullname" . }} + {{- if and $useClaimTemplate (or (ne $whenDeleted "") (ne $whenScaled "")) }} + persistentVolumeClaimRetentionPolicy: + {{- if ne $whenDeleted "" }} + whenDeleted: {{ $whenDeleted }} + {{- end }} + {{- if ne $whenScaled "" }} + whenScaled: {{ $whenScaled }} + {{- end }} + {{- end }} + selector: + matchLabels: + {{- include "nodes.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: validator + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "nodes.labels" . | nindent 8 }} + app.kubernetes.io/component: validator + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "nodes.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/sh + - -c + # Advertise the pod IP so peers see a reachable address. + args: + - | + exec + /opt/besu/bin/besu \ + --config-file=/etc/besu/config.toml \ + --Xdns-enabled=true \ + --Xdns-update-enabled=true \ + --min-gas-price={{ .Values.config.minGasPrice }} \ + --p2p-host=${POD_IP} + ports: + - name: json-rpc + containerPort: {{ .Values.service.ports.rpc }} + protocol: TCP + - name: ws + containerPort: {{ .Values.service.ports.ws }} + protocol: TCP + - name: graphql + containerPort: {{ .Values.service.ports.graphql }} + protocol: TCP + - name: rlpx + containerPort: {{ .Values.service.ports.rlpx }} + protocol: TCP + - name: discovery + containerPort: {{ .Values.service.ports.discovery }} + protocol: UDP + - name: metrics + containerPort: {{ .Values.service.ports.metrics }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- $extraMounts := .Values.volumeMounts | default (list) }} + volumeMounts: + - name: besu-config + mountPath: /etc/besu/config.toml + subPath: config.toml + readOnly: true + - name: besu-genesis + mountPath: /etc/besu/genesis.json + subPath: genesis.json + readOnly: true + - name: besu-static-nodes + mountPath: /etc/besu/static-nodes.json + subPath: static-nodes.json + readOnly: true + - name: besu-private-key + mountPath: {{ printf "/data/%s" $privateKeyFilename }} + subPathExpr: "$(POD_NAME)/privateKey" + readOnly: true + {{- if $shouldMountPersistence }} + - name: {{ $volumeName }} + mountPath: /data + {{- if $subPath }} + subPath: {{ $subPath }} + {{- end }} + {{- if $readOnly }} + readOnly: true + {{- end }} + {{- end }} + {{- with $extraMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- $extraVolumes := .Values.volumes | default (list) }} + volumes: + - name: besu-config + configMap: + name: {{ include "nodes.fullname" . }}-config + - name: besu-genesis + configMap: + name: besu-genesis + - name: besu-static-nodes + configMap: + name: besu-static-nodes + - name: besu-private-key + projected: + {{- if gt $validatorReplicaBudget 0 }} + sources: + {{- range $i, $_ := until $validatorReplicaBudget }} + - secret: + name: {{ printf "besu-node-validator-%d-private-key" $i }} + items: + - key: privateKey + path: {{ printf "%s-validator-%d/privateKey" (include "nodes.fullname" $root) $i }} + {{- end }} + {{- else }} + sources: [] + {{- end }} + {{- if $useExistingClaim }} + - name: {{ $volumeName }} + persistentVolumeClaim: + claimName: {{ $existingClaim }} + {{- end }} + {{- with $extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if $useClaimTemplate }} + volumeClaimTemplates: + - metadata: + name: {{ $volumeName }} + {{- if gt (len $claimAnnotations) 0 }} + annotations: + {{- toYaml $claimAnnotations | nindent 10 }} + {{- end }} + spec: + accessModes: + {{- range $accessModes }} + - {{ . }} + {{- end }} + {{- if $storageClass }} + storageClassName: {{ $storageClass | quote }} + {{- end }} + {{- if $volumeMode }} + volumeMode: {{ $volumeMode }} + {{- end }} + resources: + requests: + storage: {{ required "persistence.size must be set when persistence.enabled is true and persistence.existingClaim is not defined" $size }} + {{- if gt (len $claimSelector) 0 }} + selector: + {{- toYaml $claimSelector | nindent 10 }} + {{- end }} + {{- end }} diff --git a/charts/network/charts/network-nodes/values.yaml b/charts/network/charts/network-nodes/values.yaml new file mode 100644 index 0000000..b3d2c0c --- /dev/null +++ b/charts/network/charts/network-nodes/values.yaml @@ -0,0 +1,418 @@ +# Default configuration values for the network-nodes subchart. All keys include helm-docs compatible annotations. + +# -- (int) Number of RPC node replicas provisioned via StatefulSet. +rpcReplicaCount: 2 + +# -- (int) Number of validator node replicas participating in consensus. +validatorReplicaCount: 4 + +# Container image configuration shared by validator and RPC pods. +image: + # -- (string) OCI image repository hosting Hyperledger Besu. + repository: docker.io/hyperledger/besu + # -- (string) Kubernetes image pull policy for Besu containers. + pullPolicy: IfNotPresent + # -- (string) Specific Besu image tag to deploy. + tag: "25.8.0" + +# -- (list) Image pull secrets granting registry access for the Besu image. +imagePullSecrets: [] + +# -- (string) Override for the short chart name used in resource naming. +nameOverride: "" + +# -- (string) Override for the fully qualified release name used in resource naming. +fullnameOverride: "besu-node" + +# Kubernetes ServiceAccount configuration applied to Besu pods. +serviceAccount: + # -- (bool) Create a ServiceAccount resource automatically for the release. + create: true + # -- (bool) Mount the ServiceAccount token volume into pods. + automount: true + # -- (object) Metadata annotations applied to the ServiceAccount. + annotations: {} + # -- (string) Existing ServiceAccount name to reuse when creation is disabled. + name: "" + +# Pod annotations merged onto all Besu pods. +podAnnotations: + # -- (string) Enables Prometheus scraping of the Besu metrics endpoint. + prometheus.io/scrape: "true" + # -- (string) HTTP path exposing Prometheus-formatted metrics. + prometheus.io/path: /metrics + # -- (string) Container port value used by Prometheus to scrape metrics. + prometheus.io/port: "9545" + # -- (string) HTTP scheme (http or https) used for metrics scraping. + prometheus.io/scheme: http + +# Additional labels applied to all Besu pods. +podLabels: {} + +# Pod-level security context shared by all containers. +podSecurityContext: + {} + # fsGroup: 2000 + +# Container-level security context applied to Besu containers. +securityContext: + {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# Kubernetes Service definition exposing Besu endpoints. +service: + # -- (string) Service type controlling how endpoints are published (ClusterIP, NodePort, etc.). + type: ClusterIP + # Named Service ports exposing Besu interfaces. + ports: + # -- (int) HTTP JSON-RPC port. + rpc: 8545 + # -- (int) WebSocket JSON-RPC port. + ws: 8546 + # -- (int) GraphQL API port. + graphql: 8547 + # -- (int) RLPx TCP peer-to-peer port. + rlpx: 30303 + # -- (int) Discovery UDP port used by devp2p. + discovery: 30303 + # -- (int) Prometheus metrics port. + metrics: 9545 + +# Core Besu configuration persisted as besu.conf for both validators and RPC nodes. +config: + # -- (string) Log verbosity level for Besu components. + logging: INFO + # -- (string) Ledger storage backend (FOREST or BONSAI). + dataStorageFormat: FOREST + # -- (string) Filename containing each node's private key within mounted secrets. + privateKeyFilename: privateKey + # -- (bool) Emit Bonsai limit trie logs for debugging state transitions. + bonsaiLimitTrieLogsEnabled: false + # -- (int) Number of recent blocks cached in memory. + cacheLastBlocks: 1024 + # -- (int) Minimum gas price accepted for transactions (wei). + minGasPrice: 0 + # Transaction pool configuration for ingress and prioritisation. + txPool: + # -- (string) Transaction ordering strategy (e.g., SEQUENCED). + type: SEQUENCED + # -- (int) Maximum transactions retained in the pool. + maxSize: 100000 + # -- (bool) Disable preferential treatment of locally submitted transactions. + noLocalPriority: true + # -- (int) Maximum percentage of the pool allowed per account. + limitByAccountPercentage: 1 + # -- (bool) Persist the transaction pool to disk between restarts. + enableSaveRestore: true + # Peer-to-peer networking settings. + p2p: + # -- (bool) Enable the devp2p networking subsystem. + enabled: true + # -- (bool) Enable the discovery protocol for automatic peer finding. + discoveryEnabled: true + # -- (string) Path to the static-nodes.json file providing fixed peers. + staticNodesFile: "/etc/besu/static-nodes.json" + # -- (string) Network interface the P2P server binds to. + interface: "0.0.0.0" + # -- (int) Maximum simultaneous peer connections. + maxPeers: 25 + # JSON-RPC subsystem configuration shared by HTTP and WebSocket servers. + rpc: + # -- (int) Maximum fee per gas accepted for transactions submitted via RPC (0 disables the cap). + txFeecap: 0 + # Chain synchronisation controls. + sync: + # -- (int) Minimum number of peers required before synchronisation starts. + minPeers: 1 + # -- (string) Synchronisation mode (FULL, FAST, SNAP, etc.). + mode: FULL + # -- (bool) Enable limits on concurrent remote JSON-RPC connections. + remoteConnectionsLimitEnabled: false + # -- (bool) Randomise peer priority to avoid deterministic ordering. + randomPeerPriorityEnabled: true + # -- (bool) Enable receipt compaction to reduce disk usage. + receiptCompactionEnabled: true + # -- (list) Hostnames allowed to access the RPC interfaces; `*` permits all. + hostAllowlist: + - "*" + # HTTP JSON-RPC server configuration. + http: + # -- (bool) Enable the HTTP JSON-RPC listener. + enabled: true + # -- (string) Network interface for the HTTP JSON-RPC listener. + host: "0.0.0.0" + # -- (list) Enabled JSON-RPC API namespaces exposed over HTTP. + api: + - DEBUG + - ETH + - ADMIN + - WEB3 + - IBFT + - NET + - TRACE + - QBFT + - PERM + - TXPOOL + - PLUGINS + # -- (list) Allowed CORS origins for HTTP JSON-RPC requests. + corsOrigins: + - all + # -- (bool) Enable JWT authentication for HTTP JSON-RPC requests. + authenticationEnabled: false + # -- (int) Maximum concurrent HTTP JSON-RPC connections. + maxActiveConnections: 2000 + # -- (int) Maximum HTTP request body size in bytes. + maxRequestContentLength: 524288000 + # -- (int) Maximum number of batched JSON-RPC calls per request. + maxBatchSize: 512 + # WebSocket JSON-RPC server configuration. + ws: + # -- (bool) Enable the WebSocket JSON-RPC listener. + enabled: true + # -- (string) Network interface for the WebSocket listener. + host: "0.0.0.0" + # -- (list) Enabled JSON-RPC API namespaces exposed over WebSockets. + api: + - DEBUG + - ETH + - ADMIN + - WEB3 + - IBFT + - NET + - TRACE + - QBFT + - PERM + - TXPOOL + - PLUGINS + # -- (bool) Enable JWT authentication for WebSocket requests. + authenticationEnabled: false + # -- (int) Maximum concurrent WebSocket connections. + maxActiveConnections: 2000 + # -- (int) Maximum WebSocket frame size in bytes. + maxFrameSize: 2097152 + # GraphQL server configuration. + graphql: + # -- (bool) Enable the GraphQL API server. + enabled: true + # -- (string) Network interface for the GraphQL server. + host: "0.0.0.0" + # -- (list) Allowed CORS origins for GraphQL requests. + corsOrigins: + - all + # Prometheus metrics export configuration. + metrics: + # -- (bool) Enable the Prometheus metrics endpoint. + enabled: true + # -- (string) Network interface for the metrics endpoint. + host: "0.0.0.0" + # -- (list) Metrics categories exposed to Prometheus. + categories: + - BLOCKCHAIN + - ETHEREUM + - EXECUTORS + - JVM + - NETWORK + - PEERS + - PROCESS + - PRUNER + - RPC + - SYNCHRONIZER + - TRANSACTION_POOL + # -- (bool) Include revert reasons in RPC responses when failures occur. + revertReasonEnabled: true + +# Ingress configuration used to expose RPC services via Kubernetes Ingress resources. +ingress: + # -- (bool) Enable creation of an Ingress resource. + enabled: false + # -- (string) ingressClassName assigned to the Ingress for controller selection. + className: "" + # Metadata annotations applied to the Ingress resource. + annotations: + {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + # -- (list) Hostname and path routing rules for the Ingress. + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + # -- (list) TLS configuration for Ingress hosts. + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# Gateway API HTTPRoute configuration for clusters with gateway-api controllers. +httpRoute: + # -- (bool) Enable rendering of an HTTPRoute resource. + enabled: false + # Metadata annotations applied to the HTTPRoute. + annotations: {} + # -- (list) Gateway references that should accept this route. + parentRefs: + - name: gateway + sectionName: http + # namespace: default + # -- (list) HTTP hostnames matched by the route. + hostnames: + - chart-example.local + # -- (list) Rules containing matches and optional filters evaluated by the Gateway. + rules: + - + # -- (list) Match conditions evaluated for each request. + matches: + - + # Path matching condition for the request. + path: + # -- (string) Path match type (Exact, PathPrefix, or RegularExpression). + type: PathPrefix + # -- (string) Path value used when evaluating the request URL. + value: /headers + # filters can be enabled to mutate headers or other request properties. + # - type: RequestHeaderModifier + # requestHeaderModifier: + # set: + # - name: My-Overwrite-Header + # value: this-is-the-only-value + # remove: + # - User-Agent + # Additional matches can be added as required by traffic patterns. + # - matches: + # - path: + # type: PathPrefix + # value: /echo + # headers: + # - name: version + # value: v2 + +# OpenShift Route configuration for exposing services through the OpenShift router. +openShiftRoute: + # -- (bool) Enable creation of an OpenShift Route resource. + enabled: false + # Metadata annotations applied to the Route. + annotations: {} + # -- (string) Desired external hostname for the Route; leave empty for automatic assignment. + host: "" + # -- (string) URL path prefix handled by the Route. + path: "" + # -- (string) Wildcard policy controlling subdomain routing (None or Subdomain). + wildcardPolicy: "" + # Target service port configuration for the Route. + port: + # -- (string) Named service port exposed through the Route. + targetPort: http + # Primary backend configuration for the Route. + to: + # -- (int) Backend weight determining traffic distribution. + weight: 100 + # -- (list) Additional backend references to balance traffic across services. + alternateBackends: [] + # -- (object|null) TLS termination settings; set to null to disable TLS. + tls: null + +# CPU and memory requests or limits for Besu containers. +resources: + {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# Liveness probe configuration ensuring containers restart when unhealthy. +livenessProbe: + # HTTP GET probe configuration hitting the Besu liveness endpoint. + httpGet: + # -- (string) HTTP path used for liveness probing. + path: /liveness + # -- (string|int) Target container port serving the liveness endpoint. + port: json-rpc + # -- (int) Seconds to wait before starting liveness checks. + initialDelaySeconds: 30 + # -- (int) Frequency of liveness checks in seconds. + periodSeconds: 10 + # -- (int) Timeout in seconds before marking the probe as failed. + timeoutSeconds: 2 + # -- (int) Consecutive failures required before the container is restarted. + failureThreshold: 3 + +# Readiness probe configuration signalling when pods can accept traffic. +readinessProbe: + # HTTP GET probe configuration hitting the Besu readiness endpoint. + httpGet: + # -- (string) HTTP path used for readiness probing, including peer/sync thresholds. + path: /readiness?minPeers=0&maxBlocksBehind=100 + # -- (string|int) Target container port serving the readiness endpoint. + port: json-rpc + # -- (int) Seconds to wait before starting readiness checks. + initialDelaySeconds: 15 + # -- (int) Frequency of readiness checks in seconds. + periodSeconds: 10 + # -- (int) Timeout in seconds before marking the probe as failed. + timeoutSeconds: 2 + # -- (int) Consecutive failures required before the pod is considered unready. + failureThreshold: 3 + +# Persistent storage configuration for both validator and RPC StatefulSets. +persistence: + # -- (bool) Enable persistent volume claims for ledger data. + enabled: false + # -- (string) Container path where the persistent volume is mounted. + mountPath: /data + # -- (string) Logical name for the volume when using volumeClaimTemplates. + volumeName: data + # -- (list) Requested access modes for the PersistentVolumeClaim. + accessModes: + - ReadWriteOnce + # -- (string) Requested storage capacity for each PersistentVolumeClaim. + size: 20Gi + # -- (string) StorageClass name used for provisioning volumes; empty selects the cluster default. + storageClass: "" + # -- (string) Volume mode (Filesystem or Block) requested for the PVC. + volumeMode: "" + # -- (string) Name of an existing PersistentVolumeClaim to reuse instead of creating new PVCs. + existingClaim: "" + # Metadata annotations applied to generated PVCs. + annotations: {} + # PVC selector allowing binding to specific PersistentVolumes. + selector: {} + # Retention policy controlling PVC lifecycle on delete or scale operations. + retention: + # -- (string) Behaviour of PVCs when the Helm release is deleted. + whenDeleted: Retain + # -- (string) Behaviour of PVCs when the StatefulSet scales down. + whenScaled: Delete + # -- (string) Subdirectory of the volume to mount instead of the root. + subPath: "" + # -- (bool) Mount the volume read-only when true. + readOnly: false + +# -- (list) Extra volumes attached to Besu pods for custom configuration or secrets. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# -- (list) Additional volume mounts applied to Besu containers. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +# Node selector constraints influencing where Besu pods can schedule. +nodeSelector: {} + +# -- (list) Tolerations allowing pods to run on nodes with matching taints. +tolerations: [] + +# Node and pod affinity or anti-affinity rules affecting scheduling decisions. +affinity: {} diff --git a/charts/network/templates/NOTES.txt b/charts/network/templates/NOTES.txt new file mode 100644 index 0000000..6de4dbd --- /dev/null +++ b/charts/network/templates/NOTES.txt @@ -0,0 +1,45 @@ +{{- $namespace := .Release.Namespace -}} +{{- $release := .Release.Name -}} +Thanks for installing the {{ .Chart.Name }} chart! + +This umbrella release wires together: +- the `network-bootstrapper` job that generates genesis artefacts and static node definitions, and +- the `network-nodes` StatefulSets that launch Besu validator and RPC nodes. + +Bootstrapper Workflow +--------------------- +1. Wait for the bootstrapper job to finish producing outputs: + + kubectl -n {{ $namespace }} wait --for=condition=complete job -l app.kubernetes.io/name=network-bootstrapper,app.kubernetes.io/instance={{ $release }} + +2. Stream the job logs to review the generated configuration or troubleshoot failures: + + kubectl -n {{ $namespace }} logs job -l app.kubernetes.io/name=network-bootstrapper,app.kubernetes.io/instance={{ $release }} + +3. When `settings.outputType` is `kubernetes`, fetch the generated artefacts: + + kubectl -n {{ $namespace }} get configmaps -l app.kubernetes.io/name=network-bootstrapper,app.kubernetes.io/instance={{ $release }} + kubectl -n {{ $namespace }} get secrets -l app.kubernetes.io/name=network-bootstrapper,app.kubernetes.io/instance={{ $release }} + +Besu Node Operations +-------------------- +1. Confirm the validator and RPC pods are healthy: + + kubectl -n {{ $namespace }} get pods -l app.kubernetes.io/name=besu-node,app.kubernetes.io/instance={{ $release }} + +2. Port-forward the shared validator service (P2P) or the RPC service for local access (Ctrl+C to stop): + + kubectl -n {{ $namespace }} port-forward svc/besu-node 8545:8545 + kubectl -n {{ $namespace }} port-forward svc/besu-node-rpc 8545:8545 + +3. Expose metrics locally for scraping or ad-hoc inspection: + + kubectl -n {{ $namespace }} port-forward svc/besu-node 9545:9545 + +Next Steps +---------- +- Inspect all resources created by this release: + + kubectl -n {{ $namespace }} get all -l app.kubernetes.io/instance={{ $release }} + +- Apply network policies, ingress controllers, or secrets required by your environment before opening the network to external peers. diff --git a/charts/network/templates/_helpers.tpl b/charts/network/templates/_helpers.tpl new file mode 100644 index 0000000..c8c5040 --- /dev/null +++ b/charts/network/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "network.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 "network.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "network.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "network.labels" -}} +helm.sh/chart: {{ include "network.chart" . }} +{{ include "network.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "network.selectorLabels" -}} +app.kubernetes.io/name: {{ include "network.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "network.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "network.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/network/values.yaml b/charts/network/values.yaml new file mode 100644 index 0000000..84847fe --- /dev/null +++ b/charts/network/values.yaml @@ -0,0 +1 @@ +# This parent chart does not define additional values; override subchart defaults here when packaging composite releases. diff --git a/package.json b/package.json index 3fa1a42..35c62b7 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,11 @@ "check": "ultracite check", "check:fix": "ultracite fix", "test": "bun test", - "helm": "helm upgrade --install networkbootstrapper ./charts/network-bootstrapper -n networkbootstrapper --create-namespace --timeout 15m", + "helm": "helm upgrade --install network ./charts/network -n network --create-namespace --timeout 15m", "docs:helm": "helm-docs --chart-search-root=. --skip-version-footer", "docs:cli": "cat README.tpl > README.md && echo '\n```' >> README.md && bun src/index.ts --help >> README.md && echo '```' >> README.md", - "package:pack": "helm package charts/network-bootstrapper --destination .", - "package:push:harbor": "helm push ./network-bootstrapper-*.tgz oci://harbor.settlemint.com/atk" + "package:pack": "helm package charts/network --destination .", + "package:push:harbor": "helm push ./network-*.tgz oci://harbor.settlemint.com/atk" }, "devDependencies": { "@biomejs/biome": "2.2.4", diff --git a/src/cli/build-command.test.ts b/src/cli/build-command.test.ts index 8e05a8a..149dffb 100644 --- a/src/cli/build-command.test.ts +++ b/src/cli/build-command.test.ts @@ -10,12 +10,14 @@ import type { OutputPayload, OutputType } from "./output.ts"; import { outputResult as realOutputResult } from "./output.ts"; const VALIDATOR_LABEL = "validator nodes"; -const RPC_LABEL = "RPC nodes"; const VALIDATOR_RETURN = 2; -const RPC_RETURN = 1; const GENESIS_MARKER = '"extraData": "0xextra"'; const EXPECTED_DEFAULT_VALIDATOR = 4; -const EXPECTED_DEFAULT_RPC = 2; +const DEFAULT_STATIC_NODE_PORT = 30_303; +const CUSTOM_STATIC_NODE_PORT = 40_000; +const LEADING_DOT_REGEX = /^\./u; +const UNCOMPRESSED_PUBLIC_KEY_PREFIX = "04"; +const UNCOMPRESSED_PUBLIC_KEY_LENGTH = 130; const HEX_RADIX = 16; const PAD_WIDTH = 2; const PAD_CHAR = "0"; @@ -24,8 +26,7 @@ const PRIVATE_KEY_REPEAT = 32; const PUBLIC_KEY_REPEAT = 64; const FIRST_VALIDATOR_INDEX = 1; const SECOND_VALIDATOR_INDEX = 2; -const FAUCET_INDEX = 4; -const CLI_FAUCET_INDEX = 3; +const FAUCET_INDEX = VALIDATOR_RETURN + 1; const createFactoryStub = () => { let counter = 0; return { @@ -50,6 +51,46 @@ const expectedAddress = (index: number) => { return getAddress(`0x${pattern.repeat(ADDRESS_REPEAT)}`); }; +const expectedPublicKey = (index: number) => { + const pattern = index.toString(HEX_RADIX).padStart(PAD_WIDTH, PAD_CHAR); + return `0x04${pattern.repeat(PUBLIC_KEY_REPEAT)}` as const; +}; + +const expectedStaticNodeUri = ( + index: number, + domain?: string, + port: number = DEFAULT_STATIC_NODE_PORT, + discoveryPort: number = DEFAULT_STATIC_NODE_PORT, + namespace?: string +): string => { + const normalizedDomain = + domain === undefined || domain.trim().length === 0 + ? undefined + : domain.trim().replace(LEADING_DOT_REGEX, ""); + const normalizedNamespace = + namespace === undefined || namespace.trim().length === 0 + ? undefined + : namespace.trim(); + const ordinal = index - 1; + const podName = `besu-node-validator-${ordinal}`; + const serviceName = "besu-node"; + const segments = [podName, serviceName]; + if (normalizedNamespace) { + segments.push(normalizedNamespace); + } + if (normalizedDomain) { + segments.push(normalizedDomain); + } + const host = segments.join("."); + const publicKey = expectedPublicKey(index).slice(2); + const nodeId = + publicKey.startsWith(UNCOMPRESSED_PUBLIC_KEY_PREFIX) && + publicKey.length === UNCOMPRESSED_PUBLIC_KEY_LENGTH + ? publicKey.slice(2) + : publicKey; + return `enode://${nodeId}@${host}:${port}?discport=${discoveryPort}`; +}; + const captureStdout = () => { let captured = ""; const original = process.stdout.write; @@ -92,10 +133,7 @@ describe("CLI command bootstrap", () => { factory, promptForCount: (label, provided, defaultValue) => { promptCalls.push([label, provided, defaultValue]); - const value = label.startsWith("validator") - ? VALIDATOR_RETURN - : RPC_RETURN; - return Promise.resolve(value); + return Promise.resolve(VALIDATOR_RETURN); }, promptForGenesis: ( _service, @@ -155,15 +193,18 @@ describe("CLI command bootstrap", () => { expect(promptCalls).toEqual([ [VALIDATOR_LABEL, undefined, EXPECTED_DEFAULT_VALIDATOR], - [RPC_LABEL, undefined, EXPECTED_DEFAULT_RPC], ]); const output = stdout.read(); expect(output).toContain("Genesis"); expect(output).toContain("Validator Nodes"); - expect(output).toContain("RPC Nodes"); + expect(output).toContain("Static Nodes"); expect(output).toContain(GENESIS_MARKER); expect(loadAllocationsPath).toBe("/tmp/alloc.json"); expect(outputInvocation?.type).toBe("screen"); + expect(outputInvocation?.payload.staticNodes).toEqual([ + expectedStaticNodeUri(FIRST_VALIDATOR_INDEX), + expectedStaticNodeUri(SECOND_VALIDATOR_INDEX), + ]); }); test("createCliCommand wires metadata", () => { @@ -194,7 +235,7 @@ describe("CLI command bootstrap", () => { algorithm: preset?.algorithm ?? ALGORITHM.QBFT, config: { chainId: preset?.chainId ?? 1, - faucetWalletAddress: expectedAddress(CLI_FAUCET_INDEX), + faucetWalletAddress: expectedAddress(VALIDATOR_RETURN + 1), gasLimit: "0x1", secondsPerBlock: preset?.secondsPerBlock ?? 1, gasPrice: preset?.gasPrice ?? 0, @@ -215,10 +256,9 @@ describe("CLI command bootstrap", () => { [ "node", "cli", + "generate", "--validators", "2", - "--rpc-nodes", - "1", "--allocations", "/tmp/mock.json", "--consensus", @@ -235,6 +275,12 @@ describe("CLI command bootstrap", () => { "2048", "--contract-size-limit", "10000", + "--static-node-domain", + "network.svc.cluster.local", + "--static-node-port", + `${CUSTOM_STATIC_NODE_PORT}`, + "--static-node-discovery-port", + "0", ], { from: "node" } ); @@ -244,8 +290,122 @@ describe("CLI command bootstrap", () => { expect(stdout.read()).toContain(GENESIS_MARKER); }); + test("createCliCommand accepts static node configuration flags", async () => { + const factory = createFactoryStub(); + let capturedPayload: OutputPayload | undefined; + + const deps: BootstrapDependencies = { + factory, + promptForCount: (_label, provided, defaultValue) => + Promise.resolve(provided ?? defaultValue), + promptForGenesis: (_service, { faucetAddress }) => + Promise.resolve({ + algorithm: ALGORITHM.QBFT, + config: { + chainId: 77, + faucetWalletAddress: faucetAddress, + gasLimit: "0x1", + secondsPerBlock: 2, + }, + genesis: { config: {}, extraData: "0xextra" } as any, + }), + service: {} as any, + loadAllocations: () => + Promise.resolve({} satisfies Record), + outputResult: (_type, payload) => { + capturedPayload = payload; + return Promise.resolve(); + }, + }; + + const command = createCliCommand(deps); + await command.parseAsync( + [ + "node", + "cli", + "generate", + "--validators", + "1", + "--static-node-domain", + "svc.cluster.local", + "--static-node-namespace", + "network", + "--static-node-port", + "40000", + "--static-node-discovery-port", + "0", + ], + { from: "node" } + ); + + expect(capturedPayload?.staticNodes).toEqual([ + expectedStaticNodeUri( + 1, + "svc.cluster.local", + CUSTOM_STATIC_NODE_PORT, + 0, + "network" + ), + ]); + }); + + test("runBootstrap builds static nodes with domain and custom ports", async () => { + const factory = createFactoryStub(); + let capturedPayload: OutputPayload | undefined; + + const deps: BootstrapDependencies = { + factory, + promptForCount: (_label, provided, defaultValue) => + Promise.resolve(provided ?? defaultValue), + promptForGenesis: (_service, { validatorAddresses, faucetAddress }) => { + expect(validatorAddresses).toHaveLength(1); + expect(faucetAddress).toBe(expectedAddress(2)); + return Promise.resolve({ + algorithm: ALGORITHM.QBFT, + config: { + chainId: 123, + faucetWalletAddress: faucetAddress, + gasLimit: "0x1", + secondsPerBlock: 2, + }, + genesis: { config: {}, extraData: "0xextra" } as any, + }); + }, + service: {} as any, + loadAllocations: () => + Promise.resolve({} satisfies Record), + outputResult: (_type, payload) => { + capturedPayload = payload; + return Promise.resolve(); + }, + }; + + await runBootstrap( + { + validators: 1, + staticNodeDomain: "svc.cluster.local", + staticNodeNamespace: "network", + staticNodePort: CUSTOM_STATIC_NODE_PORT, + staticNodeDiscoveryPort: 0, + }, + deps + ); + + expect(capturedPayload?.staticNodes).toEqual([ + expectedStaticNodeUri( + 1, + "svc.cluster.local", + CUSTOM_STATIC_NODE_PORT, + 0, + "network" + ), + ]); + }); + test("runBootstrap bypasses genesis prompts when CLI overrides provided", async () => { const factory = createFactoryStub(); + const validatorOverride = 1; + const deps: BootstrapDependencies = { factory, promptForCount: (_label, provided) => { @@ -271,7 +431,7 @@ describe("CLI command bootstrap", () => { algorithm: ALGORITHM.IBFTv2, config: { chainId: 1234, - faucetWalletAddress: expectedAddress(CLI_FAUCET_INDEX), + faucetWalletAddress: expectedAddress(validatorOverride + 1), gasLimit: "0x1", secondsPerBlock: 6, }, @@ -287,8 +447,7 @@ describe("CLI command bootstrap", () => { }; const options: CliOptions = { - validators: 1, - rpcNodes: 1, + validators: validatorOverride, consensus: ALGORITHM.IBFTv2, chainId: 1234, secondsPerBlock: 6, @@ -305,8 +464,11 @@ describe("CLI command bootstrap", () => { const shouldReject = async (args: string[], message: string) => { const command = createCliCommand(); command.exitOverride(); + for (const child of command.commands) { + child.exitOverride(); + } await expect( - command.parseAsync(["node", "cli", ...args]) + command.parseAsync(["node", "cli", "generate", ...args]) ).rejects.toThrow(message); }; @@ -331,8 +493,11 @@ describe("CLI command bootstrap", () => { test("createCliCommand rejects unsupported output type", async () => { const command = createCliCommand(); command.exitOverride(); + for (const child of command.commands) { + child.exitOverride(); + } await expect( - command.parseAsync(["node", "cli", "--outputType", "invalid"]) + command.parseAsync(["node", "cli", "generate", "--outputType", "invalid"]) ).rejects.toThrow( `Output type must be one of: ${["screen", "file", "kubernetes"].join(", ")}.` ); @@ -347,7 +512,7 @@ describe("CLI command bootstrap", () => { algorithm: ALGORITHM.QBFT, config: { chainId: 1, - faucetWalletAddress: expectedAddress(CLI_FAUCET_INDEX), + faucetWalletAddress: expectedAddress(EXPECTED_DEFAULT_VALIDATOR + 1), gasLimit: "0x1", gasPrice: 0, secondsPerBlock: 2, @@ -364,22 +529,38 @@ describe("CLI command bootstrap", () => { }; const command = createCliCommand(deps); + const generate = command.commands.find( + (child) => child.name() === "generate" + ); + expect(generate).toBeDefined(); command.exitOverride(); + for (const child of command.commands) { + child.exitOverride(); + } await expect( command.parseAsync( - ["node", "cli", '--outputType="kubernetes"', "--accept-defaults"], + [ + "node", + "cli", + "generate", + '--outputType="kubernetes"', + "--accept-defaults", + ], { from: "node" } ) ).resolves.toBeDefined(); - expect(command.opts().outputType).toBe("kubernetes"); + expect(generate?.opts().outputType).toBe("kubernetes"); expect(capturedOutputType).toBe("kubernetes"); }); test("createCliCommand rejects unsupported consensus", async () => { const command = createCliCommand(); command.exitOverride(); + for (const child of command.commands) { + child.exitOverride(); + } await expect( - command.parseAsync(["node", "cli", "--consensus", "invalid"]) + command.parseAsync(["node", "cli", "generate", "--consensus", "invalid"]) ).rejects.toThrow( `Consensus must be one of: ${Object.values(ALGORITHM).join(", ")}.` ); @@ -413,7 +594,7 @@ describe("CLI command bootstrap", () => { config: { chainId: 1, faucetWalletAddress: expectedAddress( - EXPECTED_DEFAULT_VALIDATOR + EXPECTED_DEFAULT_RPC + 1 + EXPECTED_DEFAULT_VALIDATOR + 1 ), gasLimit: "0x1", secondsPerBlock: 2, @@ -428,7 +609,6 @@ describe("CLI command bootstrap", () => { }, outputResult: (_type, payload) => { expect(payload.validators).toHaveLength(EXPECTED_DEFAULT_VALIDATOR); - expect(payload.rpcNodes).toHaveLength(EXPECTED_DEFAULT_RPC); return Promise.resolve(); }, }; diff --git a/src/cli/build-command.ts b/src/cli/build-command.ts index 20e8b40..72663ec 100644 --- a/src/cli/build-command.ts +++ b/src/cli/build-command.ts @@ -25,10 +25,13 @@ type CliOptions = { evmStackSize?: number; gasLimit?: string; gasPrice?: number; - rpcNodes?: number; validators?: number; outputType?: OutputType; secondsPerBlock?: number; + staticNodeDomain?: string; + staticNodeNamespace?: string; + staticNodePort?: number; + staticNodeDiscoveryPort?: number; }; type BootstrapDependencies = { @@ -41,8 +44,11 @@ type BootstrapDependencies = { }; const DEFAULT_VALIDATOR_COUNT = 4; -const DEFAULT_RPC_COUNT = 2; +const DEFAULT_STATIC_NODE_PORT = 30_303; const OUTPUT_CHOICES: OutputType[] = ["screen", "file", "kubernetes"]; +const LEADING_DOT_REGEX = /^\./u; +const UNCOMPRESSED_PUBLIC_KEY_PREFIX = "04"; +const UNCOMPRESSED_PUBLIC_KEY_LENGTH = 130; // Normalizes CLI inputs wrapped by orchestrators that keep literal quotes. const stripSurroundingQuotes = (value: string): string => { @@ -96,6 +102,75 @@ const generateGroup = (factory: NodeKeyFactory, count: number): IndexedNode[] => ...factory.generate(), })); +const normalizeStaticNodeDomain = ( + domain: string | undefined +): string | undefined => { + if (!domain) { + return; + } + + const trimmed = domain.trim().replace(LEADING_DOT_REGEX, ""); + return trimmed.length === 0 ? undefined : trimmed; +}; + +const normalizeStaticNodeNamespace = ( + namespace: string | undefined +): string | undefined => { + if (!namespace) { + return; + } + + const trimmed = namespace.trim(); + return trimmed.length === 0 ? undefined : trimmed; +}; + +const deriveNodeId = (publicKey: string): string => { + const trimmed = publicKey.startsWith("0x") ? publicKey.slice(2) : publicKey; + if ( + trimmed.startsWith(UNCOMPRESSED_PUBLIC_KEY_PREFIX) && + trimmed.length === UNCOMPRESSED_PUBLIC_KEY_LENGTH + ) { + return trimmed.slice(2); + } + return trimmed; +}; + +const createStaticNodeEntries = ( + nodes: readonly IndexedNode[], + { + namespace, + domain, + port, + discoveryPort, + }: { + namespace?: string; + domain?: string; + port: number; + discoveryPort: number; + } +): string[] => { + const normalizedDomain = normalizeStaticNodeDomain(domain); + const normalizedNamespace = normalizeStaticNodeNamespace(namespace); + + return nodes.map((node) => { + // StatefulSet pod ordinals start at 0 even though our generator indexes start at 1. + const ordinal = node.index - 1; + const podName = `besu-node-validator-${ordinal}`; + const serviceName = "besu-node"; // Headless service fronting validator pods + const segments = [podName, serviceName]; + if (normalizedNamespace) { + segments.push(normalizedNamespace); + } + if (normalizedDomain) { + segments.push(normalizedDomain); + } + const host = segments.join("."); + const nodeId = deriveNodeId(node.publicKey); + + return `enode://${nodeId}@${host}:${port}?discport=${discoveryPort}`; + }); +}; + const runBootstrap = async ( options: CliOptions, deps: BootstrapDependencies @@ -110,9 +185,12 @@ const runBootstrap = async ( gasLimit, gasPrice, outputType, - rpcNodes: rpcNodeOption, secondsPerBlock, validators: validatorOption, + staticNodeDomain: staticNodeDomainOption, + staticNodeNamespace: staticNodeNamespaceOption, + staticNodePort: staticNodePortOption, + staticNodeDiscoveryPort: staticNodeDiscoveryPortOption, } = options; const resolveCount = ( @@ -134,15 +212,15 @@ const runBootstrap = async ( validatorOption, DEFAULT_VALIDATOR_COUNT ); - const rpcNodeCount = await resolveCount( - "RPC nodes", - rpcNodeOption, - DEFAULT_RPC_COUNT - ); const validators = generateGroup(deps.factory, validatorsCount); - const rpcNodes = generateGroup(deps.factory, rpcNodeCount); const faucet = deps.factory.generate(); + const staticNodes = createStaticNodeEntries(validators, { + namespace: staticNodeNamespaceOption, + domain: staticNodeDomainOption, + port: staticNodePortOption ?? DEFAULT_STATIC_NODE_PORT, + discoveryPort: staticNodeDiscoveryPortOption ?? DEFAULT_STATIC_NODE_PORT, + }); const validatorAddresses = validators.map((node) => node.address); @@ -171,8 +249,8 @@ const runBootstrap = async ( const payload: OutputPayload = { faucet, genesis, - rpcNodes, validators, + staticNodes, }; await deps.outputResult(outputType ?? "screen", payload); @@ -196,21 +274,22 @@ const createCliCommand = ( command .name("network-bootstrapper") + .description("Utilities for configuring Besu-based networks."); + + // Keep the root command free of options so future subcommands can compose alongside generate. + const generate = command + .command("generate") .description( "Generate node identities, configure consensus, and emit a Besu genesis." - ) + ); + + generate .option( "-v, --validators ", "Number of validator nodes to generate.", createCountParser("Validators"), DEFAULT_VALIDATOR_COUNT ) - .option( - "-r, --rpc-nodes ", - "Number of RPC nodes to generate.", - createCountParser("RPC nodes"), - DEFAULT_RPC_COUNT - ) .option( "-a, --allocations ", "Path to a genesis allocations JSON file. (default: none)" @@ -229,6 +308,29 @@ const createCliCommand = ( }, "screen" ) + .option( + "--static-node-domain ", + "DNS suffix appended to validator peer hostnames for static-nodes entries.", + (value: string) => stripSurroundingQuotes(value) + ) + .option( + "--static-node-namespace ", + "Namespace segment inserted between service name and domain for static-nodes entries.", + (value: string) => stripSurroundingQuotes(value) + ) + .option( + "--static-node-port ", + "P2P port used for static-nodes enode URIs.", + (value: string) => parsePositiveInteger(value, "Static node port"), + DEFAULT_STATIC_NODE_PORT + ) + .option( + "--static-node-discovery-port ", + "Discovery port used for static-nodes enode URIs.", + (value: string) => + parseNonNegativeInteger(value, "Static node discovery port"), + DEFAULT_STATIC_NODE_PORT + ) .option( "--consensus ", `Consensus algorithm (${Object.values(ALGORITHM).join(", ")}). (default: ${ @@ -282,31 +384,48 @@ const createCliCommand = ( .option( "--accept-defaults", "Accept default values for all prompts when CLI flags are omitted. (default: disabled)" - ); + ) + .action(async (options: CliOptions, cmd: Command) => { + const normalizedOptions: CliOptions = { + ...options, + validators: + cmd.getOptionValueSource("validators") === "default" + ? undefined + : options.validators, + staticNodeDomain: + cmd.getOptionValueSource("staticNodeDomain") === "default" + ? undefined + : options.staticNodeDomain, + staticNodeNamespace: + cmd.getOptionValueSource("staticNodeNamespace") === "default" + ? undefined + : options.staticNodeNamespace, + staticNodePort: + cmd.getOptionValueSource("staticNodePort") === "default" + ? undefined + : options.staticNodePort, + staticNodeDiscoveryPort: + cmd.getOptionValueSource("staticNodeDiscoveryPort") === "default" + ? undefined + : options.staticNodeDiscoveryPort, + }; - command.action(async (options: CliOptions, cmd: Command) => { - const normalizedOptions: CliOptions = { - ...options, - validators: - cmd.getOptionValueSource("validators") === "default" - ? undefined - : options.validators, - rpcNodes: - cmd.getOptionValueSource("rpcNodes") === "default" - ? undefined - : options.rpcNodes, - }; - - const sanitizedOptions: CliOptions = { - ...normalizedOptions, - allocations: - normalizedOptions.allocations === undefined - ? undefined - : stripSurroundingQuotes(normalizedOptions.allocations), - }; - - await runBootstrap(sanitizedOptions, deps); - }); + const sanitizedOptions: CliOptions = { + ...normalizedOptions, + allocations: + normalizedOptions.allocations === undefined + ? undefined + : stripSurroundingQuotes(normalizedOptions.allocations), + staticNodeDomain: normalizeStaticNodeDomain( + normalizedOptions.staticNodeDomain + ), + staticNodeNamespace: normalizeStaticNodeNamespace( + normalizedOptions.staticNodeNamespace + ), + }; + + await runBootstrap(sanitizedOptions, deps); + }); return command; }; diff --git a/src/cli/output.test.ts b/src/cli/output.test.ts index a3e5f59..f8d58e4 100644 --- a/src/cli/output.test.ts +++ b/src/cli/output.test.ts @@ -75,15 +75,21 @@ const PRIVATE_KEY_HEX_LENGTH = 64; const PUBLIC_KEY_HEX_LENGTH = 128; const HEX_RADIX = 16; const SAMPLE_VALIDATOR_INDEX = 1; -const SAMPLE_RPC_INDEX = 2; const SAMPLE_FAUCET_INDEX = 99; -const EXPECTED_CONFIGMAP_COUNT = 9; -const EXPECTED_SECRET_COUNT = 3; +const EXPECTED_CONFIGMAP_COUNT = 7; +const EXPECTED_SECRET_COUNT = 2; const HEX_PREFIX_PATTERN = /^0x/; const TEST_CHAIN_ID = 1; const HTTP_CONFLICT_STATUS = 409; const HTTP_INTERNAL_ERROR_STATUS = 500; const HTTP_SERVICE_UNAVAILABLE_STATUS = 503; +const LEADING_DOT_REGEX = /^\./u; +const DEFAULT_STATIC_NODE_PORT = 30_303; +const DEFAULT_STATIC_NODE_DISCOVERY_PORT = 30_303; +const SAMPLE_STATIC_DOMAIN = "svc.cluster.local"; +const SAMPLE_STATIC_NAMESPACE = "network"; +const UNCOMPRESSED_PUBLIC_KEY_PREFIX = "04"; +const UNCOMPRESSED_PUBLIC_KEY_LENGTH = 130; const sampleNode = (index: number): IndexedNode => { const hexValue = index.toString(HEX_RADIX); @@ -100,11 +106,59 @@ const sampleNode = (index: number): IndexedNode => { }; }; +const staticNodeUri = ( + node: IndexedNode, + domain?: string, + port = DEFAULT_STATIC_NODE_PORT, + discoveryPort = DEFAULT_STATIC_NODE_DISCOVERY_PORT, + namespace?: string +): string => { + const trimmedDomain = + domain === undefined || domain.trim().length === 0 + ? undefined + : domain.trim().replace(LEADING_DOT_REGEX, ""); + const trimmedNamespace = + namespace === undefined || namespace.trim().length === 0 + ? undefined + : namespace.trim(); + const ordinal = node.index - 1; + const podName = `besu-node-validator-${ordinal}`; + const serviceName = "besu-node"; + const segments = [podName, serviceName]; + if (trimmedNamespace) { + segments.push(trimmedNamespace); + } + if (trimmedDomain) { + segments.push(trimmedDomain); + } + const host = segments.join("."); + const publicKey = node.publicKey.startsWith("0x") + ? node.publicKey.slice(2) + : node.publicKey; + const nodeId = + publicKey.startsWith(UNCOMPRESSED_PUBLIC_KEY_PREFIX) && + publicKey.length === UNCOMPRESSED_PUBLIC_KEY_LENGTH + ? publicKey.slice(2) + : publicKey; + return `enode://${nodeId}@${host}:${port}?discport=${discoveryPort}`; +}; + +const sampleValidator = sampleNode(SAMPLE_VALIDATOR_INDEX); +const sampleFaucet = sampleNode(SAMPLE_FAUCET_INDEX); + const samplePayload: OutputPayload = { - faucet: sampleNode(SAMPLE_FAUCET_INDEX), + faucet: sampleFaucet, genesis: { config: { chainId: TEST_CHAIN_ID }, extraData: "0xabc" }, - rpcNodes: [sampleNode(SAMPLE_RPC_INDEX)], - validators: [sampleNode(SAMPLE_VALIDATOR_INDEX)], + validators: [sampleValidator], + staticNodes: [ + staticNodeUri( + sampleValidator, + SAMPLE_STATIC_DOMAIN, + DEFAULT_STATIC_NODE_PORT, + DEFAULT_STATIC_NODE_DISCOVERY_PORT, + SAMPLE_STATIC_NAMESPACE + ), + ], }; describe("outputResult", () => { @@ -113,6 +167,7 @@ describe("outputResult", () => { await outputResult("screen", samplePayload); expect(output).toContain("Genesis"); expect(output).toContain("Validator Nodes"); + expect(output).toContain("Static Nodes"); }); test("file output writes json artifacts", async () => { @@ -134,15 +189,12 @@ describe("outputResult", () => { "besu-faucet-enode", "besu-faucet-private-key", "besu-faucet-pubkey", - "besu-node-rpc-node-2-address", - "besu-node-rpc-node-2-enode", - "besu-node-rpc-node-2-private-key", - "besu-node-rpc-node-2-pubkey", - "besu-node-validator-1-address", - "besu-node-validator-1-enode", - "besu-node-validator-1-private-key", - "besu-node-validator-1-pubkey", + "besu-node-validator-0-address", + "besu-node-validator-0-enode", + "besu-node-validator-0-private-key", + "besu-node-validator-0-pubkey", "genesis", + "static-nodes.json", ].sort() ); @@ -152,6 +204,12 @@ describe("outputResult", () => { ); expect(genesisContent).toContain(`"chainId": ${TEST_CHAIN_ID}`); + const staticNodesContent = await readFile( + join(targetDirPath, "static-nodes.json"), + "utf8" + ); + expect(JSON.parse(staticNodesContent)).toEqual(samplePayload.staticNodes); + await rm("out", { recursive: true, force: true }); }); @@ -233,21 +291,28 @@ describe("outputResult", () => { expect(createdConfigMaps).toHaveLength(EXPECTED_CONFIGMAP_COUNT); expect(createdSecrets).toHaveLength(EXPECTED_SECRET_COUNT); const mapNames = createdConfigMaps.map((entry) => entry.name).sort(); - expect(mapNames).toContain("besu-node-validator-1-address"); + expect(mapNames).toContain("besu-node-validator-0-address"); expect(mapNames).toContain("besu-genesis"); expect(mapNames).toContain("besu-faucet-address"); expect(mapNames).toContain("besu-faucet-pubkey"); + expect(mapNames).toContain("besu-static-nodes"); expect(mapNames).not.toContain("besu-faucet-enode"); const secretNames = createdSecrets.map((entry) => entry.name).sort(); expect(secretNames).toEqual([ "besu-faucet-private-key", - "besu-node-rpc-node-2-private-key", - "besu-node-validator-1-private-key", + "besu-node-validator-0-private-key", ]); const privateKeySecret = createdSecrets.find((entry) => - entry.name.endsWith("validator-1-private-key") + entry.name.endsWith("validator-0-private-key") ); expect(privateKeySecret?.data?.privateKey).toMatch(HEX_PREFIX_PATTERN); + const staticNodesConfig = createdConfigMaps.find( + (entry) => entry.name === "besu-static-nodes" + ); + expect(staticNodesConfig?.data?.["static-nodes.json"]).toBeDefined(); + expect( + JSON.parse(staticNodesConfig?.data?.["static-nodes.json"] ?? "[]") + ).toEqual(samplePayload.staticNodes); } finally { (KubeConfig.prototype as any).loadFromCluster = originalLoad; (KubeConfig.prototype as any).makeApiClient = originalMake; @@ -292,7 +357,7 @@ describe("outputResult", () => { }) as unknown as ReturnType; await expect(outputResult("kubernetes", samplePayload)).rejects.toThrow( - "ConfigMap besu-node-validator-1-address already exists. Delete it or choose a different output target." + "ConfigMap besu-node-validator-0-address already exists. Delete it or choose a different output target." ); } finally { (KubeConfig.prototype as any).loadFromCluster = originalLoad; @@ -338,7 +403,7 @@ describe("outputResult", () => { }) as unknown as ReturnType; await expect(outputResult("kubernetes", samplePayload)).rejects.toThrow( - "Secret besu-node-validator-1-private-key already exists. Delete it or choose a different output target." + "Secret besu-node-validator-0-private-key already exists. Delete it or choose a different output target." ); } finally { (KubeConfig.prototype as any).loadFromCluster = originalLoad; @@ -481,7 +546,7 @@ describe("outputResult", () => { }) as unknown as ReturnType; await expect(outputResult("kubernetes", samplePayload)).rejects.toThrow( - "Failed to create ConfigMap besu-node-validator-1-address: boom" + "Failed to create ConfigMap besu-node-validator-0-address: boom" ); } finally { (KubeConfig.prototype as any).loadFromCluster = originalLoad; @@ -520,7 +585,7 @@ describe("outputResult", () => { }) as unknown as ReturnType; await expect(outputResult("kubernetes", samplePayload)).rejects.toThrow( - "Failed to create ConfigMap besu-node-validator-1-address: failed" + "Failed to create ConfigMap besu-node-validator-0-address: failed" ); } finally { (KubeConfig.prototype as any).loadFromCluster = originalLoad; @@ -561,7 +626,7 @@ describe("outputResult", () => { }) as unknown as ReturnType; await expect(outputResult("kubernetes", samplePayload)).rejects.toThrow( - "Failed to create ConfigMap besu-node-validator-1-address: unknown error" + "Failed to create ConfigMap besu-node-validator-0-address: unknown error" ); } finally { (KubeConfig.prototype as any).loadFromCluster = originalLoad; @@ -602,7 +667,7 @@ describe("outputResult", () => { }) as unknown as ReturnType; await expect(outputResult("kubernetes", samplePayload)).rejects.toThrow( - "Failed to create ConfigMap besu-node-validator-1-address: denied" + "Failed to create ConfigMap besu-node-validator-0-address: denied" ); } finally { (KubeConfig.prototype as any).loadFromCluster = originalLoad; diff --git a/src/cli/output.ts b/src/cli/output.ts index c3056ee..f47aa9c 100644 --- a/src/cli/output.ts +++ b/src/cli/output.ts @@ -13,8 +13,8 @@ type OutputType = "screen" | "file" | "kubernetes"; type OutputPayload = { faucet: GeneratedNodeKey; genesis: unknown; - rpcNodes: readonly IndexedNode[]; validators: readonly IndexedNode[]; + staticNodes: readonly string[]; }; type ConfigMapSpec = { @@ -47,6 +47,15 @@ const printGroup = (title: string, nodes: readonly IndexedNode[]): void => { process.stdout.write("\n"); }; +const printStaticNodes = (staticNodes: readonly string[]): void => { + if (staticNodes.length === 0) { + return; + } + + process.stdout.write(`${accent("Static Nodes")}\n`); + process.stdout.write(`${JSON.stringify(staticNodes, null, 2)}\n\n`); +}; + const printFaucet = (faucet: GeneratedNodeKey): void => { process.stdout.write(`${accent("Faucet Account")}\n`); process.stdout.write(` address: ${faucet.address}\n`); @@ -65,7 +74,7 @@ const outputToScreen = (payload: OutputPayload): void => { process.stdout.write("\n\n"); printGenesis("Genesis", genesisJson); printGroup("Validator Nodes", payload.validators); - printGroup("RPC Nodes", payload.rpcNodes); + printStaticNodes(payload.staticNodes); printFaucet(payload.faucet); }; @@ -87,8 +96,7 @@ const outputToFile = async (payload: OutputPayload): Promise => { const directory = join(OUTPUT_DIR, timestamp); await mkdir(directory, { recursive: true }); - const validatorSpecs = createSpecsForGroup("validator", payload.validators); - const rpcSpecs = createSpecsForGroup("rpc-node", payload.rpcNodes); + const validatorSpecs = createValidatorSpecs(payload.validators); const faucetSpecs: ConfigMapSpec[] = [ { @@ -118,12 +126,16 @@ const outputToFile = async (payload: OutputPayload): Promise => { join(directory, "genesis"), `${JSON.stringify(payload.genesis, null, 2)}\n` ), - ...[...validatorSpecs, ...rpcSpecs, ...faucetSpecs].map((spec) => + ...[...validatorSpecs, ...faucetSpecs].map((spec) => Bun.write( join(directory, spec.name), `${JSON.stringify({ [spec.key]: spec.value }, null, 2)}\n` ) ), + Bun.write( + join(directory, "static-nodes.json"), + `${JSON.stringify(payload.staticNodes, null, 2)}\n` + ), ]; await Promise.all(writes); @@ -132,9 +144,8 @@ const outputToFile = async (payload: OutputPayload): Promise => { const outputToKubernetes = async (payload: OutputPayload): Promise => { const { client, namespace } = await createKubernetesClient(); - const validatorSpecs = createSpecsForGroup("validator", payload.validators); - const rpcSpecs = createSpecsForGroup("rpc-node", payload.rpcNodes); - const allSpecs = [...validatorSpecs, ...rpcSpecs]; + const validatorSpecs = createValidatorSpecs(payload.validators); + const allSpecs = [...validatorSpecs]; const configMapSpecs = [ ...allSpecs.filter((spec) => spec.key !== "privateKey"), ...createFaucetConfigSpecs(payload.faucet), @@ -143,6 +154,11 @@ const outputToKubernetes = async (payload: OutputPayload): Promise => { key: "genesis.json", value: JSON.stringify(payload.genesis, null, 2), }, + { + name: "besu-static-nodes", + key: "static-nodes.json", + value: JSON.stringify(payload.staticNodes, null, 2), + }, ]; const secretSpecs = [ ...allSpecs.filter((spec) => spec.key === "privateKey"), @@ -159,15 +175,11 @@ const outputToKubernetes = async (payload: OutputPayload): Promise => { ); }; -const createSpecsForGroup = ( - group: "validator" | "rpc-node", - nodes: readonly IndexedNode[] -): ConfigMapSpec[] => { - const prefix = - group === "validator" ? "besu-node-validator" : "besu-node-rpc-node"; - - return nodes.flatMap((node) => { - const base = `${prefix}-${node.index}`; +const createValidatorSpecs = (nodes: readonly IndexedNode[]): ConfigMapSpec[] => + nodes.flatMap((node) => { + // Align artifact names with 0-indexed StatefulSet pod ordinals. + const ordinal = node.index - 1; + const base = `besu-node-validator-${ordinal}`; return [ { name: `${base}-address`, key: "address", value: node.address }, { @@ -179,7 +191,6 @@ const createSpecsForGroup = ( { name: `${base}-pubkey`, key: "publicKey", value: node.publicKey }, ]; }); -}; const createFaucetConfigSpecs = (faucet: GeneratedNodeKey): ConfigMapSpec[] => [ { name: "besu-faucet-address", key: "address", value: faucet.address },