This commit is contained in:
2026-10-18 14:50:07 -04:00
parent cb4aad1006
commit ece4ef223c
16 changed files with 658 additions and 94 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"[yaml]": {
"editor.insertSpaces": true,
"editor.tabSize": 2,
"editor.detectIndentation": false
}
}
+174 -38
View File
@@ -1,38 +1,174 @@
Role Name
=========
A brief description of the role goes here.
Requirements
------------
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.
Role Variables
--------------
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.
Dependencies
------------
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.
Example Playbook
----------------
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
- hosts: servers
roles:
- { role: username.rolename, x: 42 }
License
-------
BSD
Author Information
------------------
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
# Ansible role: The Button
Deploy The Button to Kubernetes with persistent game data and a TLS-enabled
Ingress. The Ingress uses cert-manager's **ClusterIssuer `ca-issuer`** by default.
## Resources
- Namespace (optional creation).
- PersistentVolumeClaim for `/data/data.json` and `/data/mailing_list.csv`, or an
existing claim in the application namespace.
- One-replica Deployment, with `Recreate` updates to avoid concurrent file writers.
- ClusterIP Service on port 80, forwarding to the application on port 8000.
- `networking.k8s.io/v1` Ingress routing `/` to the Service with TLS termination.
cert-manager's ingress-shim creates and renews the certificate in the TLS Secret.
## Requirements
- Ansible Core 2.16–2.19 on a Linux/macOS control host, or WSL on Windows. Tasks
run on the play's execution host; use `localhost` with `connection: local`.
- The `kubernetes.core` collection and its Python dependencies; see
[requirements.yml](requirements.yml) and [requirements.txt](requirements.txt).
- A reachable Kubernetes API and kubeconfig credentials authorized to manage
namespaces (unless namespace creation is disabled), PVCs, Deployments,
Services, and Ingresses. TLS certificate validation for the API stays enabled.
- A working Ingress controller, cert-manager with ingress-shim enabled, and an
existing, Ready **ClusterIssuer named `ca-issuer`** (or the issuer selected in
your annotations). The role does not install these components or create the CA.
- DNS for the chosen hostname pointing at the Ingress controller, with port 443
reachable. Select an IngressClass explicitly if there is no cluster default.
- A default StorageClass, a named StorageClass, or a pre-provisioned PVC. The
storage driver must support writable volumes for UID/GID 10001 using `fsGroup`.
- A published container image **built from The Button application's Dockerfile**.
The role deploys an image; it does not build or push one. A plain Python base
image does not contain the application. Prefer an immutable tag or digest.
Install dependencies on the execution host:
```sh
python -m pip install -r requirements.txt
ansible-galaxy collection install -r requirements.yml
```
## Example playbook
Place this checkout in your Ansible roles search path as `ansible-role-the-button`:
```yaml
---
- name: Deploy The Button
hosts: localhost
connection: local
gather_facts: false
roles:
- role: ansible-role-the-button
vars:
the_button_image: registry.example.com/the-button:1.0.0
the_button_ingress_host: button.example.com
# Omit this when a default IngressClass is configured in the cluster.
the_button_ingress_class_name: nginx
# Controller-specific: use the equivalent configuration for your controller.
the_button_ingress_annotations:
cert-manager.io/cluster-issuer: ca-issuer
nginx.ingress.kubernetes.io/ssl-redirect: "true"
```
Replace the example registry and hostname with real values. With dependencies
installed, deploy directly from this checkout using the test playbook:
```sh
ansible-playbook -i tests/inventory tests/test.yml \
-e the_button_image=registry.example.com/the-button:1.0.0 \
-e the_button_ingress_host=button.example.com
```
## Variables
All defaults are in [defaults/main.yml](defaults/main.yml).
| Variable | Default | Purpose |
| --- | --- | --- |
| `the_button_image` | `""` (required) | Complete published application image reference. |
| `the_button_ingress_host` | `""` (required) | DNS hostname, without scheme, port, path, or wildcard. |
| `the_button_name` | `the-button` | Resource name and instance label; at most 50 characters. |
| `the_button_namespace` | `the-button` | Namespace for all application resources. |
| `the_button_create_namespace` | `true` | Create the namespace; disable for an existing namespace. |
| `the_button_kubeconfig` | `null` | Optional kubeconfig path; otherwise use normal discovery, including `KUBECONFIG`. |
| `the_button_kube_context` | `null` | Optional kubeconfig context. |
| `the_button_wait` | `true` | Wait for the Deployment to become ready. |
| `the_button_wait_timeout` | `180` | Readiness timeout in seconds. |
| `the_button_image_pull_policy` | `IfNotPresent` | Container image pull policy. |
| `the_button_image_pull_secrets` | `[]` | Names of existing image-pull Secrets in the namespace. |
| `the_button_duration_seconds` | `86400` | Countdown duration; must be a positive integer. |
| `the_button_ingress_class_name` | `""` | IngressClass; empty omits the field and uses the cluster default. |
| `the_button_ingress_annotations` | `{"cert-manager.io/cluster-issuer": "ca-issuer"}` | Complete Ingress annotations map, including issuer and controller-specific settings. |
| `the_button_tls_secret_name` | `<name>-tls` | Secret managed by cert-manager in the application namespace. |
| `the_button_existing_claim` | `""` | Existing PVC to mount; empty creates `<name>-data`. |
| `the_button_storage_size` | `1Gi` | Size of the role-managed PVC. |
| `the_button_storage_class` | `null` | Omit for cluster default; `""` disables dynamic provisioning; a string selects a class. |
| `the_button_storage_access_modes` | `[ReadWriteOnce]` | Access modes for the role-managed PVC. |
| `the_button_resources` | See defaults | Requests: 100m CPU / 64Mi RAM; limits: 1 CPU / 256Mi RAM. Tune for actual load. |
| `the_button_node_selector` | `{}` | Pod node selector. |
| `the_button_tolerations` | `[]` | Pod tolerations. |
| `the_button_affinity` | `{}` | Pod affinity configuration. |
Configure the issuer directly in `the_button_ingress_annotations`, alongside any
controller-specific annotations. Overriding this map replaces the defaults: include
`cert-manager.io/cluster-issuer: ca-issuer` to retain the default issuer, choose
another ClusterIssuer, or use `cert-manager.io/issuer` for a namespace-scoped
Issuer. Do not specify both issuer annotations. An empty map renders no annotations;
in that case, arrange certificate provisioning separately for the TLS Secret.
## TLS and security
- TLS terminates at the Ingress; traffic from the controller to the Pod is HTTP.
Configure HTTP-to-HTTPS redirects, HSTS, modern TLS policy, rate limiting, and
request-size limits on your Ingress controller as appropriate. Redirect
annotations differ between controllers and are not imposed by this role.
- Certificates from a private CA are **not automatically trusted by browsers**.
Distribute the CA trust chain to clients; do not bypass certificate validation.
cert-manager owns private-key generation, certificate issuance, and renewal;
no private keys or credentials are hardcoded in the role.
- Readiness waiting covers the Deployment, **not** certificate issuance or DNS
propagation. Confirm the generated Certificate is Ready and the HTTPS endpoint
presents a valid certificate for the configured hostname before exposing it.
- The container runs as non-root UID/GID 10001, drops all capabilities, disallows
privilege escalation, uses `RuntimeDefault` seccomp, and has a read-only root
filesystem. Only `/data` and a bounded `/tmp` volume are writable. Kubernetes
service-account credentials are not mounted. Health probes use `/api/state`.
- The Service is internal only. Apply cluster-specific NetworkPolicies to allow
your Ingress controller to reach TCP/8000 and restrict other traffic. The role
does not assume controller namespace labels or manage cluster network policy.
- Use an encrypted StorageClass and encrypted, access-controlled backups: the
volume contains password hashes, session hashes, and mailing-list personal data.
Restrict access to the namespace and TLS/registry Secrets through cluster RBAC.
## Persistence and updates
**Do not scale this Deployment beyond one replica or attach its PVC to another
running application instance.** The application caches state in memory and writes
local files; a shared filesystem does not make multiple replicas safe.
Re-running the role applies resource changes idempotently. Updates intentionally
incur brief downtime because the previous Pod must stop before its replacement
starts. There is no autoscaler. Back up `/data` before upgrades and test restores.
Changing the countdown setting does not promise to reset an existing saved game.
PVC creation does not wait for binding before creating the Deployment, allowing
StorageClasses with `WaitForFirstConsumer`. Existing claims are not modified.
Do not change immutable PVC fields on an existing claim. Expansion requires a
StorageClass that permits it. Changing the resource name or namespace creates new
resources; it does not migrate data or clean up the old installation.
The role has no uninstall operation and does not delete claims. Manually deleting
the namespace or PVC can destroy game data according to the PV reclaim policy.
## Tests
Cluster-free render checks exercise default and custom settings, TLS wiring,
labels and ports, storage modes, existing-claim selection, and container hardening:
```sh
ansible-playbook -i tests/inventory tests/render.yml
ansible-playbook -i tests/inventory tests/test.yml --syntax-check
```
[tests/test.yml](tests/test.yml) is an integration deployment playbook: supply the
required variables and a test-cluster kubeconfig as shown above. Run it twice to
check idempotency, then verify HTTPS, certificate readiness, game operations, and
data persistence across a Pod replacement. These integration checks require a
real cluster and are not performed by the cluster-free tests.
## License
MIT-0 (matching the role's SPDX headers).
+45 -3
View File
@@ -1,3 +1,45 @@
#SPDX-License-Identifier: MIT-0
---
# defaults file for ansible-role-the-button
# SPDX-License-Identifier: MIT-0
---
# Required: a published image built from The Button's Dockerfile and a DNS name.
the_button_image: ""
the_button_ingress_host: ""
the_button_name: the-button
the_button_namespace: the-button
the_button_create_namespace: true
# Leave unset to use the controller's normal kubeconfig discovery (including KUBECONFIG).
the_button_kubeconfig: null
the_button_kube_context: null
the_button_wait: true
the_button_wait_timeout: 180
the_button_image_pull_policy: IfNotPresent
# Names of existing registry Secrets in the application namespace.
the_button_image_pull_secrets: []
the_button_duration_seconds: 86400
# Keep this empty to use the cluster's default IngressClass.
the_button_ingress_class_name: ""
the_button_ingress_annotations:
cert-manager.io/cluster-issuer: ca-issuer
the_button_tls_secret_name: "{{ the_button_name }}-tls"
# The application uses local files, so persistence and a single writer are mandatory.
the_button_existing_claim: ""
the_button_storage_size: 1Gi
# null: cluster default; "": no StorageClass; otherwise: named StorageClass.
the_button_storage_class: null
the_button_storage_access_modes:
- ReadWriteOnce
the_button_resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: "1"
memory: 256Mi
the_button_node_selector: {}
the_button_tolerations: []
the_button_affinity: {}
+3 -3
View File
@@ -1,3 +1,3 @@
#SPDX-License-Identifier: MIT-0
---
# handlers file for ansible-role-the-button
#SPDX-License-Identifier: MIT-0
---
# handlers file for ansible-role-the-button
+14 -35
View File
@@ -1,35 +1,14 @@
#SPDX-License-Identifier: MIT-0
galaxy_info:
author: your name
description: your role description
company: your company (optional)
# If the issue tracker for your role is not on github, uncomment the
# next line and provide a value
# issue_tracker_url: http://example.com/issue/tracker
# Choose a valid license ID from https://spdx.org - some suggested licenses:
# - BSD-3-Clause (default)
# - 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:
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: []
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
# if you add dependencies to this list.
# SPDX-License-Identifier: MIT-0
---
galaxy_info:
role_name: the_button
author: Eric Meehan
description: Deploy The Button to Kubernetes with persistent storage and cert-manager TLS ingress.
license: MIT-0
min_ansible_version: "2.16"
galaxy_tags:
- kubernetes
- ingress
- certmanager
- games
dependencies: []
+5
View File
@@ -0,0 +1,5 @@
# Install on the Ansible execution host (Linux/macOS or WSL), not in the app image.
ansible-core>=2.16,<2.20
kubernetes>=24.2.0,<35.0.0
PyYAML>=6.0.1,<7.0.0
jsonpatch>=1.33,<2.0
+4
View File
@@ -0,0 +1,4 @@
---
collections:
- name: kubernetes.core
version: ">=3.0.0,<7.0.0"
+62 -3
View File
@@ -1,3 +1,62 @@
#SPDX-License-Identifier: MIT-0
---
# tasks file for ansible-role-the-button
# SPDX-License-Identifier: MIT-0
---
- name: Validate required deployment settings
ansible.builtin.assert:
that:
- the_button_image is string and the_button_image | length > 0
- the_button_ingress_host is string and the_button_ingress_host | length > 0
- the_button_ingress_host | length <= 253
- the_button_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])?$')
- the_button_name is match('^[a-z]([a-z0-9-]*[a-z0-9])?$')
- the_button_name | length <= 50
- the_button_namespace is match('^[a-z0-9]([a-z0-9-]*[a-z0-9])?$')
- the_button_namespace | length <= 63
- the_button_tls_secret_name | length > 0
- the_button_duration_seconds | string is match('^[1-9][0-9]*$')
- the_button_image_pull_policy in ['Always', 'IfNotPresent', 'Never']
- the_button_ingress_annotations is mapping
- the_button_resources is mapping
- the_button_storage_access_modes | length > 0
- the_button_wait_timeout | int > 0
fail_msg: >-
Set the_button_image to a published application image and the_button_ingress_host
to a DNS hostname (no scheme or path). Names must be valid Kubernetes names,
and duration and wait timeout must be positive.
- name: Create the application namespace
kubernetes.core.k8s:
kubeconfig: "{{ the_button_kubeconfig | default(omit, true) }}"
context: "{{ the_button_kube_context | default(omit, true) }}"
state: present
apply: true
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ the_button_namespace }}"
when: the_button_create_namespace | bool
- name: Create persistent storage for game data and mailing list
kubernetes.core.k8s:
kubeconfig: "{{ the_button_kubeconfig | default(omit, true) }}"
context: "{{ the_button_kube_context | default(omit, true) }}"
state: present
apply: true
definition: "{{ lookup('ansible.builtin.template', 'pvc.yml.j2') | from_yaml }}"
# Do not wait here: WaitForFirstConsumer volumes need the Deployment first.
wait: false
when: the_button_existing_claim | length == 0
- name: Deploy The Button service, application, and TLS ingress
kubernetes.core.k8s:
kubeconfig: "{{ the_button_kubeconfig | default(omit, true) }}"
context: "{{ the_button_kube_context | default(omit, true) }}"
state: present
apply: true
definition: "{{ lookup('ansible.builtin.template', item) | from_yaml }}"
wait: "{{ the_button_wait | bool }}"
wait_timeout: "{{ the_button_wait_timeout | int }}"
loop:
- service.yml.j2
- deployment.yml.j2
- ingress.yml.j2
+103
View File
@@ -0,0 +1,103 @@
# SPDX-License-Identifier: MIT-0
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ the_button_name | to_json }}
namespace: {{ the_button_namespace | to_json }}
labels:
app.kubernetes.io/name: the-button
app.kubernetes.io/instance: {{ the_button_name | to_json }}
app.kubernetes.io/managed-by: ansible
spec:
# State is cached in-process and persisted to files; never run concurrent replicas.
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: the-button
app.kubernetes.io/instance: {{ the_button_name | to_json }}
template:
metadata:
labels:
app.kubernetes.io/name: the-button
app.kubernetes.io/instance: {{ the_button_name | to_json }}
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
terminationGracePeriodSeconds: 30
{% if the_button_image_pull_secrets | length > 0 %}
imagePullSecrets:
{% for secret in the_button_image_pull_secrets %}
- name: {{ secret | to_json }}
{% endfor %}
{% endif %}
nodeSelector: {{ the_button_node_selector | to_json }}
tolerations: {{ the_button_tolerations | to_json }}
affinity: {{ the_button_affinity | to_json }}
containers:
- name: the-button
image: {{ the_button_image | to_json }}
imagePullPolicy: {{ the_button_image_pull_policy | to_json }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
ports:
- name: http
containerPort: 8000
protocol: TCP
env:
- name: BUTTON_HOST
value: "0.0.0.0"
- name: BUTTON_PORT
value: "8000"
- name: BUTTON_DURATION_SECONDS
value: {{ (the_button_duration_seconds | string) | to_json }}
- name: BUTTON_DATA_FILE
value: /data/data.json
- name: BUTTON_MAILING_LIST_FILE
value: /data/mailing_list.csv
- name: PYTHONDONTWRITEBYTECODE
value: "1"
resources: {{ the_button_resources | to_json }}
startupProbe:
httpGet:
path: /api/state
port: http
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
readinessProbe:
httpGet:
path: /api/state
port: http
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /api/state
port: http
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
volumeMounts:
- name: data
mountPath: /data
- name: tmp
mountPath: /tmp
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ (the_button_existing_claim or (the_button_name ~ '-data')) | to_json }}
- name: tmp
emptyDir:
sizeLimit: 64Mi
+37
View File
@@ -0,0 +1,37 @@
# SPDX-License-Identifier: MIT-0
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ the_button_name | to_json }}
namespace: {{ the_button_namespace | to_json }}
labels:
app.kubernetes.io/name: the-button
app.kubernetes.io/instance: {{ the_button_name | to_json }}
app.kubernetes.io/managed-by: ansible
{% if the_button_ingress_annotations %}
annotations:
{% for key, value in the_button_ingress_annotations.items() %}
{{ key | to_json }}: {{ (value | string) | to_json }}
{% endfor %}
{% else %}
annotations: {}
{% endif %}
spec:
{% if the_button_ingress_class_name | length > 0 %}
ingressClassName: {{ the_button_ingress_class_name | to_json }}
{% endif %}
tls:
- hosts:
- {{ the_button_ingress_host | to_json }}
secretName: {{ the_button_tls_secret_name | to_json }}
rules:
- host: {{ the_button_ingress_host | to_json }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ the_button_name | to_json }}
port:
name: http
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: MIT-0
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ (the_button_name ~ '-data') | to_json }}
namespace: {{ the_button_namespace | to_json }}
labels:
app.kubernetes.io/name: the-button
app.kubernetes.io/instance: {{ the_button_name | to_json }}
app.kubernetes.io/managed-by: ansible
spec:
accessModes: {{ the_button_storage_access_modes | to_json }}
{% if the_button_storage_class is not none %}
storageClassName: {{ the_button_storage_class | to_json }}
{% endif %}
resources:
requests:
storage: {{ the_button_storage_size | to_json }}
+20
View File
@@ -0,0 +1,20 @@
# SPDX-License-Identifier: MIT-0
apiVersion: v1
kind: Service
metadata:
name: {{ the_button_name | to_json }}
namespace: {{ the_button_namespace | to_json }}
labels:
app.kubernetes.io/name: the-button
app.kubernetes.io/instance: {{ the_button_name | to_json }}
app.kubernetes.io/managed-by: ansible
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: the-button
app.kubernetes.io/instance: {{ the_button_name | to_json }}
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
+3 -3
View File
@@ -1,3 +1,3 @@
#SPDX-License-Identifier: MIT-0
localhost
#SPDX-License-Identifier: MIT-0
localhost
+150
View File
@@ -0,0 +1,150 @@
# SPDX-License-Identifier: MIT-0
---
- name: Validate rendered Kubernetes resources without a cluster
hosts: localhost
connection: local
gather_facts: false
vars:
template_directory: "{{ playbook_dir }}/../templates"
tasks:
- name: Load the role defaults
ansible.builtin.include_vars:
file: "{{ playbook_dir }}/../defaults/main.yml"
- name: Set required test inputs
ansible.builtin.set_fact:
the_button_image: registry.example.com/the-button:1.0.0
the_button_ingress_host: button.example.com
- name: Render default manifests using Ansible's template engine
ansible.builtin.set_fact:
deployment: "{{ lookup('ansible.builtin.template', template_directory ~ '/deployment.yml.j2') | from_yaml }}"
service: "{{ lookup('ansible.builtin.template', template_directory ~ '/service.yml.j2') | from_yaml }}"
ingress: "{{ lookup('ansible.builtin.template', template_directory ~ '/ingress.yml.j2') | from_yaml }}"
pvc: "{{ lookup('ansible.builtin.template', template_directory ~ '/pvc.yml.j2') | from_yaml }}"
- name: Check TLS routing, persistent storage, and single-writer invariants
ansible.builtin.assert:
that:
- deployment.apiVersion == 'apps/v1'
- deployment.spec.replicas == 1
- deployment.spec.strategy.type == 'Recreate'
- deployment.spec.selector.matchLabels == service.spec.selector
- deployment.spec.template.metadata.labels == service.spec.selector
- deployment.spec.template.spec.volumes[0].persistentVolumeClaim.claimName == pvc.metadata.name
- deployment.spec.template.spec.containers[0].volumeMounts[0].mountPath == '/data'
- deployment.spec.template.spec.containers[0].image == the_button_image
- deployment.spec.template.spec.containers[0].ports[0].containerPort == 8000
- service.spec.type == 'ClusterIP'
- service.spec.ports[0].port == 80
- service.spec.ports[0].targetPort == 'http'
- ingress.apiVersion == 'networking.k8s.io/v1'
- ingress.metadata.annotations['cert-manager.io/cluster-issuer'] == 'ca-issuer'
- ingress.spec.tls[0].hosts == [the_button_ingress_host]
- ingress.spec.tls[0].secretName == 'the-button-tls'
- ingress.spec.rules[0].host == the_button_ingress_host
- ingress.spec.rules[0].http.paths[0].backend.service.name == service.metadata.name
- ingress.spec.rules[0].http.paths[0].backend.service.port.name == 'http'
- ingress.spec.rules[0].http.paths[0].pathType == 'Prefix'
- "'ingressClassName' not in ingress.spec"
- "'storageClassName' not in pvc.spec"
- pvc.spec.accessModes == ['ReadWriteOnce']
- pvc.spec.resources.requests.storage == '1Gi'
- name: Check container hardening and health probes
ansible.builtin.assert:
that:
- not deployment.spec.template.spec.automountServiceAccountToken
- deployment.spec.template.spec.securityContext.runAsNonRoot
- deployment.spec.template.spec.securityContext.runAsUser == 10001
- deployment.spec.template.spec.securityContext.fsGroup == 10001
- deployment.spec.template.spec.securityContext.seccompProfile.type == 'RuntimeDefault'
- not deployment.spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation
- deployment.spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem
- deployment.spec.template.spec.containers[0].securityContext.capabilities.drop == ['ALL']
- deployment.spec.template.spec.containers[0].startupProbe.httpGet.path == '/api/state'
- deployment.spec.template.spec.containers[0].readinessProbe.httpGet.port == 'http'
- deployment.spec.template.spec.containers[0].livenessProbe.httpGet.port == 'http'
- name: Configure custom deployment settings
ansible.builtin.set_fact:
the_button_name: custom-button
the_button_namespace: games
the_button_ingress_class_name: nginx
the_button_ingress_annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: custom-ca-issuer
the_button_existing_claim: restored-game-data
the_button_storage_class: fast
the_button_image_pull_secrets:
- private-registry
the_button_duration_seconds: 60
the_button_node_selector:
kubernetes.io/os: linux
the_button_tolerations:
- key: games
operator: Exists
effect: NoSchedule
- name: Render custom manifests
ansible.builtin.set_fact:
deployment: "{{ lookup('ansible.builtin.template', template_directory ~ '/deployment.yml.j2') | from_yaml }}"
ingress: "{{ lookup('ansible.builtin.template', template_directory ~ '/ingress.yml.j2') | from_yaml }}"
pvc: "{{ lookup('ansible.builtin.template', template_directory ~ '/pvc.yml.j2') | from_yaml }}"
- name: Check configurable settings and custom annotations
ansible.builtin.assert:
that:
- deployment.metadata.name == 'custom-button'
- deployment.metadata.namespace == 'games'
- deployment.spec.template.spec.volumes[0].persistentVolumeClaim.claimName == 'restored-game-data'
- deployment.spec.template.spec.imagePullSecrets == [{'name':'private-registry'}]
- deployment.spec.template.spec.nodeSelector['kubernetes.io/os'] == 'linux'
- deployment.spec.template.spec.tolerations[0].key == 'games'
- deployment.spec.template.spec.containers[0].env[2].value == '60'
- ingress.spec.ingressClassName == 'nginx'
- ingress.metadata.annotations['nginx.ingress.kubernetes.io/ssl-redirect'] == 'true'
- ingress.metadata.annotations['cert-manager.io/cluster-issuer'] == 'custom-ca-issuer'
- ingress.spec.tls[0].secretName == 'custom-button-tls'
- pvc.spec.storageClassName == 'fast'
- name: Configure a namespace-scoped issuer through annotations
ansible.builtin.set_fact:
the_button_ingress_annotations:
cert-manager.io/issuer: namespace-issuer
- name: Render ingress with a namespace-scoped issuer
ansible.builtin.set_fact:
ingress: "{{ lookup('ansible.builtin.template', template_directory ~ '/ingress.yml.j2') | from_yaml }}"
- name: Check issuer annotations are rendered without injected defaults
ansible.builtin.assert:
that:
- ingress.metadata.annotations == the_button_ingress_annotations
- "'cert-manager.io/cluster-issuer' not in ingress.metadata.annotations"
- name: Configure an empty annotations map
ansible.builtin.set_fact:
the_button_ingress_annotations: {}
- name: Render ingress without annotations
ansible.builtin.set_fact:
ingress: "{{ lookup('ansible.builtin.template', template_directory ~ '/ingress.yml.j2') | from_yaml }}"
- name: Check annotations can be cleared entirely
ansible.builtin.assert:
that:
- ingress.metadata.annotations == {}
- name: Disable dynamic provisioning
ansible.builtin.set_fact:
the_button_storage_class: ""
- name: Render a claim with dynamic provisioning disabled
ansible.builtin.set_fact:
static_pvc: "{{ lookup('ansible.builtin.template', template_directory ~ '/pvc.yml.j2') | from_yaml }}"
- name: Check explicit empty StorageClass is preserved
ansible.builtin.assert:
that:
- static_pvc.spec.storageClassName == ''
+10 -6
View File
@@ -1,6 +1,10 @@
#SPDX-License-Identifier: MIT-0
---
- hosts: localhost
remote_user: root
roles:
- ansible-role-the-button
# SPDX-License-Identifier: MIT-0
---
- name: Deploy The Button to a test cluster
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Run the role from this checkout
ansible.builtin.include_role:
name: "{{ playbook_dir }}/.."
+3 -3
View File
@@ -1,3 +1,3 @@
#SPDX-License-Identifier: MIT-0
---
# vars file for ansible-role-the-button
#SPDX-License-Identifier: MIT-0
---
# vars file for ansible-role-the-button