Compare commits
4
Commits
1a7598858a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc194b8168
|
||
|
|
885f4160a7
|
||
|
|
d038fe3bab
|
||
|
|
321f7d8c00
|
@@ -0,0 +1,16 @@
|
|||||||
|
MIT No Attribution
|
||||||
|
|
||||||
|
Copyright 2026 Eric Meehan
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||||
|
software and associated documentation files (the "Software"), to deal in the Software
|
||||||
|
without restriction, including without limitation the rights to use, copy, modify,
|
||||||
|
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||||
|
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -1,38 +1,379 @@
|
|||||||
Role Name
|
# Ansible role: Wikideck
|
||||||
=========
|
|
||||||
|
|
||||||
A brief description of the role goes here.
|
Deploy the Wikideck production container to an existing Kubernetes cluster using
|
||||||
|
`kubernetes.core.k8s`. The role creates a single-replica Deployment, a persistent
|
||||||
|
volume claim, a dedicated ServiceAccount, a ClusterIP Service, and a NetworkPolicy.
|
||||||
|
An HTTPS Ingress is optional and disabled by default.
|
||||||
|
|
||||||
Requirements
|
**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. 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).
|
||||||
|
|
||||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
## Requirements
|
||||||
|
|
||||||
Role Variables
|
- A Linux/macOS/WSL controller with Python 3.10+ and Ansible Core 2.16–2.19
|
||||||
--------------
|
(the selected Ansible release may require a newer Python).
|
||||||
|
- Install controller packages from [requirements.txt](requirements.txt) and the
|
||||||
|
collection from [requirements.yml](requirements.yml).
|
||||||
|
- A supported Kubernetes cluster with `apps/v1` and `networking.k8s.io/v1` APIs,
|
||||||
|
a storage provisioner or an existing PVC, and a NetworkPolicy-capable CNI.
|
||||||
|
- 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, immutable image built from Wikideck's production Dockerfile.** There is no
|
||||||
|
assumed official registry image. Set `wikideck_image` to your actual tagged or
|
||||||
|
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. 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.
|
||||||
|
|
||||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
Install dependencies on the controller with `pip install -r requirements.txt`
|
||||||
|
and `ansible-galaxy collection install -r requirements.yml`. Use an isolated Python
|
||||||
|
environment. Windows controllers must run Ansible through WSL or Linux containers.
|
||||||
|
|
||||||
Dependencies
|
## Usage
|
||||||
------------
|
|
||||||
|
|
||||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
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) 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.
|
||||||
|
|
||||||
Example Playbook
|
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.
|
||||||
|
|
||||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
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.
|
||||||
|
The app allows this local origin. Different hosts, ports, schemes, and browser
|
||||||
|
profiles use different browser wallets. This does not bypass mainnet startup gates.
|
||||||
|
|
||||||
- hosts: servers
|
For remote browsers, enable ingress and set `wikideck_public_origin` to exactly
|
||||||
roles:
|
`https://` followed by `wikideck_ingress_host` (no slash or path). HTTPS is needed
|
||||||
- { role: username.rolename, x: 42 }
|
for browser wallet cryptography. Configure HTTP-to-HTTPS redirect at your ingress
|
||||||
|
controller; the example includes the ingress-nginx annotation. Ingress creation
|
||||||
|
does not wait for DNS, certificate issuance, or an externally reachable endpoint.
|
||||||
|
|
||||||
License
|
### Connect validators
|
||||||
-------
|
|
||||||
|
|
||||||
BSD
|
Set `wikideck_peers` to a list of up to 16 compatible peer HTTPS origins, such as
|
||||||
|
`https://peer-a.example.org`. The role joins this list into `WIKIDECK_PEERS`.
|
||||||
|
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.
|
||||||
|
|
||||||
Author Information
|
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
|
||||||
|
those routes. Keep peer JSON responses uncompressed and do not redirect HTTPS
|
||||||
|
peer requests: the client rejects redirects, non-JSON and compressed responses.
|
||||||
|
An interactive login gateway on `/p2p/*` will break the current unauthenticated
|
||||||
|
peer protocol; use deliberate source restrictions/abuse controls instead.
|
||||||
|
|
||||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
Public peers must use HTTPS and public DNS/IP addresses. Wikideck validates every
|
||||||
|
connection's DNS answers and pins the connection to that validated result. Never
|
||||||
|
disable TLS verification to work around a certificate or split-horizon DNS error.
|
||||||
|
The default policy allows public TCP 443 only when peers are configured. For
|
||||||
|
HTTPS peer origins on another port, add that port to
|
||||||
|
`wikideck_network_policy_https_ports`, or supply narrowly scoped explicit egress
|
||||||
|
rules. Browser Wikimedia image fetches are client-side; validators no longer
|
||||||
|
fetch random Wikipedia articles.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
wikideck_peers:
|
||||||
|
- http://other-node.games.svc.cluster.local:80
|
||||||
|
wikideck_allow_private_peers: true
|
||||||
|
wikideck_network_policy_https_to: []
|
||||||
|
wikideck_network_policy_peer_egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: games
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/instance: other-node
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 3001
|
||||||
|
```
|
||||||
|
|
||||||
|
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. 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.
|
||||||
|
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).
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `wikideck_image` | empty, required | Published image with explicit tag or SHA-256 digest; prefer a verified digest. |
|
||||||
|
| `wikideck_name` | `wikideck` | Resource name and instance label; at most 50 characters. |
|
||||||
|
| `wikideck_namespace` | `wikideck` | Namespace for all namespaced resources. |
|
||||||
|
| `wikideck_create_namespace` | `true` | Create the namespace; disable for a pre-provisioned namespace. |
|
||||||
|
| `wikideck_kubeconfig` / `wikideck_kube_context` | `null` | Override controller authentication/context. API TLS verification remains enabled. |
|
||||||
|
| `wikideck_wait` / `wikideck_wait_timeout` | `true` / `900` | Wait for Deployment readiness, timeout in seconds. |
|
||||||
|
| `wikideck_startup_probe_failure_threshold` | `180` | Failed probes at 5-second intervals before startup restart; default budget 900 seconds. |
|
||||||
|
| `wikideck_image_pull_policy` | `IfNotPresent` | `Always`, `IfNotPresent`, or `Never`. |
|
||||||
|
| `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. |
|
||||||
|
| `wikideck_ingress_class_name` | empty | Omit to use the cluster default IngressClass. |
|
||||||
|
| `wikideck_ingress_annotations` | `{}` | Controller/cert-manager-specific configuration. |
|
||||||
|
| `wikideck_tls_secret_name` | `{{ wikideck_name }}-tls` | Existing or externally provisioned TLS Secret. |
|
||||||
|
| `wikideck_existing_claim` | empty | Use an existing PVC instead of creating one. |
|
||||||
|
| `wikideck_storage_size` | `1Gi` | Requested PVC capacity; resizing requires StorageClass support. |
|
||||||
|
| `wikideck_storage_class` | `null` | `null`: cluster default; empty string: no class; otherwise a class name. |
|
||||||
|
| `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. |
|
||||||
|
| `wikideck_network_policy_https_to` | public IPv4/IPv6 ranges, private/link-local excluded | Public peer destinations; omitted when there are no configured peers. |
|
||||||
|
| `wikideck_network_policy_https_ports` | `[443]` | Public peer TCP ports; extend for custom HTTPS origins. |
|
||||||
|
| `wikideck_network_policy_peer_egress` | `[]` | Additional explicit `to` + numeric TCP `ports` rules, emitted only with configured peers. |
|
||||||
|
|
||||||
|
`wikideck_offline` / `WIKIDECK_OFFLINE` have been removed. The current catalog is
|
||||||
|
fixed and genesis-bound, not a live Wikipedia feed. Remove the old role variable;
|
||||||
|
it no longer controls networking. The image's packaged `RANDOMX_BINARY` and default
|
||||||
|
entrypoint are deliberately preserved; no production fake-PoW fallback is added.
|
||||||
|
|
||||||
|
## Storage and safe updates
|
||||||
|
|
||||||
|
- Exactly **one replica** and **Recreate** updates are deliberate and cannot be
|
||||||
|
changed through role variables. Updates briefly interrupt service. Do not add
|
||||||
|
an HPA, manually scale, or attach the same chain PVC to another writer.
|
||||||
|
- `ReadWriteOnce` alone does not enforce a single pod on the same node. Prefer
|
||||||
|
`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.
|
||||||
|
- 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.
|
||||||
|
No privileged/root ownership-fixing init container is used.
|
||||||
|
- The PVC is created before the Deployment without waiting, supporting
|
||||||
|
`WaitForFirstConsumer` storage classes. Existing claims are not modified.
|
||||||
|
- The role never deletes PVCs or Namespaces, and does not assign ownerReferences
|
||||||
|
that would garbage-collect storage with the Deployment. Renaming resources or
|
||||||
|
switching claims does not migrate data or remove previous resources.
|
||||||
|
- Back up the chain before upgrades. Stop the Deployment for a consistent file
|
||||||
|
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.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
The application removes its lock on graceful shutdown but deliberately does
|
||||||
|
**not** automatically recover a stale lock after an OOM kill, forced termination,
|
||||||
|
node loss, or interruption during startup replay. Kubernetes restarts can remain
|
||||||
|
in CrashLoopBackOff until an operator intervenes. This role never deletes the lock
|
||||||
|
in an init container or as an automatic "repair".
|
||||||
|
|
||||||
|
To recover, first stop the Deployment and verify every former owner is gone;
|
||||||
|
fence an unreachable old node before mounting the claim elsewhere. Take a backup,
|
||||||
|
then remove only the confirmed stale lock using an approved maintenance pod or
|
||||||
|
storage procedure. Preserve the ledger, restore the Deployment to one replica,
|
||||||
|
and inspect replay and peer status. Do not automatically remove locks based on
|
||||||
|
their PID: PIDs are reused and isolated between containers.
|
||||||
|
|
||||||
|
Each validator uses native **light-mode RandomX**, requiring a 256 MiB cache plus
|
||||||
|
scratchpad, Node heap, ledger states, and process overhead. The new 1 GiB limit
|
||||||
|
matches the updated upstream Compose starting budget, but must be measured against
|
||||||
|
your ledger, peer load and replay. Watch OOM events, CPU throttling, replay time,
|
||||||
|
and block propagation. Fast miners need **over 2.3 GiB per process** and separate
|
||||||
|
capacity/security planning; this role never launches one or mounts its wallet.
|
||||||
|
|
||||||
|
## Security and networking
|
||||||
|
|
||||||
|
The workload uses a read-only root filesystem, drops all Linux capabilities,
|
||||||
|
disallows privilege escalation, uses runtime-default seccomp, and has no mounted
|
||||||
|
service account token or RBAC grants. Only persistent data and a 32 MiB memory
|
||||||
|
temporary directory are writable. All three HTTP probes target `/healthz` on port
|
||||||
|
3001; Kubernetes does not use the image's Docker HEALTHCHECK. A 30-second grace
|
||||||
|
period accommodates the application's 25-second shutdown deadline.
|
||||||
|
|
||||||
|
NetworkPolicy defaults allow same-namespace clients, cluster DNS in `kube-system`,
|
||||||
|
and public HTTPS only when peer origins are configured. With ingress, allow the ingress
|
||||||
|
controller's namespace/pods as shown in the example. Peer entries are additive
|
||||||
|
OR alternatives: put a namespaceSelector and podSelector in the **same entry** to
|
||||||
|
restrict pods within that namespace. Empty peer lists deny that traffic; empty
|
||||||
|
peer objects are rejected to avoid accidentally allowing every source/destination.
|
||||||
|
|
||||||
|
Standard NetworkPolicy cannot allow peer origins by hostname; public
|
||||||
|
TCP 443 egress is broader than the app's fixed peer allowlist. Narrow CIDRs or use CNI FQDN policies or an
|
||||||
|
egress gateway if required. Policies are additive with any other policies in the
|
||||||
|
namespace and have no effect without an enforcing CNI. NodeLocal DNS, host-network
|
||||||
|
ingress controllers, service meshes, and private proxies may need peer changes.
|
||||||
|
|
||||||
|
TLS terminates at ingress; backend HTTP is private but **not encrypted by this
|
||||||
|
role**. Use a properly configured service mesh if in-cluster encryption is
|
||||||
|
required. TLS keys and registry credentials must be provisioned separately, not
|
||||||
|
committed to playbooks. Use trusted, rotated certificates and retain Kubernetes
|
||||||
|
API certificate verification. The role does not embed secrets or implement its
|
||||||
|
own cryptography; image signature verification and encrypted storage are cluster
|
||||||
|
operator responsibilities. Scan and rebuild the application image regularly.
|
||||||
|
|
||||||
|
Native RandomX uses ordinary memory allocations and secure W^X JIT; the current
|
||||||
|
app does not request huge pages, host devices, executable files in `/tmp`, or
|
||||||
|
privileged capabilities. Keep the existing hardening. Verify actual hashing on
|
||||||
|
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_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.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
[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,
|
||||||
|
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
|
||||||
|
checkout. Syntax-check it with `ansible-playbook -i tests/inventory tests/test.yml --syntax-check`.
|
||||||
|
To deploy to a disposable test namespace, run it with
|
||||||
|
`ansible-playbook -i tests/inventory tests/test.yml -e wikideck_image=YOUR_IMAGE -e wikideck_namespace=wikideck-test`.
|
||||||
|
Run it a second time to verify idempotence. Check Pod readiness, `/healthz`, PVC
|
||||||
|
retention across updates, DNS/peer egress, TLS, and wallet/transfer flows on your
|
||||||
|
chosen cluster. `/healthz` is only process health, not sync readiness. Inspect
|
||||||
|
`/api/peers` for connected peers and `/p2p/status` for matching network, chainId and
|
||||||
|
tip across independently deployed nodes. `/api/peers` exposes configuration;
|
||||||
|
apply your access policy deliberately. Offline tests do not prove CSI/CNI/controller
|
||||||
|
behavior or consensus safety. Mining a real block is a separate explicit-consent
|
||||||
|
test; it must not be part of routine deployment or health probes.
|
||||||
|
|
||||||
|
[tests/runtime-smoke.mjs](tests/runtime-smoke.mjs) is a disposable image-level
|
||||||
|
check: it runs the native known-vector self-test (hashing, **not mining**), checks
|
||||||
|
node API health, and verifies graceful lock cleanup and restart durability. Mount
|
||||||
|
it read-only and invoke Node inside the current image with disposable writable
|
||||||
|
data and no network. It does not use or modify a real chain or wallet.
|
||||||
|
|
||||||
|
Ansible check mode (`--check --diff`) is supported by the Kubernetes module, but
|
||||||
|
still needs API access; creating a new namespace in check mode may require a
|
||||||
|
separate namespace provisioning pass. No uninstall or automatic data deletion is
|
||||||
|
provided.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT-0. See [LICENSE](LICENSE). The application and Wikipedia/Wikimedia content
|
||||||
|
retain their own upstream licenses and attribution requirements.
|
||||||
|
|||||||
+92
-1
@@ -1,2 +1,93 @@
|
|||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# defaults file for ansible-role-wikideck
|
# Required: publish an image built from Wikideck's production Dockerfile.
|
||||||
|
wikideck_image: ""
|
||||||
|
wikideck_name: wikideck
|
||||||
|
wikideck_namespace: wikideck
|
||||||
|
wikideck_create_namespace: true
|
||||||
|
|
||||||
|
# null uses normal kubeconfig discovery, including KUBECONFIG.
|
||||||
|
wikideck_kubeconfig: null
|
||||||
|
wikideck_kube_context: null
|
||||||
|
wikideck_wait: true
|
||||||
|
wikideck_wait_timeout: 900
|
||||||
|
# Full native RandomX ledger revalidation happens before HTTP starts.
|
||||||
|
wikideck_startup_probe_failure_threshold: 180
|
||||||
|
wikideck_image_pull_policy: IfNotPresent
|
||||||
|
# Existing registry Secret names in the application namespace.
|
||||||
|
wikideck_image_pull_secrets: []
|
||||||
|
|
||||||
|
# Static peer origins, at most 16. Empty means an isolated validator, not a network.
|
||||||
|
wikideck_peers: []
|
||||||
|
# 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
|
||||||
|
wikideck_ingress_host: ""
|
||||||
|
wikideck_ingress_class_name: ""
|
||||||
|
wikideck_ingress_annotations: {}
|
||||||
|
# Existing TLS Secret, or one provisioned by cert-manager via annotations.
|
||||||
|
wikideck_tls_secret_name: "{{ wikideck_name }}-tls"
|
||||||
|
|
||||||
|
# Persistence is mandatory; existing claims are never modified by this role.
|
||||||
|
wikideck_existing_claim: ""
|
||||||
|
wikideck_storage_size: 1Gi
|
||||||
|
# null: cluster default; "": no StorageClass; otherwise: named class.
|
||||||
|
wikideck_storage_class: null
|
||||||
|
wikideck_storage_access_modes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
|
||||||
|
wikideck_resources:
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 512Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 1Gi
|
||||||
|
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
|
||||||
|
# Kubernetes NetworkPolicyPeer objects; controller namespaces must be explicit.
|
||||||
|
wikideck_network_policy_ingress_from:
|
||||||
|
- podSelector: {}
|
||||||
|
# DNS is allowed to kube-system on UDP/TCP 53. Adapt for NodeLocal/custom DNS.
|
||||||
|
wikideck_network_policy_dns_to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
# Public peer HTTPS egress, emitted only when wikideck_peers is nonempty.
|
||||||
|
# Standard NetworkPolicy cannot select FQDNs; narrow these CIDRs when practical.
|
||||||
|
wikideck_network_policy_https_ports:
|
||||||
|
- 443
|
||||||
|
# Explicit additional NetworkPolicy egress rules for private/custom-port peers.
|
||||||
|
# Each rule must include nonempty 'to' peers and TCP 'ports'. No rules by default.
|
||||||
|
wikideck_network_policy_peer_egress: []
|
||||||
|
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"
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
- name: Deploy Wikideck
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
wikideck_ingress_class_name: nginx
|
||||||
|
wikideck_ingress_annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
wikideck_network_policy_ingress_from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: ingress-nginx
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
---
|
|
||||||
# handlers file for ansible-role-wikideck
|
|
||||||
+12
-50
@@ -1,52 +1,14 @@
|
|||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
|
---
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
role_name: wikideck
|
||||||
description: your role description
|
author: Eric Meehan
|
||||||
company: your company (optional)
|
description: Deploy the gated Wikideck mainnet candidate or an explicit development node to Kubernetes.
|
||||||
|
license: MIT-0
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
min_ansible_version: "2.16"
|
||||||
# next line and provide a value
|
galaxy_tags:
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
- kubernetes
|
||||||
|
- wikideck
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
- ingress
|
||||||
# - BSD-3-Clause (default)
|
- games
|
||||||
# - MIT
|
|
||||||
# - GPL-2.0-or-later
|
|
||||||
# - GPL-3.0-only
|
|
||||||
# - Apache-2.0
|
|
||||||
# - CC-BY-4.0
|
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
|
||||||
|
|
||||||
min_ansible_version: 2.1
|
|
||||||
|
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
|
||||||
# min_ansible_container_version:
|
|
||||||
|
|
||||||
#
|
|
||||||
# Provide a list of supported platforms, and for each platform a list of versions.
|
|
||||||
# If you don't wish to enumerate all versions for a particular platform, use 'all'.
|
|
||||||
# To view available platforms and versions (or releases), visit:
|
|
||||||
# https://galaxy.ansible.com/api/v1/platforms/
|
|
||||||
#
|
|
||||||
# platforms:
|
|
||||||
# - name: Fedora
|
|
||||||
# versions:
|
|
||||||
# - all
|
|
||||||
# - 25
|
|
||||||
# - name: SomePlatform
|
|
||||||
# versions:
|
|
||||||
# - all
|
|
||||||
# - 1.0
|
|
||||||
# - 7
|
|
||||||
# - 99.99
|
|
||||||
|
|
||||||
galaxy_tags: []
|
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Install on a Linux/macOS/WSL Ansible controller, not on Kubernetes nodes.
|
||||||
|
ansible-core>=2.16,<2.20
|
||||||
|
kubernetes>=24.2.0,<35.0.0
|
||||||
|
PyYAML>=6.0.2,<7.0.0
|
||||||
|
jsonpatch>=1.33,<2.0.0
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
collections:
|
||||||
|
- name: kubernetes.core
|
||||||
|
version: ">=3.0.0,<7.0.0"
|
||||||
+59
-182
@@ -1,199 +1,76 @@
|
|||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# tasks file for ansible-role-wikideck
|
- name: Validate role configuration
|
||||||
- name: Create WikiDeck namespace
|
ansible.builtin.import_tasks: validate.yml
|
||||||
k8s:
|
|
||||||
|
- name: Create application namespace
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
|
||||||
|
context: "{{ wikideck_kube_context | default(omit, true) }}"
|
||||||
state: present
|
state: present
|
||||||
|
apply: true
|
||||||
definition:
|
definition:
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Namespace
|
kind: Namespace
|
||||||
metadata:
|
metadata:
|
||||||
name: wikideck
|
name: "{{ wikideck_namespace }}"
|
||||||
|
when: wikideck_create_namespace | bool
|
||||||
|
|
||||||
- name: Create PVC for MariaDB
|
- name: Create persistent chain storage
|
||||||
k8s:
|
kubernetes.core.k8s:
|
||||||
|
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
|
||||||
|
context: "{{ wikideck_kube_context | default(omit, true) }}"
|
||||||
state: present
|
state: present
|
||||||
definition:
|
apply: true
|
||||||
apiVersion: v1
|
definition: "{{ lookup('ansible.builtin.template', 'pvc.yml.j2') | from_yaml }}"
|
||||||
kind: PersistentVolumeClaim
|
# WaitForFirstConsumer claims cannot bind until the pod is scheduled.
|
||||||
metadata:
|
wait: false
|
||||||
name: mariadb
|
when: wikideck_existing_claim | length == 0
|
||||||
namespace: wikideck
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 2Ti
|
|
||||||
|
|
||||||
- name: Create Deployment for MariaDB
|
- name: Create workload identity and internal service
|
||||||
k8s:
|
kubernetes.core.k8s:
|
||||||
|
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
|
||||||
|
context: "{{ wikideck_kube_context | default(omit, true) }}"
|
||||||
state: present
|
state: present
|
||||||
definition:
|
apply: true
|
||||||
apiVersion: v1
|
definition: "{{ lookup('ansible.builtin.template', item) | from_yaml }}"
|
||||||
kind: Deployment
|
loop:
|
||||||
metadata:
|
- serviceaccount.yml.j2
|
||||||
name: mariadb
|
- service.yml.j2
|
||||||
namespace: wikideck
|
|
||||||
labels:
|
|
||||||
app: mariadb
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: mariadb
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: mariadb
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: mariadb
|
|
||||||
image: mariadb
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /var/lib/mysql
|
|
||||||
ports:
|
|
||||||
- containerPort: 3306
|
|
||||||
env:
|
|
||||||
- name: MARIADB_ROOT_PASSWORD
|
|
||||||
value: "{{ mariadb_root_password }}"
|
|
||||||
- name: MARIADB_DATABASE
|
|
||||||
value: wikideck
|
|
||||||
- name: MARIADB_USER
|
|
||||||
value: wikideck
|
|
||||||
- name: MARIADB_PASSWORD
|
|
||||||
value: "{{ wikideck_mariadb_password }}"
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: mariadb
|
|
||||||
|
|
||||||
- name: Create Service for MariaDB
|
- name: Configure network isolation before starting the application
|
||||||
k8s:
|
kubernetes.core.k8s:
|
||||||
state: present
|
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
|
||||||
definition:
|
context: "{{ wikideck_kube_context | default(omit, true) }}"
|
||||||
apiVersion: v1
|
state: "{{ 'present' if wikideck_network_policy_enabled else 'absent' }}"
|
||||||
kind: Service
|
definition: "{{ lookup('ansible.builtin.template', 'networkpolicy.yml.j2') | from_yaml }}"
|
||||||
metadata:
|
|
||||||
name: mariadb
|
|
||||||
namespace: wikideck
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: mariadb
|
|
||||||
ports:
|
|
||||||
- port: 3306
|
|
||||||
name: mariadb
|
|
||||||
type: ClusterIP
|
|
||||||
|
|
||||||
- name: Create PVC for WikiDeck Mine
|
- name: Deploy the single-writer application
|
||||||
k8s:
|
kubernetes.core.k8s:
|
||||||
|
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
|
||||||
|
context: "{{ wikideck_kube_context | default(omit, true) }}"
|
||||||
state: present
|
state: present
|
||||||
definition:
|
apply: true
|
||||||
apiVersion: v1
|
definition: "{{ lookup('ansible.builtin.template', 'deployment.yml.j2') | from_yaml }}"
|
||||||
kind: PersistentVolumeClaim
|
wait: "{{ wikideck_wait | bool }}"
|
||||||
metadata:
|
wait_timeout: "{{ wikideck_wait_timeout | int }}"
|
||||||
name: wikideck-mine
|
|
||||||
namespace: wikideck
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 2Ti
|
|
||||||
|
|
||||||
- name: Create Deployment for WikiDeck Mine
|
- name: Configure optional TLS ingress
|
||||||
k8s:
|
kubernetes.core.k8s:
|
||||||
|
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
|
||||||
|
context: "{{ wikideck_kube_context | default(omit, true) }}"
|
||||||
state: present
|
state: present
|
||||||
definition:
|
apply: true
|
||||||
apiVersion: v1
|
definition: "{{ lookup('ansible.builtin.template', 'ingress.yml.j2') | from_yaml }}"
|
||||||
kind: Deployment
|
when: wikideck_ingress_enabled | bool
|
||||||
metadata:
|
|
||||||
name: wikideck-mine
|
|
||||||
namespace: wikideck
|
|
||||||
labels:
|
|
||||||
app: wikideck-mine
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: wikideck-mine
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: wikideck-mine
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: wikideck-mine
|
|
||||||
image: ericomeehan/wikideck-mine
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /data
|
|
||||||
ports:
|
|
||||||
- containerPort: 80
|
|
||||||
env:
|
|
||||||
- name: ROLE
|
|
||||||
value: mine
|
|
||||||
- name: DATA_PATH
|
|
||||||
value: /data
|
|
||||||
- name: DIFFICULTY_REQUIREMENT
|
|
||||||
value: 10
|
|
||||||
- name: MINE_LOG_FILE
|
|
||||||
value: /tmp/mine.log
|
|
||||||
- name: MARIADB_HOST
|
|
||||||
value: mariadb
|
|
||||||
- name: MARIADB_DATABASE
|
|
||||||
value: mine
|
|
||||||
- name: MARIADB_USER
|
|
||||||
value: mine
|
|
||||||
- name: MARIADB_PASSWORD
|
|
||||||
value: "{{ wikideck_mine_mariadb_password }}"
|
|
||||||
volumes:
|
|
||||||
- name: data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: wikideck-mine
|
|
||||||
|
|
||||||
- name: Create Service for WikiDeck Mine
|
- name: Remove role ingress when disabled
|
||||||
k8s:
|
kubernetes.core.k8s:
|
||||||
state: present
|
kubeconfig: "{{ wikideck_kubeconfig | default(omit, true) }}"
|
||||||
definition:
|
context: "{{ wikideck_kube_context | default(omit, true) }}"
|
||||||
apiVersion: v1
|
state: absent
|
||||||
kind: Service
|
api_version: networking.k8s.io/v1
|
||||||
metadata:
|
|
||||||
name: wikideck-mine
|
|
||||||
namespace: wikideck
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: wikideck-mine
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
name: http
|
|
||||||
type: ClusterIP
|
|
||||||
|
|
||||||
- name: Create Ingress
|
|
||||||
k8s:
|
|
||||||
state: present
|
|
||||||
definition:
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
namespace: "{{ wikideck_namespace }}"
|
||||||
annotations:
|
name: "{{ wikideck_name }}"
|
||||||
cert-manager.io/cluster-issuer: ca-issuer
|
when: not wikideck_ingress_enabled | bool
|
||||||
name: wikideck-mine
|
|
||||||
namespace: wikideck
|
|
||||||
spec:
|
|
||||||
ingressClassName: nginx
|
|
||||||
rules:
|
|
||||||
- host: wikideck.eom.dev
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- pathType: Prefix
|
|
||||||
path: /mine
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: wikideck-mine
|
|
||||||
port:
|
|
||||||
number: 80
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- wikideck.eom.dev
|
|
||||||
secretName: wikideck
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
|
---
|
||||||
|
- name: Validate deployment settings
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- wikideck_image is string and wikideck_image | length > 0
|
||||||
|
- wikideck_image is match('^[^\s@]+(:[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}|@sha256:[a-f0-9]{64})$')
|
||||||
|
- wikideck_name is string and wikideck_name is match('^[a-z]([a-z0-9-]*[a-z0-9])?$')
|
||||||
|
- wikideck_name | length <= 50
|
||||||
|
- wikideck_namespace is string and wikideck_namespace is match('^[a-z0-9]([a-z0-9-]*[a-z0-9])?$')
|
||||||
|
- wikideck_namespace | length <= 63
|
||||||
|
- wikideck_image_pull_policy in ['Always', 'IfNotPresent', 'Never']
|
||||||
|
- wikideck_wait_timeout | string is match('^[1-9][0-9]*$')
|
||||||
|
- wikideck_startup_probe_failure_threshold is integer and wikideck_startup_probe_failure_threshold > 0
|
||||||
|
- wikideck_resources is mapping
|
||||||
|
- wikideck_resources.requests is defined and wikideck_resources.requests is mapping
|
||||||
|
- wikideck_resources.limits is defined and wikideck_resources.limits is mapping
|
||||||
|
- wikideck_resources.requests.cpu is defined and wikideck_resources.requests.memory is defined
|
||||||
|
- 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
|
||||||
|
- wikideck_storage_access_modes | difference(['ReadWriteOnce', 'ReadWriteOncePod']) | length == 0
|
||||||
|
- wikideck_storage_class is none or wikideck_storage_class is string
|
||||||
|
- wikideck_existing_claim is string
|
||||||
|
- 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
|
||||||
|
- wikideck_create_namespace is boolean
|
||||||
|
- wikideck_wait is boolean
|
||||||
|
fail_msg: >-
|
||||||
|
Provide a tagged or digest-pinned wikideck_image and valid Kubernetes names.
|
||||||
|
Resources need CPU/memory requests and limits; storage must be single-writer.
|
||||||
|
PUBLIC_ORIGIN must be an HTTPS origin without a path or trailing slash.
|
||||||
|
|
||||||
|
- name: Validate TLS ingress settings
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- wikideck_ingress_host is string and wikideck_ingress_host | length > 0
|
||||||
|
- wikideck_ingress_host | length <= 253
|
||||||
|
- wikideck_ingress_host is match('^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$')
|
||||||
|
- wikideck_tls_secret_name is string and wikideck_tls_secret_name | length > 0
|
||||||
|
- wikideck_public_origin == 'https://' ~ wikideck_ingress_host
|
||||||
|
fail_msg: >-
|
||||||
|
Ingress requires a DNS hostname, a TLS Secret name, and wikideck_public_origin
|
||||||
|
set to https:// followed by exactly wikideck_ingress_host.
|
||||||
|
when: wikideck_ingress_enabled | bool
|
||||||
|
|
||||||
|
- name: Validate list settings
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- lookup('ansible.builtin.vars', item) is sequence
|
||||||
|
- lookup('ansible.builtin.vars', item) is not string
|
||||||
|
- lookup('ansible.builtin.vars', item) is not mapping
|
||||||
|
fail_msg: "{{ item }} must be a list."
|
||||||
|
loop:
|
||||||
|
- wikideck_image_pull_secrets
|
||||||
|
- wikideck_tolerations
|
||||||
|
- wikideck_storage_access_modes
|
||||||
|
- wikideck_network_policy_ingress_from
|
||||||
|
- wikideck_network_policy_dns_to
|
||||||
|
- wikideck_network_policy_https_to
|
||||||
|
- wikideck_network_policy_https_ports
|
||||||
|
- wikideck_network_policy_peer_egress
|
||||||
|
- wikideck_peers
|
||||||
|
- wikideck_trusted_proxies
|
||||||
|
|
||||||
|
- name: Validate the static peer allowlist
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- wikideck_peers | length <= 16
|
||||||
|
fail_msg: Wikideck supports at most 16 configured peer origins.
|
||||||
|
|
||||||
|
- name: Validate peer origins without credentials or paths
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- item is string
|
||||||
|
- item | length <= 2048
|
||||||
|
- item is match('^https?://([a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?|\[[0-9a-fA-F:]+\])(:[0-9]{1,5})?$')
|
||||||
|
- 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 cluster peers require wikideck_allow_private_peers=true.
|
||||||
|
Wikideck additionally validates ports and public DNS/IP addresses at runtime.
|
||||||
|
loop: "{{ wikideck_peers }}"
|
||||||
|
|
||||||
|
- name: Validate public peer egress ports
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- item is integer and item > 0 and item <= 65535
|
||||||
|
fail_msg: HTTPS egress ports must be integers in the range 1..65535.
|
||||||
|
loop: "{{ wikideck_network_policy_https_ports }}"
|
||||||
|
|
||||||
|
- name: Validate explicit peer egress rules
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- item is mapping
|
||||||
|
- item.to is defined and item.to is sequence and item.to is not string and item.to is not mapping
|
||||||
|
- item.to | length > 0
|
||||||
|
- item.ports is defined and item.ports is sequence and item.ports is not string and item.ports is not mapping
|
||||||
|
- item.ports | length > 0
|
||||||
|
fail_msg: Peer egress rules require explicit nonempty to and ports lists.
|
||||||
|
loop: "{{ wikideck_network_policy_peer_egress }}"
|
||||||
|
|
||||||
|
- name: Validate explicit peer egress TCP ports
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- item is mapping
|
||||||
|
- item.protocol | default('TCP') == 'TCP'
|
||||||
|
- item.port is defined and item.port is integer and item.port > 0 and item.port <= 65535
|
||||||
|
- item.endPort is not defined
|
||||||
|
fail_msg: Peer egress ports must be individual numeric TCP ports in the range 1..65535.
|
||||||
|
loop: "{{ wikideck_network_policy_peer_egress | map(attribute='ports') | flatten }}"
|
||||||
|
|
||||||
|
- name: Require explicit network policy peers
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- item is mapping
|
||||||
|
- item | length > 0
|
||||||
|
fail_msg: Use an explicit podSelector, namespaceSelector, or ipBlock rather than an empty peer.
|
||||||
|
loop: >-
|
||||||
|
{{ wikideck_network_policy_ingress_from + wikideck_network_policy_dns_to
|
||||||
|
+ wikideck_network_policy_https_to
|
||||||
|
+ (wikideck_network_policy_peer_egress | map(attribute='to') | flatten) }}
|
||||||
|
when: wikideck_network_policy_enabled | bool
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ wikideck_name | to_json }}
|
||||||
|
namespace: {{ wikideck_namespace | to_json }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: wikideck
|
||||||
|
app.kubernetes.io/instance: {{ wikideck_name | to_json }}
|
||||||
|
app.kubernetes.io/managed-by: ansible
|
||||||
|
spec:
|
||||||
|
# One validator per ledger/PVC. Other validators need separate role instances.
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
revisionHistoryLimit: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: wikideck
|
||||||
|
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 }}
|
||||||
|
spec:
|
||||||
|
serviceAccountName: {{ wikideck_name | to_json }}
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
enableServiceLinks: false
|
||||||
|
terminationGracePeriodSeconds: 30
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 1000
|
||||||
|
fsGroup: 1000
|
||||||
|
fsGroupChangePolicy: OnRootMismatch
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
{% if wikideck_image_pull_secrets | length > 0 %}
|
||||||
|
imagePullSecrets:
|
||||||
|
{% for secret in wikideck_image_pull_secrets %}
|
||||||
|
- name: {{ secret | to_json }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
nodeSelector: {{ wikideck_node_selector | to_json }}
|
||||||
|
tolerations: {{ wikideck_tolerations | to_json }}
|
||||||
|
affinity: {{ wikideck_affinity | to_json }}
|
||||||
|
containers:
|
||||||
|
- name: wikideck
|
||||||
|
image: {{ wikideck_image | to_json }}
|
||||||
|
imagePullPolicy: {{ wikideck_image_pull_policy | to_json }}
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 3001
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: NODE_ENV
|
||||||
|
value: production
|
||||||
|
- name: HOST
|
||||||
|
value: "0.0.0.0"
|
||||||
|
- name: PORT
|
||||||
|
value: "3001"
|
||||||
|
- name: WIKIDECK_DATA_DIR
|
||||||
|
value: /app/data
|
||||||
|
- name: WIKIDECK_PEERS
|
||||||
|
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 }}
|
||||||
|
{% endif %}
|
||||||
|
resources: {{ wikideck_resources | to_json }}
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: http
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: {{ wikideck_startup_probe_failure_threshold | int }}
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: http
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: http
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /app/data
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /tmp
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ (wikideck_existing_claim if wikideck_existing_claim | length > 0 else wikideck_name ~ '-data') | to_json }}
|
||||||
|
- name: tmp
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
sizeLimit: 32Mi
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ wikideck_name | to_json }}
|
||||||
|
namespace: {{ wikideck_namespace | to_json }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: wikideck
|
||||||
|
app.kubernetes.io/instance: {{ wikideck_name | to_json }}
|
||||||
|
app.kubernetes.io/managed-by: ansible
|
||||||
|
annotations: {{ wikideck_ingress_annotations | to_json }}
|
||||||
|
spec:
|
||||||
|
{% if wikideck_ingress_class_name | length > 0 %}
|
||||||
|
ingressClassName: {{ wikideck_ingress_class_name | to_json }}
|
||||||
|
{% endif %}
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- {{ wikideck_ingress_host | to_json }}
|
||||||
|
secretName: {{ wikideck_tls_secret_name | to_json }}
|
||||||
|
rules:
|
||||||
|
- host: {{ wikideck_ingress_host | to_json }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ wikideck_name | to_json }}
|
||||||
|
port:
|
||||||
|
name: http
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: {{ wikideck_name | to_json }}
|
||||||
|
namespace: {{ wikideck_namespace | to_json }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: wikideck
|
||||||
|
app.kubernetes.io/instance: {{ wikideck_name | to_json }}
|
||||||
|
app.kubernetes.io/managed-by: ansible
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: wikideck
|
||||||
|
app.kubernetes.io/instance: {{ wikideck_name | to_json }}
|
||||||
|
policyTypes: [Ingress, Egress]
|
||||||
|
{% if wikideck_network_policy_ingress_from | length > 0 %}
|
||||||
|
ingress:
|
||||||
|
- from: {{ wikideck_network_policy_ingress_from | to_json }}
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 3001
|
||||||
|
{% else %}
|
||||||
|
ingress: []
|
||||||
|
{% endif %}
|
||||||
|
{% set public_egress = wikideck_peers | length > 0 and wikideck_network_policy_https_to | length > 0 and wikideck_network_policy_https_ports | length > 0 %}
|
||||||
|
{% if wikideck_network_policy_dns_to | length == 0 and not public_egress and (wikideck_peers | length == 0 or wikideck_network_policy_peer_egress | length == 0) %}
|
||||||
|
egress: []
|
||||||
|
{% else %}
|
||||||
|
egress:
|
||||||
|
{% if wikideck_network_policy_dns_to | length > 0 %}
|
||||||
|
- to: {{ wikideck_network_policy_dns_to | to_json }}
|
||||||
|
ports:
|
||||||
|
- protocol: UDP
|
||||||
|
port: 53
|
||||||
|
- protocol: TCP
|
||||||
|
port: 53
|
||||||
|
{% endif %}
|
||||||
|
{% if public_egress %}
|
||||||
|
- to: {{ wikideck_network_policy_https_to | to_json }}
|
||||||
|
ports:
|
||||||
|
{% for port in wikideck_network_policy_https_ports %}
|
||||||
|
- protocol: TCP
|
||||||
|
port: {{ port | int }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% if wikideck_peers | length > 0 %}
|
||||||
|
{% for rule in wikideck_network_policy_peer_egress %}
|
||||||
|
- {{ rule | to_json }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ (wikideck_name ~ '-data') | to_json }}
|
||||||
|
namespace: {{ wikideck_namespace | to_json }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: wikideck
|
||||||
|
app.kubernetes.io/instance: {{ wikideck_name | to_json }}
|
||||||
|
app.kubernetes.io/managed-by: ansible
|
||||||
|
spec:
|
||||||
|
accessModes: {{ wikideck_storage_access_modes | to_json }}
|
||||||
|
{% if wikideck_storage_class is not none %}
|
||||||
|
storageClassName: {{ wikideck_storage_class | to_json }}
|
||||||
|
{% endif %}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ wikideck_storage_size | to_json }}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ wikideck_name | to_json }}
|
||||||
|
namespace: {{ wikideck_namespace | to_json }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: wikideck
|
||||||
|
app.kubernetes.io/instance: {{ wikideck_name | to_json }}
|
||||||
|
app.kubernetes.io/managed-by: ansible
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: wikideck
|
||||||
|
app.kubernetes.io/instance: {{ wikideck_name | to_json }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# SPDX-License-Identifier: MIT-0
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ wikideck_name | to_json }}
|
||||||
|
namespace: {{ wikideck_namespace | to_json }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: wikideck
|
||||||
|
app.kubernetes.io/instance: {{ wikideck_name | to_json }}
|
||||||
|
app.kubernetes.io/managed-by: ansible
|
||||||
|
automountServiceAccountToken: false
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
- name: Reset validation result
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
wikideck_test_invalid_accepted: false
|
||||||
|
|
||||||
|
- name: Try invalid deployment settings
|
||||||
|
vars:
|
||||||
|
wikideck_image: "{{ invalid_case.image | default('registry.example.com/wikideck:1.0.0') }}"
|
||||||
|
wikideck_name: "{{ invalid_case.name | default('wikideck') }}"
|
||||||
|
wikideck_storage_access_modes: "{{ invalid_case.access_modes | default(['ReadWriteOnce']) }}"
|
||||||
|
wikideck_ingress_enabled: "{{ invalid_case.ingress | default(false) }}"
|
||||||
|
wikideck_ingress_host: "{{ invalid_case.host | default('wikideck.example.com') }}"
|
||||||
|
wikideck_public_origin: "{{ invalid_case.origin | default('') }}"
|
||||||
|
wikideck_resources: "{{ invalid_case.resources | default({'requests': {'cpu': '100m', 'memory': '128Mi'}, 'limits': {'cpu': '1', 'memory': '512Mi'}}) }}"
|
||||||
|
wikideck_peers: "{{ invalid_case.peers | default([]) }}"
|
||||||
|
wikideck_allow_private_peers: "{{ invalid_case.allow_private | default(false) }}"
|
||||||
|
wikideck_startup_probe_failure_threshold: "{{ invalid_case.startup_threshold | default(180) }}"
|
||||||
|
wikideck_network_policy_https_ports: "{{ invalid_case.https_ports | default([443]) }}"
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Record unexpectedly accepted inputs
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
wikideck_test_invalid_accepted: true
|
||||||
|
rescue:
|
||||||
|
- name: Confirm invalid inputs failed validation
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Rejected invalid settings: {{ invalid_case.label }}"
|
||||||
|
|
||||||
|
- name: Assert invalid configuration was rejected
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: not wikideck_test_invalid_accepted
|
||||||
|
fail_msg: "Validation incorrectly accepted {{ invalid_case.label }}"
|
||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
localhost
|
[local]
|
||||||
|
localhost ansible_connection=local
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
---
|
||||||
|
# Offline tests: no Kubernetes credentials or API calls.
|
||||||
|
- name: Validate role configuration and rendered Kubernetes resources
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
vars_files:
|
||||||
|
- ../defaults/main.yml
|
||||||
|
tasks:
|
||||||
|
- name: Test rendering variants
|
||||||
|
ansible.builtin.include_tasks: render_case.yml
|
||||||
|
vars:
|
||||||
|
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
|
||||||
|
wikideck_public_origin: "{{ 'https://wikideck.example.com' if render_case.ingress | default(false) else '' }}"
|
||||||
|
wikideck_ingress_class_name: "{{ render_case.ingress_class | default('') }}"
|
||||||
|
wikideck_ingress_annotations: "{{ render_case.annotations | default({}) }}"
|
||||||
|
wikideck_storage_class: "{{ render_case.storage_class | default(none) }}"
|
||||||
|
wikideck_existing_claim: "{{ render_case.existing_claim | default('') }}"
|
||||||
|
wikideck_image_pull_secrets: "{{ render_case.pull_secrets | default([]) }}"
|
||||||
|
wikideck_network_policy_ingress_from: "{{ render_case.ingress_from | default([{'podSelector': {}}]) }}"
|
||||||
|
wikideck_network_policy_dns_to: "{{ render_case.dns_to | default([{'namespaceSelector': {}}]) }}"
|
||||||
|
wikideck_network_policy_https_to: "{{ render_case.https_to | default([{'ipBlock': {'cidr': '0.0.0.0/0'}}]) }}"
|
||||||
|
wikideck_network_policy_https_ports: "{{ render_case.https_ports | default([443]) }}"
|
||||||
|
wikideck_network_policy_peer_egress: "{{ render_case.peer_egress | default([]) }}"
|
||||||
|
loop:
|
||||||
|
- label: isolated validator defaults
|
||||||
|
- label: TLS ingress and existing storage
|
||||||
|
peers: [https://peer-a.example.org, https://peer-b.example.org]
|
||||||
|
ingress: true
|
||||||
|
ingress_class: nginx
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
existing_claim: existing-chain
|
||||||
|
storage_class: fast-encrypted
|
||||||
|
pull_secrets: [registry-credentials]
|
||||||
|
name: custom-deck
|
||||||
|
namespace: games
|
||||||
|
- label: isolated validator and no storage class
|
||||||
|
storage_class: ""
|
||||||
|
- label: default-deny all traffic
|
||||||
|
ingress_from: []
|
||||||
|
dns_to: []
|
||||||
|
https_to: []
|
||||||
|
- label: public peers with custom HTTPS port and long startup
|
||||||
|
peers: [https://peer.example.org:8443]
|
||||||
|
https_ports: [443, 8443]
|
||||||
|
startup_threshold: 360
|
||||||
|
- label: private HTTP testnet with restricted egress
|
||||||
|
peers: [http://other-node.games.svc.cluster.local:80]
|
||||||
|
allow_private: true
|
||||||
|
https_to: []
|
||||||
|
peer_egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: games
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/instance: other-node
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 3001
|
||||||
|
- label: public peer with all egress explicitly denied
|
||||||
|
peers: [https://peer.example.org]
|
||||||
|
dns_to: []
|
||||||
|
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 }}"
|
||||||
|
|
||||||
|
- name: Test invalid settings are rejected
|
||||||
|
ansible.builtin.include_tasks: invalid_case.yml
|
||||||
|
loop:
|
||||||
|
- label: missing image
|
||||||
|
image: ""
|
||||||
|
- label: untagged image
|
||||||
|
image: registry.example.com/wikideck
|
||||||
|
- label: registry port is not an image tag
|
||||||
|
image: registry.example.com:5000/wikideck
|
||||||
|
- label: malformed image digest
|
||||||
|
image: registry.example.com/wikideck@sha256:abcd
|
||||||
|
- label: invalid resource name
|
||||||
|
name: Invalid_Name
|
||||||
|
- label: unsafe multi-writer storage
|
||||||
|
access_modes: [ReadWriteMany]
|
||||||
|
- label: missing ingress origin
|
||||||
|
ingress: true
|
||||||
|
- label: mismatched ingress origin
|
||||||
|
ingress: true
|
||||||
|
origin: https://another.example.com
|
||||||
|
- label: insecure public origin
|
||||||
|
origin: http://wikideck.example.com
|
||||||
|
- label: origin with path
|
||||||
|
origin: https://wikideck.example.com/path
|
||||||
|
- label: invalid ingress hostname
|
||||||
|
ingress: true
|
||||||
|
host: invalid_host
|
||||||
|
origin: https://wikideck.example.com
|
||||||
|
- label: missing resource limits
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 100m, memory: 128Mi}
|
||||||
|
- label: ambiguous boolean
|
||||||
|
allow_private: "false"
|
||||||
|
- label: wrong list type
|
||||||
|
pull_secrets: not-a-list
|
||||||
|
- label: empty peer allows all traffic
|
||||||
|
ingress_from: [{}]
|
||||||
|
- label: too many peers
|
||||||
|
peers: [https://a.example.org, https://b.example.org, https://c.example.org, https://d.example.org, https://e.example.org, https://f.example.org, https://g.example.org, https://h.example.org, https://i.example.org, https://j.example.org, https://k.example.org, https://l.example.org, https://m.example.org, https://n.example.org, https://o.example.org, https://p.example.org, https://q.example.org]
|
||||||
|
- label: peers must be a list
|
||||||
|
peers: https://peer.example.org
|
||||||
|
- label: HTTP peers need explicit opt-in
|
||||||
|
peers: [http://peer.example.org]
|
||||||
|
- label: credentials in peer URL
|
||||||
|
peers: [https://user:password@peer.example.org]
|
||||||
|
- label: path in peer URL
|
||||||
|
peers: [https://peer.example.org/p2p]
|
||||||
|
- label: query in peer URL
|
||||||
|
peers: ['https://peer.example.org?x=1']
|
||||||
|
- label: comma can inject a second peer
|
||||||
|
peers: ['https://peer.example.org,https://another.example.org']
|
||||||
|
- label: zero startup threshold
|
||||||
|
startup_threshold: 0
|
||||||
|
- label: invalid HTTPS egress port
|
||||||
|
https_ports: [65536]
|
||||||
|
- label: egress rule without destinations
|
||||||
|
peer_egress:
|
||||||
|
- ports: [{protocol: TCP, port: 3001}]
|
||||||
|
- label: egress rule without ports
|
||||||
|
peer_egress:
|
||||||
|
- to: [{podSelector: {}}]
|
||||||
|
- label: egress rule with an empty peer
|
||||||
|
peer_egress:
|
||||||
|
- to: [{}]
|
||||||
|
ports: [{protocol: TCP, port: 3001}]
|
||||||
|
- label: non-TCP peer egress
|
||||||
|
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 }}"
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
- name: Validate rendering case inputs
|
||||||
|
ansible.builtin.include_tasks: ../tasks/validate.yml
|
||||||
|
|
||||||
|
- name: Reset rendered resources
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
wikideck_test_manifests: {}
|
||||||
|
|
||||||
|
- name: Render and parse all Kubernetes templates
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
wikideck_test_manifests: >-
|
||||||
|
{{ wikideck_test_manifests | combine({item: lookup('ansible.builtin.template',
|
||||||
|
playbook_dir ~ '/../templates/' ~ item ~ '.yml.j2') | from_yaml}) }}
|
||||||
|
loop: [deployment, service, serviceaccount, pvc, ingress, networkpolicy]
|
||||||
|
|
||||||
|
- name: Check safety and wiring invariants
|
||||||
|
ansible.builtin.assert:
|
||||||
|
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
|
||||||
|
- wikideck_test_manifests.service.spec.type == 'ClusterIP'
|
||||||
|
- wikideck_test_manifests.service.spec.ports[0].targetPort == 'http'
|
||||||
|
- pod.serviceAccountName == wikideck_name
|
||||||
|
- not pod.automountServiceAccountToken
|
||||||
|
- not wikideck_test_manifests.serviceaccount.automountServiceAccountToken
|
||||||
|
- pod.securityContext.runAsNonRoot and pod.securityContext.runAsUser == 1000
|
||||||
|
- pod.securityContext.fsGroup == 1000
|
||||||
|
- pod.securityContext.seccompProfile.type == 'RuntimeDefault'
|
||||||
|
- container.securityContext.readOnlyRootFilesystem
|
||||||
|
- 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'
|
||||||
|
- container.startupProbe.failureThreshold == wikideck_startup_probe_failure_threshold
|
||||||
|
- container.startupProbe.periodSeconds == 5
|
||||||
|
- container.readinessProbe.httpGet.path == '/healthz'
|
||||||
|
- container.livenessProbe.httpGet.path == '/healthz'
|
||||||
|
- pod.terminationGracePeriodSeconds >= 30
|
||||||
|
- pod.volumes[0].persistentVolumeClaim.claimName == expected_claim
|
||||||
|
- pod.volumes[1].emptyDir.medium == 'Memory'
|
||||||
|
- container.volumeMounts[0].mountPath == '/app/data'
|
||||||
|
- env.HOST == '0.0.0.0' and env.PORT == '3001'
|
||||||
|
- 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
|
||||||
|
- pod.containers | length == 1
|
||||||
|
- (env.PUBLIC_ORIGIN | default('')) == wikideck_public_origin
|
||||||
|
- wikideck_test_manifests.pvc.spec.accessModes == wikideck_storage_access_modes
|
||||||
|
- wikideck_test_manifests.ingress.spec.tls[0].secretName == wikideck_tls_secret_name
|
||||||
|
- wikideck_test_manifests.ingress.spec.rules[0].host == wikideck_ingress_host
|
||||||
|
- wikideck_test_manifests.ingress.spec.rules[0].http.paths[0].backend.service.name == wikideck_name
|
||||||
|
- wikideck_test_manifests.ingress.metadata.annotations == wikideck_ingress_annotations
|
||||||
|
- (wikideck_test_manifests.ingress.spec.ingressClassName | default('')) == wikideck_ingress_class_name
|
||||||
|
- (pod.imagePullSecrets | default([]) | map(attribute='name') | list) == wikideck_image_pull_secrets
|
||||||
|
- wikideck_test_manifests.networkpolicy.spec.policyTypes == ['Ingress', 'Egress']
|
||||||
|
- wikideck_test_manifests.networkpolicy.spec.ingress | length == (1 if wikideck_network_policy_ingress_from | length > 0 else 0)
|
||||||
|
- wikideck_test_manifests.networkpolicy.spec.egress | length == expected_egress_count | int
|
||||||
|
- wikideck_resources.requests.memory == '512Mi'
|
||||||
|
- wikideck_resources.limits.memory == '1Gi'
|
||||||
|
vars:
|
||||||
|
deployment: "{{ wikideck_test_manifests.deployment }}"
|
||||||
|
pod: "{{ deployment.spec.template.spec }}"
|
||||||
|
container: "{{ pod.containers[0] }}"
|
||||||
|
env: "{{ dict(container.env | map(attribute='name') | zip(container.env | map(attribute='value'))) }}"
|
||||||
|
expected_claim: "{{ wikideck_existing_claim if wikideck_existing_claim | length > 0 else wikideck_name ~ '-data' }}"
|
||||||
|
expected_egress_count: >-
|
||||||
|
{{ (1 if wikideck_network_policy_dns_to | length > 0 else 0)
|
||||||
|
+ (1 if wikideck_peers | length > 0 and wikideck_network_policy_https_to | length > 0
|
||||||
|
and wikideck_network_policy_https_ports | length > 0 else 0)
|
||||||
|
+ (wikideck_network_policy_peer_egress | length if wikideck_peers | length > 0 else 0) }}
|
||||||
|
|
||||||
|
- name: Check public peer egress destinations and ports
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- public_rule.to == wikideck_network_policy_https_to
|
||||||
|
- public_rule.ports | map(attribute='port') | list == wikideck_network_policy_https_ports
|
||||||
|
- public_rule.ports | map(attribute='protocol') | unique | list == ['TCP']
|
||||||
|
vars:
|
||||||
|
public_rule: "{{ wikideck_test_manifests.networkpolicy.spec.egress[1 if wikideck_network_policy_dns_to | length > 0 else 0] }}"
|
||||||
|
when:
|
||||||
|
- wikideck_peers | length > 0
|
||||||
|
- wikideck_network_policy_https_to | length > 0
|
||||||
|
- wikideck_network_policy_https_ports | length > 0
|
||||||
|
|
||||||
|
- name: Check explicit peer egress survives unchanged
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: wikideck_test_manifests.networkpolicy.spec.egress[-(wikideck_network_policy_peer_egress | length):] == wikideck_network_policy_peer_egress
|
||||||
|
when:
|
||||||
|
- wikideck_peers | length > 0
|
||||||
|
- wikideck_network_policy_peer_egress | length > 0
|
||||||
|
|
||||||
|
- name: Check omitted default StorageClass
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: wikideck_test_manifests.pvc.spec.storageClassName is not defined
|
||||||
|
when: wikideck_storage_class is none
|
||||||
|
|
||||||
|
- name: Check explicit StorageClass including empty string
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: wikideck_test_manifests.pvc.spec.storageClassName == wikideck_storage_class
|
||||||
|
when: wikideck_storage_class is not none
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// Image-level compatibility test only: no mining, wallets, peers, or live-chain writes.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { spawn, execFileSync } from 'node:child_process';
|
||||||
|
import { once } from 'node:events';
|
||||||
|
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, '');
|
||||||
|
assert.ok(process.env.RANDOMX_BINARY, 'Use the updated production image');
|
||||||
|
execFileSync(process.env.RANDOMX_BINARY, ['--self-test'], {
|
||||||
|
timeout: 120_000,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
async function runNode() {
|
||||||
|
const child = spawn(process.execPath, ['--experimental-strip-types', 'server/index.ts'], {
|
||||||
|
cwd: '/app',
|
||||||
|
env: process.env,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
const exited = once(child, 'exit');
|
||||||
|
let output = '';
|
||||||
|
const timeout = setTimeout(() => child.kill('SIGKILL'), 120_000);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
child.on('error', reject);
|
||||||
|
child.once('exit', (code) => reject(new Error(`Node exited before ready: ${code}\n${output}`)));
|
||||||
|
child.stderr.on('data', (chunk) => { output = (output + chunk).slice(-8192); });
|
||||||
|
child.stdout.on('data', (chunk) => {
|
||||||
|
output = (output + chunk).slice(-8192);
|
||||||
|
if (output.includes('Wikideck RandomX testnet:')) resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
for (const path of ['/healthz', '/p2p/status', '/api/peers']) {
|
||||||
|
const response = await fetch(`http://127.0.0.1:3001${path}`, {
|
||||||
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
});
|
||||||
|
assert.equal(response.status, 200, path);
|
||||||
|
const body = await response.json();
|
||||||
|
if (path === '/healthz') {
|
||||||
|
assert.equal(body.status, 'ok');
|
||||||
|
assert.equal(body.network, 'wikideck-randomx-testnet-6');
|
||||||
|
} else if (path === '/p2p/status') {
|
||||||
|
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 = 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' });
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (child.exitCode === null && child.signalCode === null) {
|
||||||
|
child.kill('SIGKILL');
|
||||||
|
await exited;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await runNode();
|
||||||
|
await runNode();
|
||||||
|
console.log('PASS: native RandomX, node API, graceful lock cleanup and empty-ledger restart');
|
||||||
+9
-4
@@ -1,5 +1,10 @@
|
|||||||
---
|
---
|
||||||
- hosts: localhost
|
# Live-cluster smoke test. Supply wikideck_image with --extra-vars.
|
||||||
remote_user: root
|
- name: Deploy Wikideck to the selected test cluster
|
||||||
roles:
|
hosts: localhost
|
||||||
- ansible-role-wikideck
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Apply the role from this checkout
|
||||||
|
ansible.builtin.include_role:
|
||||||
|
name: "{{ playbook_dir }}/.." # noqa: role-name[path] -- smoke-test the checkout without installation
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
---
|
|
||||||
# vars file for ansible-role-wikideck
|
|
||||||
Reference in New Issue
Block a user