v2
This commit is contained in:
@@ -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,187 @@
|
|||||||
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 a prototype, not a hardened public service.** This role does not add
|
||||||
------------
|
application authentication, distributed consensus, a database, or wallet recovery.
|
||||||
|
Use test wallets and restrict access. Mining happens in the browser; the server
|
||||||
|
coordinates the chain and verifies proofs.
|
||||||
|
|
||||||
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 image built from Wikideck's production Dockerfile.** There is no
|
||||||
|
assumed official registry image. Set `wikideck_image` to your actual tagged or
|
||||||
|
digest-pinned image. The role does not build/push images or install a cluster.
|
||||||
|
- 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) shows an HTTPS deployment with
|
||||||
|
ingress-nginx and cert-manager. Replace the image, hostname, class, issuer, and
|
||||||
|
controller namespace with real values before running it.
|
||||||
|
|
||||||
Example Playbook
|
```yaml
|
||||||
----------------
|
- name: Deploy a cluster-internal Wikideck instance
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
roles:
|
||||||
|
- role: wikideck
|
||||||
|
vars:
|
||||||
|
wikideck_image: registry.example.com/games/wikideck:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
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.
|
||||||
|
|
||||||
- hosts: servers
|
For a private local preview, forward the Service with
|
||||||
roles:
|
`kubectl -n wikideck port-forward service/wikideck 3001:80` and open
|
||||||
- { role: username.rolename, x: 42 }
|
<http://127.0.0.1:3001>. Keep the forwarding bind address at its loopback default.
|
||||||
|
Wikideck already allows this local origin. Different hosts, ports, schemes, and
|
||||||
|
browser profiles use different browser wallets.
|
||||||
|
|
||||||
License
|
For remote browsers, enable ingress and set `wikideck_public_origin` to exactly
|
||||||
-------
|
`https://` followed by `wikideck_ingress_host` (no slash or path). HTTPS is needed
|
||||||
|
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.
|
||||||
|
|
||||||
BSD
|
## Variables
|
||||||
|
|
||||||
Author Information
|
All defaults are in [defaults/main.yml](defaults/main.yml).
|
||||||
------------------
|
|
||||||
|
|
||||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
| 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` / `300` | Wait for Deployment readiness, timeout in seconds. |
|
||||||
|
| `wikideck_image_pull_policy` | `IfNotPresent` | `Always`, `IfNotPresent`, or `Never`. |
|
||||||
|
| `wikideck_image_pull_secrets` | `[]` | Names of existing registry Secrets in this namespace. |
|
||||||
|
| `wikideck_offline` | `false` | Starter articles without live Wikipedia requests. |
|
||||||
|
| `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 `100m`/`128Mi`, limits `1`/`512Mi`. |
|
||||||
|
| `wikideck_node_selector` / `wikideck_tolerations` / `wikideck_affinity` | `{}` / `[]` / `{}` | Pod placement settings. |
|
||||||
|
| `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 | TCP 443 egress peers; omitted in offline mode. |
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- `/app/data` contains the chain and signed transfer journal. 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.
|
||||||
|
- Chain verification runs at startup. For large chains, review resources,
|
||||||
|
startup probe budget (300 seconds), and readiness wait timeout. JSON storage
|
||||||
|
is unbounded; this role cannot solve Wikideck's persistence scalability limits.
|
||||||
|
|
||||||
|
## 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 for Wikipedia. With ingress, explicitly 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 Wikipedia by hostname; the default public
|
||||||
|
TCP 443 egress is broader than a domain allowlist. 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.
|
||||||
|
|
||||||
|
Wikideck does not currently configure Express `trust proxy`. Clients behind one
|
||||||
|
proxy can therefore share an IP-based application rate limit. Review the
|
||||||
|
application before public exposure; do not try to fix this by blindly trusting
|
||||||
|
forwarded headers. The role adds no authentication, WAF, or abuse protection.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
[tests/render.yml](tests/render.yml) runs entirely offline: it validates inputs,
|
||||||
|
renders/parses every template across default, ingress, existing-claim, storage
|
||||||
|
class, offline, deny-all, and digest-image 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/HTTPS egress, TLS, and browser mining/transfer flows
|
||||||
|
on your chosen cluster. Offline tests do not prove CSI/CNI/controller behavior.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
+72
-1
@@ -1,2 +1,73 @@
|
|||||||
|
# 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: 300
|
||||||
|
wikideck_image_pull_policy: IfNotPresent
|
||||||
|
# Existing registry Secret names in the application namespace.
|
||||||
|
wikideck_image_pull_secrets: []
|
||||||
|
|
||||||
|
wikideck_offline: false
|
||||||
|
# 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: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 512Mi
|
||||||
|
wikideck_node_selector: {}
|
||||||
|
wikideck_tolerations: []
|
||||||
|
wikideck_affinity: {}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# Live article discovery needs HTTPS. Standard NetworkPolicy cannot select FQDNs.
|
||||||
|
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,21 @@
|
|||||||
|
---
|
||||||
|
- name: Deploy Wikideck
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
roles:
|
||||||
|
- role: wikideck
|
||||||
|
vars:
|
||||||
|
# Replace with your published, scanned image; preferably use @sha256:...
|
||||||
|
wikideck_image: registry.example.com/games/wikideck:1.0.0
|
||||||
|
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 Wikideck to Kubernetes with persistent storage and optional TLS ingress.
|
||||||
|
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,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"
|
||||||
+60
-183
@@ -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:
|
kind: Ingress
|
||||||
name: wikideck-mine
|
namespace: "{{ wikideck_namespace }}"
|
||||||
namespace: wikideck
|
name: "{{ wikideck_name }}"
|
||||||
spec:
|
when: not wikideck_ingress_enabled | bool
|
||||||
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
|
|
||||||
metadata:
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: ca-issuer
|
|
||||||
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,77 @@
|
|||||||
|
# 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_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_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_ingress_enabled is boolean
|
||||||
|
- wikideck_network_policy_enabled is boolean
|
||||||
|
- wikideck_offline 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
|
||||||
|
|
||||||
|
- 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 }}
|
||||||
|
when: wikideck_network_policy_enabled | bool
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# 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:
|
||||||
|
# Never expose replicas as a variable: chain.json has no multi-writer locking.
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
revisionHistoryLimit: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: wikideck
|
||||||
|
app.kubernetes.io/instance: {{ wikideck_name | to_json }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
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_OFFLINE
|
||||||
|
value: {{ ('1' if wikideck_offline else '0') | to_json }}
|
||||||
|
{% 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: 60
|
||||||
|
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,44 @@
|
|||||||
|
# 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 %}
|
||||||
|
{% if wikideck_network_policy_dns_to | length == 0 and (wikideck_offline or wikideck_network_policy_https_to | 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 not wikideck_offline and wikideck_network_policy_https_to | length > 0 %}
|
||||||
|
- to: {{ wikideck_network_policy_https_to | to_json }}
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 443
|
||||||
|
{% 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,33 @@
|
|||||||
|
---
|
||||||
|
- 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_offline: "{{ invalid_case.offline | default(false) }}"
|
||||||
|
wikideck_image_pull_secrets: "{{ invalid_case.pull_secrets | default([]) }}"
|
||||||
|
wikideck_network_policy_ingress_from: "{{ invalid_case.ingress_from | default([{'podSelector': {}}]) }}"
|
||||||
|
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,94 @@
|
|||||||
|
---
|
||||||
|
# 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_offline: "{{ render_case.offline | default(false) }}"
|
||||||
|
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'}}]) }}"
|
||||||
|
loop:
|
||||||
|
- label: defaults
|
||||||
|
- label: TLS ingress and existing storage
|
||||||
|
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: offline and no storage class
|
||||||
|
offline: true
|
||||||
|
storage_class: ""
|
||||||
|
- label: default-deny all traffic
|
||||||
|
ingress_from: []
|
||||||
|
dns_to: []
|
||||||
|
https_to: []
|
||||||
|
- label: DNS-only offline mode
|
||||||
|
offline: true
|
||||||
|
- label: digest-pinned image
|
||||||
|
image: registry.example.com/wikideck@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
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
|
||||||
|
offline: "false"
|
||||||
|
- label: wrong list type
|
||||||
|
pull_secrets: not-a-list
|
||||||
|
- label: empty peer allows all traffic
|
||||||
|
ingress_from: [{}]
|
||||||
|
loop_control:
|
||||||
|
loop_var: invalid_case
|
||||||
|
label: "{{ invalid_case.label }}"
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
- 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.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.resources == wikideck_resources
|
||||||
|
- container.ports[0].containerPort == 3001
|
||||||
|
- container.startupProbe.httpGet.path == '/healthz'
|
||||||
|
- 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_OFFLINE == ('1' if wikideck_offline else '0')
|
||||||
|
- (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
|
||||||
|
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 not wikideck_offline and wikideck_network_policy_https_to | length > 0 else 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
|
||||||
+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