diff --git a/.github/ct.yaml b/.github/ct.yaml index dd514f0..30d99fa 100644 --- a/.github/ct.yaml +++ b/.github/ct.yaml @@ -2,3 +2,4 @@ chart-dirs: - charts remote: origin target-branch: main +check-version-increment: false diff --git a/README.md b/README.md index 6fe3e69..0327584 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,130 @@ Generate node identities, configure consensus, and emit a Besu genesis. The helm chart to run this on Kubernetes / OpenShift can be found [here](./charts/network-bootstrapper/README.md) +### Deployment modes + +Two deployment paths are supported: fully auto-generated artefacts or supplying your own genesis/static peers while sourcing node keys from an external secret store such as Conjur. + +#### Auto-generated artefacts (bootstrapper job) + +```bash +cat <<'EOF' > values-generated.yaml +network-bootstrapper: + artifacts: + source: generated + settings: + validators: 4 + +network-nodes: + global: + validatorReplicaCount: 4 +EOF + +helm upgrade --install besu-network ./charts/network \ + --namespace besu \ + --create-namespace \ + --values values-generated.yaml +``` + +The bootstrapper Job generates the genesis file, static-nodes list, validator keys, and faucet account and publishes them as ConfigMaps/Secrets consumed by the Besu StatefulSets. + +#### External genesis/static peers with Conjur-managed keys + +Genesis and static peer data can be committed to version control while validator and faucet private keys are injected at deployment time. The chart expects the validator count in `artifacts.external.validators` to match `global.validatorReplicaCount`. + +Create a Summon manifest describing the Conjur variables and a templated values file that references the injected environment variables: + +```bash +cat <<'EOF' > conjur.env.yml +BESU_NODE_VALIDATOR_0_PRIVATE_KEY: !var production/besu/validator0/private-key +BESU_NODE_VALIDATOR_1_PRIVATE_KEY: !var production/besu/validator1/private-key +BESU_FAUCET_PRIVATE_KEY: !var production/besu/faucet/private-key +EOF + +cat <<'EOF' > values-external.tpl.yaml +network-bootstrapper: + artifacts: + source: external + external: + genesis: + config: + chainId: 12345 + alloc: + "0xfund": + balance: "0x56bc75e2d63100000" + extraData: "0x" + staticNodes: + - enode://node1@validator-0.besu.svc.cluster.local:30303 + - enode://node2@validator-1.besu.svc.cluster.local:30303 + validators: + - address: "0x111" + publicKey: "0x222" + privateKey: "${BESU_NODE_VALIDATOR_0_PRIVATE_KEY}" + enode: enode://validator1@validator-0.besu.svc.cluster.local:30303 + - address: "0x333" + publicKey: "0x444" + privateKey: "${BESU_NODE_VALIDATOR_1_PRIVATE_KEY}" + enode: enode://validator2@validator-1.besu.svc.cluster.local:30303 + faucet: + address: "0xfaucet" + publicKey: "0xfaucetpub" + privateKey: "${BESU_FAUCET_PRIVATE_KEY}" + + global: + validatorReplicaCount: 2 + +network-nodes: + validatorReplicaCount: + global: + validatorReplicaCount: 2 +EOF + +summon -f conjur.env.yml envsubst < values-external.tpl.yaml > values-external.yaml + +helm upgrade --install besu-network ./charts/network \ + --namespace besu \ + --create-namespace \ + --values values-external.yaml + +rm values-external.yaml +``` + +Summon resolves the secrets in memory, `envsubst` renders them into a transient values file, and Helm creates the ConfigMaps/Secrets required by the Besu nodes. The temporary file is removed once the release is installed. + +### Local artefact generation with Docker + +Run the bootstrapper container locally to capture all artefacts before loading them into Conjur or another secret manager. + +```bash +mkdir -p artifacts + +docker run --rm \ + -v "$(pwd)/artifacts:/workspace" \ + ghcr.io/settlemint/network-bootstrapper:0.1.0 \ + generate \ + --validators=2 \ + --outputType=file \ + --chain-id=12345 \ + --seconds-per-block=2 \ + --gas-limit=9007199254740991 \ + --accept-defaults + +LATEST_DIR=$(ls -t artifacts/out | head -n 1) + +for ordinal in 0 1; do + jq -r '.privateKey' "artifacts/out/${LATEST_DIR}/besu-node-validator-${ordinal}-private-key" \ + | conjur variable values add production/besu/validator${ordinal}/private-key - +done + +jq -r '.privateKey' "artifacts/out/${LATEST_DIR}/besu-faucet-private-key" \ + | conjur variable values add production/besu/faucet/private-key - + +jq -r '.genesis.json' "artifacts/out/${LATEST_DIR}/besu-genesis" > genesis.json +jq -r '."static-nodes.json"' "artifacts/out/${LATEST_DIR}/besu-static-nodes" > static-nodes.json +``` + +The container writes artefacts beneath `/workspace/out/`; mounting a host directory captures the results. Each validator and faucet file is emitted as JSON for ease of parsing. After loading secrets into Conjur, reference the same variables in your Summon configuration and embed the exported `genesis.json` and `static-nodes.json` within the Helm values file. + ## CLI usage ``` diff --git a/README.tpl b/README.tpl index 50a9612..9d6af67 100644 --- a/README.tpl +++ b/README.tpl @@ -6,4 +6,128 @@ Generate node identities, configure consensus, and emit a Besu genesis. The helm chart to run this on Kubernetes / OpenShift can be found [here](./charts/network-bootstrapper/README.md) +### Deployment modes + +Two deployment paths are supported: fully auto-generated artefacts or supplying your own genesis/static peers while sourcing node keys from an external secret store such as Conjur. + +#### Auto-generated artefacts (bootstrapper job) + +```bash +cat <<'EOF' > values-generated.yaml +network-bootstrapper: + artifacts: + source: generated + settings: + validators: 4 + +network-nodes: + global: + validatorReplicaCount: 4 +EOF + +helm upgrade --install besu-network ./charts/network \ + --namespace besu \ + --create-namespace \ + --values values-generated.yaml +``` + +The bootstrapper Job generates the genesis file, static-nodes list, validator keys, and faucet account and publishes them as ConfigMaps/Secrets consumed by the Besu StatefulSets. + +#### External genesis/static peers with Conjur-managed keys + +Genesis and static peer data can be committed to version control while validator and faucet private keys are injected at deployment time. The chart expects the validator count in `artifacts.external.validators` to match `global.validatorReplicaCount`. + +Create a Summon manifest describing the Conjur variables and a templated values file that references the injected environment variables: + +```bash +cat <<'EOF' > conjur.env.yml +BESU_NODE_VALIDATOR_0_PRIVATE_KEY: !var production/besu/validator0/private-key +BESU_NODE_VALIDATOR_1_PRIVATE_KEY: !var production/besu/validator1/private-key +BESU_FAUCET_PRIVATE_KEY: !var production/besu/faucet/private-key +EOF + +cat <<'EOF' > values-external.tpl.yaml +network-bootstrapper: + artifacts: + source: external + external: + genesis: + config: + chainId: 12345 + alloc: + "0xfund": + balance: "0x56bc75e2d63100000" + extraData: "0x" + staticNodes: + - enode://node1@validator-0.besu.svc.cluster.local:30303 + - enode://node2@validator-1.besu.svc.cluster.local:30303 + validators: + - address: "0x111" + publicKey: "0x222" + privateKey: "${BESU_NODE_VALIDATOR_0_PRIVATE_KEY}" + enode: enode://validator1@validator-0.besu.svc.cluster.local:30303 + - address: "0x333" + publicKey: "0x444" + privateKey: "${BESU_NODE_VALIDATOR_1_PRIVATE_KEY}" + enode: enode://validator2@validator-1.besu.svc.cluster.local:30303 + faucet: + address: "0xfaucet" + publicKey: "0xfaucetpub" + privateKey: "${BESU_FAUCET_PRIVATE_KEY}" + + global: + validatorReplicaCount: 2 + +network-nodes: + validatorReplicaCount: + global: + validatorReplicaCount: 2 +EOF + +summon -f conjur.env.yml envsubst < values-external.tpl.yaml > values-external.yaml + +helm upgrade --install besu-network ./charts/network \ + --namespace besu \ + --create-namespace \ + --values values-external.yaml + +rm values-external.yaml +``` + +Summon resolves the secrets in memory, `envsubst` renders them into a transient values file, and Helm creates the ConfigMaps/Secrets required by the Besu nodes. The temporary file is removed once the release is installed. + +### Local artefact generation with Docker + +Run the bootstrapper container locally to capture all artefacts before loading them into Conjur or another secret manager. + +```bash +mkdir -p artifacts + +docker run --rm \ + -v "$(pwd)/artifacts:/workspace" \ + ghcr.io/settlemint/network-bootstrapper:0.1.0 \ + generate \ + --validators=2 \ + --outputType=file \ + --chain-id=12345 \ + --seconds-per-block=2 \ + --gas-limit=9007199254740991 \ + --accept-defaults + +LATEST_DIR=$(ls -t artifacts/out | head -n 1) + +for ordinal in 0 1; do + jq -r '.privateKey' "artifacts/out/${LATEST_DIR}/besu-node-validator-${ordinal}-private-key" \ + | conjur variable values add production/besu/validator${ordinal}/private-key - +done + +jq -r '.privateKey' "artifacts/out/${LATEST_DIR}/besu-faucet-private-key" \ + | conjur variable values add production/besu/faucet/private-key - + +jq -r '.genesis.json' "artifacts/out/${LATEST_DIR}/besu-genesis" > genesis.json +jq -r '."static-nodes.json"' "artifacts/out/${LATEST_DIR}/besu-static-nodes" > static-nodes.json +``` + +The container writes artefacts beneath `/workspace/out/`; mounting a host directory captures the results. Each validator and faucet file is emitted as JSON for ease of parsing. After loading secrets into Conjur, reference the same variables in your Summon configuration and embed the exported `genesis.json` and `static-nodes.json` within the Helm values file. + ## CLI usage diff --git a/charts/network/charts/network-bootstrapper/README.md b/charts/network/charts/network-bootstrapper/README.md index 67d1bfe..5912d14 100644 --- a/charts/network/charts/network-bootstrapper/README.md +++ b/charts/network/charts/network-bootstrapper/README.md @@ -15,6 +15,13 @@ A Helm chart for Kubernetes | Key | Type | Default | Description | |-----|------|---------|-------------| | affinity | object | `{}` | | +| artifacts.external.faucet.address | string | `""` | Faucet account address stored in the `besu-faucet-address` ConfigMap when `source` equals `external`. | +| artifacts.external.faucet.privateKey | string | `""` | Faucet private key stored in the `besu-faucet-private-key` Secret when `source` equals `external`. | +| artifacts.external.faucet.publicKey | string | `""` | Faucet account public key stored in the `besu-faucet-pubkey` ConfigMap when `source` equals `external`. | +| artifacts.external.genesis | object | `{}` | Besu genesis document rendered into the `besu-genesis` ConfigMap when `source` equals `external`. | +| artifacts.external.staticNodes | list | `[]` | Collection of enode URIs persisted to the `besu-static-nodes` ConfigMap when `source` equals `external`. | +| artifacts.external.validators | list | `[]` | Validator node definitions providing the data expected by the nodes chart. Each entry must include `address`, `publicKey`, `privateKey`, and `enode`. | +| artifacts.source | string | `"generated"` | Determines how Besu network artifacts are populated. Use `generated` to run the job or `external` to supply values manually. | | 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. | diff --git a/charts/network/charts/network-bootstrapper/templates/external-artifacts.yaml b/charts/network/charts/network-bootstrapper/templates/external-artifacts.yaml new file mode 100644 index 0000000..5cf9692 --- /dev/null +++ b/charts/network/charts/network-bootstrapper/templates/external-artifacts.yaml @@ -0,0 +1,127 @@ +{{- $artifactSource := default "generated" .Values.artifacts.source -}} +{{- if eq $artifactSource "external" }} +{{- $root := . -}} +{{- $external := .Values.artifacts.external | default (dict) -}} +{{- $genesis := get $external "genesis" -}} +{{- $staticNodes := default (list) (get $external "staticNodes") -}} +{{- $validators := default (list) (get $external "validators") -}} +{{- $faucet := default (dict) (get $external "faucet") -}} +{{- $validatorCount := len $validators -}} +{{- if not $genesis }}{{ fail "artifacts.external.genesis must be provided when artifacts.source is 'external'." }}{{- end }} +{{- if eq (len $staticNodes) 0 }}{{ fail "artifacts.external.staticNodes must include at least one enode when artifacts.source is 'external'." }}{{- end }} +{{- if eq $validatorCount 0 }}{{ fail "artifacts.external.validators must include at least one entry when artifacts.source is 'external'." }}{{- end }} +{{- if eq (len $faucet) 0 }}{{ fail "artifacts.external.faucet must be provided when artifacts.source is 'external'." }}{{- end }} +{{- if empty (get $faucet "address") }}{{ fail "artifacts.external.faucet.address must be set when artifacts.source is 'external'." }}{{- end }} +{{- if empty (get $faucet "publicKey") }}{{ fail "artifacts.external.faucet.publicKey must be set when artifacts.source is 'external'." }}{{- end }} +{{- if empty (get $faucet "privateKey") }}{{ fail "artifacts.external.faucet.privateKey must be set when artifacts.source is 'external'." }}{{- end }} +{{- $faucetAddress := get $faucet "address" -}} +{{- $faucetPublicKey := get $faucet "publicKey" -}} +{{- $faucetPrivateKey := get $faucet "privateKey" -}} +{{- $globalValues := default (dict) .Values.global -}} +{{- $globalReplicaCount := -1 -}} +{{- if and (kindIs "map" $globalValues) (hasKey $globalValues "network") -}} + {{- $networkGlobal := index $globalValues "network" -}} + {{- if and (kindIs "map" $networkGlobal) (hasKey $networkGlobal "validatorReplicaCount") -}} + {{- $globalReplicaCount = (index $networkGlobal "validatorReplicaCount" | int) -}} + {{- end -}} +{{- end -}} +{{- if and (eq $globalReplicaCount -1) (kindIs "map" $globalValues) (hasKey $globalValues "validatorReplicaCount") -}} + {{- $globalReplicaCount = (index $globalValues "validatorReplicaCount" | int) -}} +{{- end -}} +{{- if eq $globalReplicaCount -1 -}} + {{- fail (printf "artifacts.external.validators has %d entries. Set global.validatorReplicaCount (or global.network.validatorReplicaCount) to this value and align network-nodes.validatorReplicaCount." $validatorCount) -}} +{{- end -}} +{{- if ne $globalReplicaCount $validatorCount -}} + {{- fail (printf "artifacts.external.validators has %d entries but the configured validator replica count is %d. Update global.validatorReplicaCount (or global.network.validatorReplicaCount) and network-nodes.validatorReplicaCount to match." $validatorCount $globalReplicaCount) -}} +{{- end -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: besu-genesis + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} +data: + genesis.json: |- +{{ toPrettyJson $genesis | indent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: besu-static-nodes + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} +data: + static-nodes.json: |- +{{ toPrettyJson $staticNodes | indent 4 }} +{{- range $index, $validator := $validators }} +{{- $requiredAddress := required (printf "artifacts.external.validators[%d].address must be set." $index) $validator.address }} +{{- $requiredPublicKey := required (printf "artifacts.external.validators[%d].publicKey must be set." $index) $validator.publicKey }} +{{- $requiredPrivateKey := required (printf "artifacts.external.validators[%d].privateKey must be set." $index) $validator.privateKey }} +{{- $requiredEnode := required (printf "artifacts.external.validators[%d].enode must be set." $index) $validator.enode }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ printf "besu-node-validator-%d-address" $index }} + labels: + {{- include "network-bootstrapper.labels" $root | nindent 4 }} +data: + address: {{ $requiredAddress | quote }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ printf "besu-node-validator-%d-enode" $index }} + labels: + {{- include "network-bootstrapper.labels" $root | nindent 4 }} +data: + enode: {{ $requiredEnode | quote }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ printf "besu-node-validator-%d-pubkey" $index }} + labels: + {{- include "network-bootstrapper.labels" $root | nindent 4 }} +data: + publicKey: {{ $requiredPublicKey | quote }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ printf "besu-node-validator-%d-private-key" $index }} + labels: + {{- include "network-bootstrapper.labels" $root | nindent 4 }} +type: Opaque +stringData: + privateKey: {{ $requiredPrivateKey | quote }} +{{- end }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: besu-faucet-address + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} +data: + address: {{ $faucetAddress | quote }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: besu-faucet-pubkey + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} +data: + publicKey: {{ $faucetPublicKey | quote }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: besu-faucet-private-key + labels: + {{- include "network-bootstrapper.labels" . | nindent 4 }} +type: Opaque +stringData: + privateKey: {{ $faucetPrivateKey | quote }} +{{- end }} diff --git a/charts/network/charts/network-bootstrapper/templates/job.yaml b/charts/network/charts/network-bootstrapper/templates/job.yaml index e2bf78b..b0e902b 100644 --- a/charts/network/charts/network-bootstrapper/templates/job.yaml +++ b/charts/network/charts/network-bootstrapper/templates/job.yaml @@ -1,3 +1,5 @@ +{{- $artifactSource := default "generated" .Values.artifacts.source -}} +{{- if eq $artifactSource "generated" }} apiVersion: batch/v1 kind: Job metadata: @@ -106,3 +108,4 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} +{{- end }} diff --git a/charts/network/charts/network-bootstrapper/values.yaml b/charts/network/charts/network-bootstrapper/values.yaml index 65bd8ff..e28632c 100644 --- a/charts/network/charts/network-bootstrapper/values.yaml +++ b/charts/network/charts/network-bootstrapper/values.yaml @@ -121,3 +121,22 @@ settings: evmStackSize: # -- (int) Maximum smart-contract bytecode size accepted by the EVM. contractSizeLimit: + +# Artifact sourcing controls for bootstrap data used by the nodes chart. +artifacts: + # -- (string) Determines how Besu network artifacts are populated. Use `generated` to run the job or `external` to supply values manually. + source: generated + external: + # -- (object) Besu genesis document rendered into the `besu-genesis` ConfigMap when `source` equals `external`. + genesis: {} + # -- (list) Collection of enode URIs persisted to the `besu-static-nodes` ConfigMap when `source` equals `external`. + staticNodes: [] + # -- (list) Validator node definitions providing the data expected by the nodes chart. Each entry must include `address`, `publicKey`, `privateKey`, and `enode`. + validators: [] + faucet: + # -- (string) Faucet account address stored in the `besu-faucet-address` ConfigMap when `source` equals `external`. + address: "" + # -- (string) Faucet account public key stored in the `besu-faucet-pubkey` ConfigMap when `source` equals `external`. + publicKey: "" + # -- (string) Faucet private key stored in the `besu-faucet-private-key` Secret when `source` equals `external`. + privateKey: "" diff --git a/charts/network/charts/network-nodes/README.md b/charts/network/charts/network-nodes/README.md index 2d33913..cd562d2 100644 --- a/charts/network/charts/network-nodes/README.md +++ b/charts/network/charts/network-nodes/README.md @@ -135,6 +135,6 @@ A Helm chart for Kubernetes | 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. | +| validatorReplicaCount | int | `nil` | Number of validator node replicas participating in consensus. Leave unset to derive from global.validatorReplicaCount. | | 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/_helpers.tpl b/charts/network/charts/network-nodes/templates/_helpers.tpl index 1113259..f37082a 100644 --- a/charts/network/charts/network-nodes/templates/_helpers.tpl +++ b/charts/network/charts/network-nodes/templates/_helpers.tpl @@ -60,3 +60,29 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +Resolve the number of validator replicas, falling back to global overrides when provided. +*/}} +{{- define "nodes.validatorReplicaCount" -}} +{{- $explicit := .Values.validatorReplicaCount -}} +{{- if not (empty $explicit) -}} +{{- $explicit | int -}} +{{- else -}} +{{- $global := default (dict) .Values.global -}} +{{- $networkGlobal := dict -}} +{{- if and (kindIs "map" $global) (hasKey $global "network") -}} + {{- $networkCandidate := index $global "network" -}} + {{- if kindIs "map" $networkCandidate -}} + {{- $networkGlobal = $networkCandidate -}} + {{- end -}} +{{- end -}} +{{- if and (kindIs "map" $networkGlobal) (hasKey $networkGlobal "validatorReplicaCount") -}} +{{- (index $networkGlobal "validatorReplicaCount") | int -}} +{{- else if and (kindIs "map" $global) (hasKey $global "validatorReplicaCount") -}} +{{- (index $global "validatorReplicaCount") | int -}} +{{- else -}} +4 +{{- end -}} +{{- end -}} +{{- end }} diff --git a/charts/network/charts/network-nodes/templates/statefulset-validator.yaml b/charts/network/charts/network-nodes/templates/statefulset-validator.yaml index a7b371a..085d040 100644 --- a/charts/network/charts/network-nodes/templates/statefulset-validator.yaml +++ b/charts/network/charts/network-nodes/templates/statefulset-validator.yaml @@ -27,8 +27,8 @@ spec: {{- $useClaimTemplate := and $persistenceEnabled (not $useExistingClaim) }} {{- $privateKeyFilename := default "privateKey" .Values.config.privateKeyFilename }} {{- $shouldMountPersistence := $persistenceEnabled }} - {{- $validatorReplicaBudget := .Values.validatorReplicaCount | int }} - replicas: {{ .Values.validatorReplicaCount }} + {{- $validatorReplicaBudget := (include "nodes.validatorReplicaCount" . | int) }} + replicas: {{ $validatorReplicaBudget }} serviceName: {{ include "nodes.fullname" . }} {{- if and $useClaimTemplate (or (ne $whenDeleted "") (ne $whenScaled "")) }} persistentVolumeClaimRetentionPolicy: diff --git a/charts/network/charts/network-nodes/values.yaml b/charts/network/charts/network-nodes/values.yaml index b3d2c0c..eeb35dd 100644 --- a/charts/network/charts/network-nodes/values.yaml +++ b/charts/network/charts/network-nodes/values.yaml @@ -3,8 +3,8 @@ # -- (int) Number of RPC node replicas provisioned via StatefulSet. rpcReplicaCount: 2 -# -- (int) Number of validator node replicas participating in consensus. -validatorReplicaCount: 4 +# -- (int) Number of validator node replicas participating in consensus. Leave unset to derive from global.validatorReplicaCount. +validatorReplicaCount: # Container image configuration shared by validator and RPC pods. image: