Compare commits

..
2 Commits
Author SHA1 Message Date
eric dc194b8168 Production 2026-11-01 17:11:07 -05:00
eric 885f4160a7 Updated for new version 2026-10-29 11:44:17 -04:00
11 changed files with 525 additions and 60 deletions
+107 -44
View File
@@ -5,13 +5,16 @@ Deploy the Wikideck production container to an existing Kubernetes cluster using
volume claim, a dedicated ServiceAccount, a ClusterIP Service, and a NetworkPolicy.
An HTTPS Ingress is optional and disabled by default.
**Wikideck is an experimental RandomX testnet, not a production-ready public
mainnet.** The updated application has independent validating nodes, fork choice,
native NFTs, and encrypted wallet backup/recovery. It supplies no public bootnodes
or deployed public network. Use test wallets and no valuable assets. This role
**Wikideck is mainnet-first in code but remains an experimental, launch-gated
candidate; the current production decision is NO-GO.** No public network,
bootnodes, independent audit, or production readiness is established. The
application includes validating nodes, fork choice, native NFTs, and encrypted
local-wallet backup/recovery. Use test wallets and no valuable assets. This role
deploys a **validator**, never a miner: mining is an explicitly started native CLI
process outside the browser. Private keys, backups and passwords must stay off
validator pods and out of Ansible variables.
validator pods and out of Ansible variables. See the upstream
[mainnet launch gates](https://github.com/ericomeehan/wikideck/blob/main/docs/mainnet-launch.md)
and [operations/recovery guidance](https://github.com/ericomeehan/wikideck/blob/main/docs/operations-recovery.md).
## Requirements
@@ -24,13 +27,16 @@ validator pods and out of Ansible variables.
- Kubeconfig access to create/update/read the resources in the target namespace,
delete the role's Ingress/NetworkPolicy when disabled, and optionally create a
Namespace. No cluster-admin access or in-cluster workload RBAC is needed.
- **A published image built from Wikideck's production Dockerfile.** There is no
- **A published, immutable image built from Wikideck's production Dockerfile.** There is no
assumed official registry image. Set `wikideck_image` to your actual tagged or
digest-pinned image. The role does not build/push images or install a cluster.
preferably digest-pinned image. The role does not build/push images or install a cluster.
Rebuild from the current source: the image must include Node 24, libstdc++, and
the native RandomX executable, with `RANDOMX_BINARY` set by the image. An old
browser-mining image is incompatible. Build and verify for your worker node
architecture; do not assume a locally built image is multi-architecture.
architecture; do not assume a locally built image is multi-architecture. The
production Dockerfile defaults to mainnet and pins its profile/genesis into the
image manifest; runtime settings cannot change that identity. Testnet is an
explicit development fixture, not a public deployment profile.
- For ingress: an ingress controller, DNS, and a trusted TLS Secret in the app
namespace. Alternatively, configure cert-manager annotations and an existing
issuer. The role does not install cert-manager or create an issuer.
@@ -43,30 +49,23 @@ environment. Windows controllers must run Ansible through WSL or Linux container
Install this checkout into your playbook's role search path as `roles/wikideck`,
or install it using an Ansible Galaxy Git role requirement named `wikideck`.
[examples/playbook.yml](examples/playbook.yml) shows an HTTPS deployment with
ingress-nginx and cert-manager. Replace the image, hostname, peer URLs, class,
issuer, and controller namespace with real values before running it.
```yaml
- name: Deploy a cluster-internal Wikideck instance
hosts: localhost
connection: local
gather_facts: false
roles:
- role: wikideck
vars:
wikideck_image: registry.example.com/games/wikideck:1.0.0
```
[examples/playbook.yml](examples/playbook.yml) is the gated HTTPS deployment
example with ingress-nginx and cert-manager. It requires an approved image
digest, the image's verified manifest/genesis, launch gates, and real peers before
it will change the cluster. There is intentionally no ungated, copy-and-run
mainnet snippet: use a pre-task gate like the example. Empty gates cause the
application to exit before listening, but do not by themselves prevent Kubernetes
resources from being created.
Run the playbook on the controller, using its normal kubeconfig discovery or
`wikideck_kubeconfig` / `wikideck_kube_context` overrides. Do not target every
Kubernetes node: the role operates on the Kubernetes API, not over node SSH.
For a private local preview, forward the Service with
For an explicitly built testnet development image, forward the Service with
`kubectl -n wikideck port-forward service/wikideck 3001:80` and open
<http://127.0.0.1:3001>. Keep the forwarding bind address at its loopback default.
Wikideck already allows this local origin. Different hosts, ports, schemes, and
browser profiles use different browser wallets.
The app allows this local origin. Different hosts, ports, schemes, and browser
profiles use different browser wallets. This does not bypass mainnet startup gates.
For remote browsers, enable ingress and set `wikideck_public_origin` to exactly
`https://` followed by `wikideck_ingress_host` (no slash or path). HTTPS is needed
@@ -81,6 +80,11 @@ Set `wikideck_peers` to a list of up to 16 compatible peer HTTPS origins, such a
An empty list deliberately starts an **isolated validator**. There is no automatic
discovery, bootnode list, or automatic reciprocal configuration: configure the
other operators' nodes too. All nodes must run the same network/genesis/consensus.
For mainnet, application startup additionally requires at least two distinct
peer origins (public HTTPS by default), an arrived image-pinned genesis time, the exact chain-ID
approval, and explicit unaudited-risk acceptance. These switches are not launch
approval from this role; the project currently records a NO-GO decision. The role
does not discover or verify peer independence.
Peer RPC uses `/p2p/*` on the **same port 3001** as the UI/API; no separate TCP/UDP
P2P Service or host networking is required. The existing `/` ingress path forwards
@@ -98,7 +102,8 @@ HTTPS peer origins on another port, add that port to
rules. Browser Wikimedia image fetches are client-side; validators no longer
fetch random Wikipedia articles.
For an explicitly private testnet only, set `wikideck_allow_private_peers: true`
For an explicitly private testnet or operator-controlled mainnet candidate cluster,
set `wikideck_allow_private_peers: true`
and provide `wikideck_network_policy_peer_egress` rules with destination selectors
and numeric TCP ports. This opt-in permits **both private addresses and HTTP** in
the application; it does not automatically open network policy. For example:
@@ -124,7 +129,12 @@ wikideck_network_policy_peer_egress:
The receiving node must separately allow the sender through its ingress policy.
Service DNAT handling depends on the CNI; the example selects the destination pod
and its target port 3001 rather than opening all destinations on Service port 80.
Do not enable private-peer exceptions for public peer configuration.
Do not enable private-peer exceptions for public peer configuration. Updated
mainnet candidate images advertise `privatePeersSupported: true` in their manifest;
older images reject the private opt-in. Browser HTTPS, chain approval, risk
acknowledgment and arrived genesis remain mandatory. Private HTTP RPC is not
authenticated or encrypted; keep it within a trusted cluster boundary or add
independently configured mesh transport protection.
Deploy multiple validators as **separate role instances with different names and
PVCs** (and separate hostnames if using ingress), not by scaling one Deployment.
@@ -132,6 +142,45 @@ Sharing a machine/cluster/storage failure domain does not provide independent
operator security. Empty local chains can synchronize from configured peers;
never seed a second running node by sharing its writable PVC.
### Three-node latest candidate playbook
[playbooks/wikideck.yaml](playbooks/wikideck.yaml) configures the public `wikideck`
node plus `wikideck-peer-a` and `wikideck-peer-b` within the same namespace. Each
uses its own Service and 2 TiB PVC (6 TiB requested in total), reciprocal internal
DNS peer URLs, scoped validator-pod ingress/egress, and public TCP 443 egress for
Wikipedia article and Wikimedia statistics requests used by mining templates.
Standard NetworkPolicy cannot select FQDNs; private address ranges remain excluded
from that public HTTPS rule. Only the public node gets
the `wikideck.eom.dev` TLS ingress. All three are created before readiness waits.
This deploys validators, not miners, and does not establish independent operators.
The playbook uses this role checkout directly, not a separately installed older
Galaxy role. It defaults to `ericomeehan/wikideck:latest` with pull policy `Always`
and `wikideck_candidate_risk_ack: operator-accepts-unaudited-risk`. This explicit
candidate workflow is not the digest-pinned public-launch example. Before creating
ledger resources, a short-lived hardened Kubernetes Job runs the image's
`--network-manifest` command. The playbook generates `wikideck_candidate_chain_id`
from its output and rejects older images without private-peer support, non-mainnet
images, or a future image-baked genesis. The Job is removed even on failure; it
mounts no ledger, wallet secrets, or service-account token.
Rebuild and publish `latest` from the updated application before running this
playbook. The default Dockerfile genesis is November 1, 2026, 00:00 UTC
(`1793491200`), so a normal rebuild can start now without custom genesis arguments.
Older images retain their old genesis; publishing updated source is required.
Never change the chain identity of an existing ledger without a deliberate backup
and migration plan. Do not move `latest` while inspection/deployment is in progress:
all nodes must pull the same image. For reproducible public releases use the
digest-pinned [examples/playbook.yml](examples/playbook.yml) instead. A mutable tag
with `Always` is pulled when pods start. The playbook records the inspected
container image ID in a pod-template annotation, so rerunning after publishing a
new `latest` triggers a rollout; unchanged image IDs do not force a restart.
Preserve existing data and keep the same genesis for updates to an operating chain.
The controller needs Job creation/deletion and Pod/log reads in addition to the
role's permissions. Manifest inspection needs a live cluster, so this playbook
rejects check mode; the role's offline render tests require no cluster credentials.
## Variables
All defaults are in [defaults/main.yml](defaults/main.yml).
@@ -149,6 +198,8 @@ All defaults are in [defaults/main.yml](defaults/main.yml).
| `wikideck_image_pull_secrets` | `[]` | Names of existing registry Secrets in this namespace. |
| `wikideck_peers` | `[]` | Up to 16 static peer origins; empty means isolated. |
| `wikideck_allow_private_peers` | `false` | Explicit test-only private/HTTP peer permission. |
| `wikideck_mainnet_launch` | empty | Exact 64-character chain-ID startup approval; leave empty unless a launch is formally approved. |
| `wikideck_mainnet_risk_ack` | empty | Must equal `operator-accepts-unaudited-risk` to acknowledge residual risk; not a certification. |
| `wikideck_public_origin` | empty | Additional allowed HTTPS browser origin; required with ingress. |
| `wikideck_ingress_enabled` | `false` | Create TLS ingress; disabling removes the role's named ingress. |
| `wikideck_ingress_host` | empty | DNS hostname, without scheme, port, or path. |
@@ -161,6 +212,8 @@ All defaults are in [defaults/main.yml](defaults/main.yml).
| `wikideck_storage_access_modes` | `[ReadWriteOnce]` | `ReadWriteOnce` or `ReadWriteOncePod`; never `ReadWriteMany`. |
| `wikideck_resources` | see defaults | CPU/memory requests `250m`/`512Mi`, limits `1`/`1Gi`; small-validator starting point, not a capacity guarantee. |
| `wikideck_node_selector` / `wikideck_tolerations` / `wikideck_affinity` | `{}` / `[]` / `{}` | Pod placement settings. |
| `wikideck_trusted_proxies` | `[]` | Up to 16 literal proxy IPs/CIDRs; only trust actual edge source addresses after the edge overwrites forwarded headers. |
| `wikideck_ops_port` | empty | Optional port for a diagnostics listener bound only to container loopback; never add it to the Service, Ingress, or NetworkPolicy. |
| `wikideck_network_policy_enabled` | `true` | Manage network isolation; disabling deletes this role's policy. |
| `wikideck_network_policy_ingress_from` | same-namespace pods | List of NetworkPolicy peers allowed to reach TCP 3001; empty list denies all ingress. |
| `wikideck_network_policy_dns_to` | `kube-system` namespace | DNS peers allowed on UDP/TCP 53; adapt for custom/NodeLocal DNS. |
@@ -182,10 +235,14 @@ entrypoint are deliberately preserved; no production fake-PoW fallback is added.
`ReadWriteOncePod` if supported by your CSI driver. Do not force-delete an old
pod on a partitioned node until it has been fenced; Recreate is not a
distributed storage lock.
- `/app/data/testnet/ledger.json` stores the block DAG and pending transfers;
`/app/data/testnet-node.lock` exclusively locks the entire data directory.
The old demo chain is separate, not imported, and has **no migration** to the
new testnet. Back it up first; preferably use a fresh claim for the new network.
- The active ledger is SQLite at `/app/data/<profile>/ledger.sqlite` (with WAL
sidecars); `/app/data/<profile>-node.lock` exclusively locks that profile's data.
A matching legacy `ledger.json` may be imported once and is retained as a
recovery copy. The import path accepts at most 128 MiB and 10,000 blocks; these
are legacy-import limits, not live SQLite disk bounds. Different profiles and
chain identities must not share a writable claim. Preserve and back up existing
data before changing image profile or genesis; incompatible chain data fails
closed rather than resetting automatically.
The process runs
as UID/GID 1000 with `fsGroup: 1000`. Your storage driver must honor ownership
settings, or the existing volume must already be writable by this identity.
@@ -199,15 +256,18 @@ entrypoint are deliberately preserved; no production fake-PoW fallback is added.
backup or use your CSI provider's documented snapshot procedure. Encrypt disks
and backups, define retention, and test restores. Deleting a namespace or PVC
can still destroy data depending on the underlying reclaim policy.
- The current `node:backup` utility creates/verifies/restores ledger-only packages
while the node is stopped. It does not contain browser wallets, secrets, or
configuration, and it does not encrypt the package; store it in restricted,
encrypted off-host storage and verify a restore before relying on it.
- Full native RandomX revalidation runs before the server listens. Increase
`wikideck_startup_probe_failure_threshold` and `wikideck_wait_timeout` together
for your measured replay time, leaving scheduling/image-pull margin. A healthy
empty node does not exercise the native cache or prove peer synchronization.
- The app bounds the ledger to 128 MiB and 10,000 total DAG blocks, with 256
pending transfers. Complete NFT states are retained per DAG node, so RAM may
grow quadratically despite disk bounds. Whole-ledger rewrites and full replay
are **not suitable for a long-lived public chain**; a larger PVC does not fix
those application limits.
- SQLite avoids whole-ledger JSON rewrites, but the node retains DAG records in
memory, rebuilds state roots over the full NFT set, and revalidates proofs at
startup. Long-chain capacity and archival-scale operation remain unresolved;
a larger PVC does not fix memory, replay, or consensus-capacity limits.
### Crash recovery and resource sizing
@@ -268,12 +328,15 @@ your worker architecture and runtime security profile; HTTP health alone cannot
detect every native engine failure. Do not grant privileged mode or disable
seccomp as an untested performance workaround.
Wikideck does not currently configure Express `trust proxy`. Clients behind one
proxy can therefore share an IP-based application rate limit. Review the
application before public exposure; do not try to fix this by blindly trusting
forwarded headers. The current app limits `/api` and `/p2p` together; default
forwarded-header behavior can also produce express-rate-limit validation errors
in logs. The role adds no authentication, WAF, or abuse protection. Eclipse,
`WIKIDECK_TRUSTED_PROXIES` is empty/untrusted by default. Populate
`wikideck_trusted_proxies` only with the literal source addresses/CIDRs actually
used by the trusted edge; the app validates them at startup. The edge must
overwrite forwarded headers, and network controls must prevent direct backend
access. Do not copy a generic proxy range without verifying the cluster path.
The optional `WIKIDECK_OPS_PORT` listener is loopback-only inside the pod and is
not exposed by role resources. It is intended for local/container-exec checks,
not an ingress endpoint. The role adds no authentication, WAF, or abuse
protection. Eclipse,
majority-hashpower, spam/DoS and reorganization risks remain application/protocol
issues; deployment hardening does not make a public mainnet safe.
@@ -282,8 +345,8 @@ issues; deployment hardening does not make a public mainnet safe.
[tests/render.yml](tests/render.yml) runs entirely offline: it validates inputs,
renders/parses every template across default, ingress, existing-claim, storage
class, isolated/public/private peers, custom HTTPS ports, deny-all, longer startup,
and digest-image variants, and checks wiring/security
invariants. It also checks that unsafe/invalid settings are rejected. Run it with
digest-image, launch-gate, proxy-trust and diagnostics variants, and checks
wiring/security invariants. It also checks that unsafe/invalid settings are rejected. Run it with
`ansible-playbook -i tests/inventory tests/render.yml`.
[tests/test.yml](tests/test.yml) is a real-cluster smoke test using the local
+10 -1
View File
@@ -19,8 +19,11 @@ wikideck_image_pull_secrets: []
# Static peer origins, at most 16. Empty means an isolated validator, not a network.
wikideck_peers: []
# Test/private networks only; permits private addresses AND HTTP in the app.
# Operator-controlled test/private candidate clusters; permits private addresses AND HTTP.
wikideck_allow_private_peers: false
# Explicit mainnet candidate startup gates. Never set these before launch approval.
wikideck_mainnet_launch: ""
wikideck_mainnet_risk_ack: ""
# Exact browser origin, without a trailing slash. Required when using ingress.
wikideck_public_origin: ""
wikideck_ingress_enabled: false
@@ -48,6 +51,12 @@ wikideck_resources:
wikideck_node_selector: {}
wikideck_tolerations: []
wikideck_affinity: {}
# Pod-template annotations can trigger a rollout when a mutable image tag changes.
wikideck_pod_annotations: {}
# Trust only literal proxy source IPs/CIDRs whose edge overwrites forwarded headers.
wikideck_trusted_proxies: []
# Optional private operations listener; application binds it to container loopback.
wikideck_ops_port: ""
# Requires a NetworkPolicy-capable CNI. Same-namespace clients only by default.
wikideck_network_policy_enabled: true
+23 -7
View File
@@ -2,16 +2,32 @@
- name: Deploy Wikideck
hosts: localhost
connection: local
gather_facts: false
gather_facts: true
pre_tasks:
- name: Require an approved and ready mainnet candidate release
ansible.builtin.assert:
that:
- wikideck_mainnet_deployment_approved | default(false) | bool
- wikideck_release_image is defined and wikideck_release_image is string and wikideck_release_image is match('^.+@sha256:[a-f0-9]{64}$')
- wikideck_release_peers is defined and wikideck_release_peers is sequence and wikideck_release_peers is not string and wikideck_release_peers | length >= 2
- wikideck_release_peers | unique | list | length == wikideck_release_peers | length
- wikideck_candidate_chain_id is defined and wikideck_candidate_chain_id is match('^[a-f0-9]{64}$')
- wikideck_candidate_risk_ack | default('') == 'operator-accepts-unaudited-risk'
- wikideck_release_genesis_time is defined and (wikideck_release_genesis_time | string) is match('^[1-9][0-9]{8,10}$')
- wikideck_release_genesis_time | int >= 1700000000 and wikideck_release_genesis_time | int <= 4102444800
- wikideck_release_genesis_time | int <= ansible_date_time.epoch | int
fail_msg: >-
Mainnet is currently NO-GO. Supply only an approved digest-pinned image,
verified frozen manifest and arrived genesis, independent public HTTPS
peers, and explicit unaudited-risk acceptance.
roles:
- role: wikideck
vars:
# Replace with your published, scanned image; preferably use @sha256:...
wikideck_image: registry.example.com/games/wikideck:1.0.0
# Replace with real compatible testnet peers. No bootnodes are supplied.
wikideck_peers:
- https://peer-a.example.org
- https://peer-b.example.org
wikideck_image: "{{ wikideck_release_image }}"
wikideck_image_pull_policy: IfNotPresent
wikideck_peers: "{{ wikideck_release_peers }}"
wikideck_mainnet_launch: "{{ wikideck_candidate_chain_id }}"
wikideck_mainnet_risk_ack: "{{ wikideck_candidate_risk_ack }}"
wikideck_ingress_enabled: true
wikideck_ingress_host: wikideck.example.com
wikideck_public_origin: https://wikideck.example.com
+1 -1
View File
@@ -3,7 +3,7 @@
galaxy_info:
role_name: wikideck
author: Eric Meehan
description: Deploy Wikideck to Kubernetes with persistent storage and optional TLS ingress.
description: Deploy the gated Wikideck mainnet candidate or an explicit development node to Kubernetes.
license: MIT-0
min_ansible_version: "2.16"
galaxy_tags:
+268
View File
@@ -0,0 +1,268 @@
- name: Deploy a three-node private Wikideck mainnet candidate
hosts: alpha-control-plane
become: true
vars_files:
- ~/Desktop/secrets.yaml
vars:
# This is an operator-controlled candidate, not an independent public network.
wikideck_release_image: ericomeehan/wikideck:latest
wikideck_candidate_risk_ack: operator-accepts-unaudited-risk
wikideck_cluster_nodes:
- wikideck
- wikideck-peer-a
- wikideck-peer-b
wikideck_namespace: wikideck
wikideck_image: "{{ wikideck_release_image }}"
wikideck_image_pull_policy: Always
# Roll out latest only when the inspected image changes, not on every run.
wikideck_pod_annotations:
wikideck.eom.dev/image-id: "{{ wikideck_manifest_image_id }}"
wikideck_allow_private_peers: true
wikideck_mainnet_launch: "{{ wikideck_candidate_chain_id }}"
wikideck_mainnet_risk_ack: "{{ wikideck_candidate_risk_ack }}"
# Internal validators share the browser origin, but only the first has ingress.
wikideck_public_origin: https://wikideck.eom.dev
wikideck_ingress_host: wikideck.eom.dev
wikideck_ingress_class_name: nginx
wikideck_ingress_annotations:
cert-manager.io/cluster-issuer: ca-issuer
nginx.ingress.kubernetes.io/ssl-redirect: "true"
wikideck_storage_size: 2Ti
wikideck_storage_class: nfs-client
# Populate only after confirming the real proxy source IPs/CIDRs.
wikideck_trusted_proxies: "{{ wikideck_ingress_proxy_sources | default([]) }}"
# Start every node before waiting; the first must not wait for absent peers.
wikideck_wait: false
wikideck_wait_timeout: 900
wikideck_network_policy_ingress_from:
- podSelector:
matchLabels:
app.kubernetes.io/name: wikideck
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
# Mining templates fetch Wikipedia articles and Wikimedia statistics over HTTPS.
# Standard NetworkPolicy cannot select FQDNs; permit public TCP 443, not private ranges.
wikideck_network_policy_https_to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 100.64.0.0/10
- 127.0.0.0/8
- 169.254.0.0/16
- 172.16.0.0/12
- 192.168.0.0/16
- ipBlock:
cidr: "::/0"
except:
- "::1/128"
- "fc00::/7"
- "fe80::/10"
wikideck_network_policy_peer_egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: wikideck
ports:
- protocol: TCP
port: 3001
pre_tasks:
- name: Validate candidate cluster inputs
ansible.builtin.assert:
that:
- not ansible_check_mode
- wikideck_release_image is string and wikideck_release_image is match('^[^\s@]+:latest$')
- wikideck_cluster_nodes is sequence and wikideck_cluster_nodes is not string and wikideck_cluster_nodes is not mapping
- wikideck_cluster_nodes | length == 3
- wikideck_cluster_nodes | unique | list | length == 3
- wikideck_cluster_nodes | select('match', '^[a-z]([a-z0-9-]*[a-z0-9])?$') | list | length == 3
- wikideck_cluster_nodes | map('length') | max <= 50
- wikideck_candidate_risk_ack | default('') == 'operator-accepts-unaudited-risk'
fail_msg: >-
This candidate playbook requires a latest-tag image, three unique node
names, and operator-accepts-unaudited-risk. Check mode cannot inspect the
image manifest; use the offline render tests instead.
run_once: true
- name: Initialize manifest registry Secret references
ansible.builtin.set_fact:
wikideck_manifest_pull_secrets: []
run_once: true
- name: Convert registry Secret names to Kubernetes references
ansible.builtin.set_fact:
wikideck_manifest_pull_secrets: "{{ wikideck_manifest_pull_secrets + [{'name': item}] }}"
loop: "{{ wikideck_image_pull_secrets | default([]) }}"
run_once: true
- name: Create namespace for read-only image inspection
kubernetes.core.k8s:
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
context: "{{ wikideck_kube_context | default(omit, true) }}"
state: present
apply: true
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ wikideck_namespace }}"
run_once: true
- name: Inspect latest without opening a ledger or starting a validator
run_once: true
block:
- name: Remove a stale manifest inspection Job
kubernetes.core.k8s:
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
context: "{{ wikideck_kube_context | default(omit, true) }}"
state: absent
api_version: batch/v1
kind: Job
namespace: "{{ wikideck_namespace }}"
name: wikideck-candidate-manifest
wait: true
- name: Run the image manifest command
kubernetes.core.k8s:
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
context: "{{ wikideck_kube_context | default(omit, true) }}"
state: present
wait: true
wait_timeout: 300
wait_condition:
type: Complete
status: "True"
definition:
apiVersion: batch/v1
kind: Job
metadata:
name: wikideck-candidate-manifest
namespace: "{{ wikideck_namespace }}"
spec:
backoffLimit: 0
activeDeadlineSeconds: 240
ttlSecondsAfterFinished: 600
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
enableServiceLinks: false
imagePullSecrets: "{{ wikideck_manifest_pull_secrets }}"
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: manifest
image: "{{ wikideck_release_image }}"
imagePullPolicy: Always
command:
- node
- --experimental-strip-types
- server/index.ts
- --network-manifest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {cpu: "1", memory: 512Mi}
- name: Locate the completed manifest pod
kubernetes.core.k8s_info:
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
context: "{{ wikideck_kube_context | default(omit, true) }}"
kind: Pod
namespace: "{{ wikideck_namespace }}"
label_selectors:
- job-name=wikideck-candidate-manifest
register: wikideck_manifest_pods
- name: Require exactly one completed manifest pod
ansible.builtin.assert:
that:
- wikideck_manifest_pods.resources | length == 1
- wikideck_manifest_pods.resources[0].status.phase == 'Succeeded'
- name: Read the public image manifest
kubernetes.core.k8s_log:
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
context: "{{ wikideck_kube_context | default(omit, true) }}"
namespace: "{{ wikideck_namespace }}"
name: "{{ wikideck_manifest_pods.resources[0].metadata.name }}"
container: manifest
register: wikideck_manifest_log
- name: Parse the image identity
ansible.builtin.set_fact:
wikideck_candidate_manifest: "{{ wikideck_manifest_log.log | from_json }}"
- name: Require a compatible image and an arrived image-baked genesis
ansible.builtin.assert:
that:
- wikideck_candidate_manifest.profile == 'mainnet'
- wikideck_candidate_manifest.network == 'wikideck-randomx-mainnet-1'
- wikideck_candidate_manifest.privatePeersSupported | default(false) == true
- wikideck_candidate_manifest.chainId is string and wikideck_candidate_manifest.chainId is match('^[a-f0-9]{64}$')
- wikideck_candidate_manifest.genesisTime is integer
- wikideck_candidate_manifest.genesisTime >= 1700000000 and wikideck_candidate_manifest.genesisTime <= 4102444800
- wikideck_candidate_manifest.genesisTime <= ansible_date_time.epoch | int
fail_msg: >-
Rebuild and publish latest from code supporting private candidate
peers, with a deliberately chosen, arrived mainnet genesis frozen
into the image. Runtime genesis overrides are not supported.
- name: Generate the candidate chain approval from this image
ansible.builtin.set_fact:
wikideck_candidate_chain_id: "{{ wikideck_candidate_manifest.chainId }}"
wikideck_release_genesis_time: "{{ wikideck_candidate_manifest.genesisTime }}"
wikideck_manifest_image_id: "{{ wikideck_manifest_pods.resources[0].status.containerStatuses[0].imageID }}"
always:
- name: Remove the manifest inspection Job and its pod
kubernetes.core.k8s:
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
context: "{{ wikideck_kube_context | default(omit, true) }}"
state: absent
api_version: batch/v1
kind: Job
namespace: "{{ wikideck_namespace }}"
name: wikideck-candidate-manifest
wait: true
tasks:
- name: Deploy each validator with its own service and persistent ledger
ansible.builtin.include_role:
# Use this updated checkout rather than an older installed Galaxy role.
name: "{{ playbook_dir }}/.." # noqa: role-name[path]
vars:
wikideck_name: "{{ wikideck_cluster_node }}"
wikideck_existing_claim: ""
wikideck_ingress_enabled: "{{ wikideck_cluster_node == wikideck_cluster_nodes[0] }}"
wikideck_peers: >-
{{ wikideck_cluster_nodes | reject('equalto', wikideck_cluster_node)
| map('regex_replace', '^', 'http://')
| map('regex_replace', '$', '.' ~ wikideck_namespace ~ '.svc.cluster.local:80') | list }}
loop: "{{ wikideck_cluster_nodes }}"
loop_control:
loop_var: wikideck_cluster_node
run_once: true
- name: Wait for all three validators after creating the full peer topology
kubernetes.core.k8s_info:
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
context: "{{ wikideck_kube_context | default(omit, true) }}"
api_version: apps/v1
kind: Deployment
namespace: "{{ wikideck_namespace }}"
name: "{{ item }}"
wait: true
wait_timeout: "{{ wikideck_wait_timeout | int }}"
wait_condition:
type: Available
status: "True"
loop: "{{ wikideck_cluster_nodes }}"
run_once: true
+13 -1
View File
@@ -19,6 +19,7 @@
- wikideck_resources.limits.cpu is defined and wikideck_resources.limits.memory is defined
- wikideck_node_selector is mapping
- wikideck_affinity is mapping
- wikideck_pod_annotations is mapping
- wikideck_ingress_annotations is mapping
- wikideck_storage_size is string and wikideck_storage_size | length > 0
- wikideck_storage_access_modes | length > 0
@@ -28,6 +29,16 @@
- wikideck_existing_claim | length == 0 or wikideck_existing_claim is match('^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$')
- wikideck_public_origin is string
- wikideck_public_origin | length == 0 or wikideck_public_origin is match('^https://[a-z0-9.-]+(:[0-9]+)?$')
- wikideck_mainnet_launch is string
- wikideck_mainnet_launch | length == 0 or wikideck_mainnet_launch is match('^[a-f0-9]{64}$')
- wikideck_mainnet_risk_ack is string
- wikideck_mainnet_risk_ack in ['', 'operator-accepts-unaudited-risk']
- wikideck_trusted_proxies is sequence and wikideck_trusted_proxies is not string and wikideck_trusted_proxies is not mapping
- wikideck_trusted_proxies | length <= 16
- wikideck_trusted_proxies | select('string') | list | length == wikideck_trusted_proxies | length
- wikideck_trusted_proxies | select('match', '^[0-9a-fA-F:.]+(/[0-9]{1,3})?$') | list | length == wikideck_trusted_proxies | length
- wikideck_ops_port is string or wikideck_ops_port is integer
- wikideck_ops_port | string | length == 0 or ((wikideck_ops_port | string) is match('^[1-9][0-9]{0,4}$') and wikideck_ops_port | int <= 65535)
- wikideck_ingress_enabled is boolean
- wikideck_network_policy_enabled is boolean
- wikideck_allow_private_peers is boolean
@@ -68,6 +79,7 @@
- wikideck_network_policy_https_ports
- wikideck_network_policy_peer_egress
- wikideck_peers
- wikideck_trusted_proxies
- name: Validate the static peer allowlist
ansible.builtin.assert:
@@ -84,7 +96,7 @@
- wikideck_allow_private_peers or item is match('^https://')
fail_msg: >-
Peers must be HTTPS origins without credentials, paths, commas, queries or
fragments. Private/HTTP test peers require wikideck_allow_private_peers=true.
fragments. Private/HTTP cluster peers require wikideck_allow_private_peers=true.
Wikideck additionally validates ports and public DNS/IP addresses at runtime.
loop: "{{ wikideck_peers }}"
+13
View File
@@ -20,6 +20,7 @@ spec:
app.kubernetes.io/instance: {{ wikideck_name | to_json }}
template:
metadata:
annotations: {{ wikideck_pod_annotations | to_json }}
labels:
app.kubernetes.io/name: wikideck
app.kubernetes.io/instance: {{ wikideck_name | to_json }}
@@ -71,6 +72,18 @@ spec:
value: {{ (wikideck_peers | join(',')) | to_json }}
- name: WIKIDECK_ALLOW_PRIVATE_PEERS
value: {{ ('1' if wikideck_allow_private_peers else '0') | to_json }}
- name: WIKIDECK_MAINNET_LAUNCH
value: {{ wikideck_mainnet_launch | to_json }}
- name: WIKIDECK_MAINNET_RISK_ACK
value: {{ wikideck_mainnet_risk_ack | to_json }}
{% if wikideck_trusted_proxies | length > 0 %}
- name: WIKIDECK_TRUSTED_PROXIES
value: {{ (wikideck_trusted_proxies | join(',')) | to_json }}
{% endif %}
{% if wikideck_ops_port | string | length > 0 %}
- name: WIKIDECK_OPS_PORT
value: {{ (wikideck_ops_port | string) | to_json }}
{% endif %}
{% if wikideck_public_origin | length > 0 %}
- name: PUBLIC_ORIGIN
value: {{ wikideck_public_origin | to_json }}
+4
View File
@@ -19,6 +19,10 @@
wikideck_network_policy_peer_egress: "{{ invalid_case.peer_egress | default([]) }}"
wikideck_image_pull_secrets: "{{ invalid_case.pull_secrets | default([]) }}"
wikideck_network_policy_ingress_from: "{{ invalid_case.ingress_from | default([{'podSelector': {}}]) }}"
wikideck_trusted_proxies: "{{ invalid_case.trusted_proxies | default([]) }}"
wikideck_ops_port: "{{ invalid_case.ops_port | default('') }}"
wikideck_mainnet_launch: "{{ invalid_case.mainnet_launch | default('') }}"
wikideck_mainnet_risk_ack: "{{ invalid_case.risk_ack | default('') }}"
block:
- name: Run role input validation
ansible.builtin.include_tasks: ../tasks/validate.yml
+67
View File
@@ -13,8 +13,14 @@
wikideck_image: "{{ render_case.image | default('registry.example.com/wikideck:1.0.0') }}"
wikideck_name: "{{ render_case.name | default('wikideck') }}"
wikideck_namespace: "{{ render_case.namespace | default('wikideck') }}"
wikideck_image_pull_policy: "{{ render_case.pull_policy | default('IfNotPresent') }}"
wikideck_pod_annotations: "{{ render_case.pod_annotations | default({}) }}"
wikideck_peers: "{{ render_case.peers | default([]) }}"
wikideck_allow_private_peers: "{{ render_case.allow_private | default(false) }}"
wikideck_mainnet_launch: "{{ render_case.mainnet_launch | default('') }}"
wikideck_mainnet_risk_ack: "{{ render_case.risk_ack | default('') }}"
wikideck_trusted_proxies: "{{ render_case.trusted_proxies | default([]) }}"
wikideck_ops_port: "{{ render_case.ops_port | default('') }}"
wikideck_startup_probe_failure_threshold: "{{ render_case.startup_threshold | default(180) }}"
wikideck_ingress_enabled: "{{ render_case.ingress | default(false) }}"
wikideck_ingress_host: wikideck.example.com
@@ -73,6 +79,59 @@
https_to: []
- label: digest-pinned image
image: registry.example.com/wikideck@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- label: proxy trust and private diagnostics
peers: [https://peer-a.example.org, https://peer-b.example.org]
trusted_proxies: [10.20.30.40, 2001:db8::/64]
ops_port: "9090"
- label: exact mainnet launch configuration
mainnet_launch: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
risk_ack: operator-accepts-unaudited-risk
- label: latest private mainnet candidate public node
image: ericomeehan/wikideck:latest
pull_policy: Always
pod_annotations:
wikideck.eom.dev/image-id: docker-pullable://ericomeehan/wikideck@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
peers: [http://wikideck-peer-a.wikideck.svc.cluster.local:80, http://wikideck-peer-b.wikideck.svc.cluster.local:80]
allow_private: true
ingress: true
mainnet_launch: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
risk_ack: operator-accepts-unaudited-risk
# Public HTTPS is needed for Wikipedia mining templates and statistics.
https_to:
- ipBlock:
cidr: 0.0.0.0/0
except: [10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16]
- ipBlock:
cidr: "::/0"
except: ["::1/128", "fc00::/7", "fe80::/10"]
ingress_from:
- podSelector:
matchLabels:
app.kubernetes.io/name: wikideck
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
peer_egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: wikideck
ports: [{protocol: TCP, port: 3001}]
- label: latest private mainnet candidate internal peer
image: ericomeehan/wikideck:latest
pull_policy: Always
name: wikideck-peer-a
peers: [http://wikideck.wikideck.svc.cluster.local:80, http://wikideck-peer-b.wikideck.svc.cluster.local:80]
allow_private: true
mainnet_launch: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
risk_ack: operator-accepts-unaudited-risk
https_to: []
peer_egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: wikideck
ports: [{protocol: TCP, port: 3001}]
loop_control:
loop_var: render_case
label: "{{ render_case.label }}"
@@ -146,6 +205,14 @@
peer_egress:
- to: [{podSelector: {}}]
ports: [{protocol: UDP, port: 3001}]
- label: malformed mainnet chain approval
mainnet_launch: not-a-chain-id
- label: unsupported mainnet risk acknowledgement
risk_ack: operator-accepts-risk
- label: proxy entry is not an IP or CIDR
trusted_proxies: [proxy.example.org]
- label: operations port out of range
ops_port: "70000"
loop_control:
loop_var: invalid_case
label: "{{ invalid_case.label }}"
+6
View File
@@ -18,6 +18,7 @@
that:
- deployment.spec.replicas == 1
- deployment.spec.strategy.type == 'Recreate'
- deployment.spec.template.metadata.annotations == wikideck_pod_annotations
- deployment.spec.selector.matchLabels == deployment.spec.template.metadata.labels
- wikideck_test_manifests.service.spec.selector == deployment.spec.selector.matchLabels
- wikideck_test_manifests.networkpolicy.spec.podSelector.matchLabels == deployment.spec.selector.matchLabels
@@ -33,6 +34,7 @@
- not container.securityContext.allowPrivilegeEscalation
- container.securityContext.capabilities.drop == ['ALL']
- container.image == wikideck_image
- container.imagePullPolicy == wikideck_image_pull_policy
- container.resources == wikideck_resources
- container.ports[0].containerPort == 3001
- container.startupProbe.httpGet.path == '/healthz'
@@ -48,6 +50,10 @@
- env.WIKIDECK_DATA_DIR == '/app/data' and env.NODE_ENV == 'production'
- env.WIKIDECK_PEERS == wikideck_peers | join(',')
- env.WIKIDECK_ALLOW_PRIVATE_PEERS == ('1' if wikideck_allow_private_peers else '0')
- env.WIKIDECK_MAINNET_LAUNCH == wikideck_mainnet_launch
- env.WIKIDECK_MAINNET_RISK_ACK == wikideck_mainnet_risk_ack
- (env.WIKIDECK_TRUSTED_PROXIES | default('')) == wikideck_trusted_proxies | join(',')
- (env.WIKIDECK_OPS_PORT | default('')) == (wikideck_ops_port | string)
- env.WIKIDECK_OFFLINE is not defined
- env.RANDOMX_BINARY is not defined
- container.command is not defined and container.args is not defined
+13 -6
View File
@@ -2,7 +2,8 @@
import assert from 'node:assert/strict';
import { spawn, execFileSync } from 'node:child_process';
import { once } from 'node:events';
import { readFile, access } from 'node:fs/promises';
import { access } from 'node:fs/promises';
import { DatabaseSync } from 'node:sqlite';
assert.equal(process.env.WIKIDECK_DATA_DIR, '/app/data');
assert.equal(process.env.WIKIDECK_PEERS, '');
@@ -39,17 +40,23 @@ async function runNode() {
const body = await response.json();
if (path === '/healthz') {
assert.equal(body.status, 'ok');
assert.equal(body.network, 'wikideck-randomx-testnet-2');
assert.equal(body.network, 'wikideck-randomx-testnet-6');
} else if (path === '/p2p/status') {
assert.equal(body.network, 'wikideck-randomx-testnet-2');
assert.equal(body.network, 'wikideck-randomx-testnet-6');
assert.equal(body.size, 0);
assert.match(body.chainId, /^[a-f0-9]{64}$/);
} else assert.deepEqual(body.peers, []);
}
await access('/app/data/testnet-node.lock');
const ledger = JSON.parse(await readFile('/app/data/testnet/ledger.json', 'utf8'));
assert.equal(ledger.network, 'wikideck-randomx-testnet-2');
assert.deepEqual(ledger.blocks, []);
const ledger = new DatabaseSync('/app/data/testnet/ledger.sqlite');
try {
const metadata = ledger.prepare('SELECT value FROM metadata WHERE key = ?').get('network');
assert.equal(metadata.value, 'wikideck-randomx-testnet-6');
assert.equal(ledger.prepare('SELECT COUNT(*) AS count FROM blocks').get().count, 0);
assert.equal(ledger.prepare('SELECT COUNT(*) AS count FROM pending_transfers').get().count, 0);
} finally {
ledger.close();
}
child.kill('SIGTERM');
assert.deepEqual(await exited, [0, null], output);
await assert.rejects(access('/app/data/testnet-node.lock'), { code: 'ENOENT' });