Initial commit
This commit is contained in:
Executable
+5
@@ -0,0 +1,5 @@
|
||||
.git
|
||||
*.retry
|
||||
__pycache__
|
||||
.venv
|
||||
*.vault.yml
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
* text=auto
|
||||
*.sh text eol=lf
|
||||
*.j2 text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
*.retry
|
||||
__pycache__/
|
||||
.venv/
|
||||
@@ -0,0 +1,27 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2026, Eric Meehan
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,167 @@
|
||||
# ansible-role-owncast
|
||||
|
||||
Reusable Ansible role for deploying [Owncast](https://owncast.online/) on an
|
||||
existing Kubernetes cluster. Extracted from ansible-role-eom; no EOM services,
|
||||
domains, certificate issuers, or Tor installation are assumed by role defaults.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Ansible Core 2.16–2.19 on a Linux/macOS controller, WSL, or a Linux container.
|
||||
- Install [requirements.txt](requirements.txt) with pip and the collection with
|
||||
`ansible-galaxy collection install -r requirements.yml`. Dependencies belong
|
||||
in the interpreter Ansible uses on localhost.
|
||||
- An authorized kubeconfig or the usual `K8S_AUTH_*` authentication environment
|
||||
settings supported by `kubernetes.core.k8s`.
|
||||
- A default StorageClass supporting ReadWriteOnce, an explicit StorageClass, or
|
||||
an existing PVC in the instance namespace. Use encrypted backing storage and
|
||||
arrange encrypted backups separately.
|
||||
- For optional ingress: DNS, an ingress controller, and a valid TLS secret or
|
||||
cert-manager with a configured ClusterIssuer. For LoadBalancer services: a
|
||||
working cluster load balancer implementation.
|
||||
- Optional onion access requires the Tor operator and its
|
||||
`tor.k8s.torproject.org/v1alpha2` OnionService CRD already installed.
|
||||
|
||||
Check out the repository as `ansible-role-owncast` under a directory in
|
||||
`ANSIBLE_ROLES_PATH`. For sibling role repositories, set that environment
|
||||
variable to their parent directory. The supplied playbooks run on the controller
|
||||
using its current Kubernetes context, with no SSH target required. Deployment
|
||||
commands below run from this repository on the controller.
|
||||
|
||||
## Your existing eom.dev instance
|
||||
|
||||
Use [playbooks/deploy-eom.yml](playbooks/deploy-eom.yml). It explicitly preserves:
|
||||
|
||||
- Namespace, Deployment, Service, PVC, and TLS secret named `owncast`.
|
||||
- Image `owncast/owncast:0.3.0`, one replica, 128Gi ReadWriteOnce storage at
|
||||
`/app/data`, CPU request `1.5`, HTTP 8080 and RTMP 1935.
|
||||
- LoadBalancer service with `externalTrafficPolicy: Local`.
|
||||
- NGINX ingress for `owncast.eom.dev`, with ClusterIssuer `ca-issuer`.
|
||||
- OnionService `http` in namespace `owncast`, port 80 → HTTP 8080, formerly
|
||||
part of the shared EOM onion task.
|
||||
|
||||
First run `ansible-playbook -i localhost, playbooks/deploy-eom.yml --check --diff`,
|
||||
review the changes, then run `ansible-playbook -i localhost, playbooks/deploy-eom.yml`.
|
||||
If Tor was never installed, add `-e owncast_onion_enabled=false` to both commands.
|
||||
|
||||
### Migration safety
|
||||
|
||||
1. Back up the data volume securely and record the live image, resource names,
|
||||
StorageClass, selector, and any provider annotations before deploying.
|
||||
2. Check the Kubernetes context. Keep the same namespace, PVC, and selector
|
||||
`app: owncast`. A different live immutable Deployment selector needs manual
|
||||
reconciliation; the role does not force-replace resources.
|
||||
3. Stop invoking the retired EOM Owncast task. Its deployment code,
|
||||
full-deployment entry, and Owncast-specific onion task have been removed.
|
||||
The old task path only fails with migration guidance; it does not delegate
|
||||
or deploy anything. No automatic dependency is added; use this playbook.
|
||||
4. Do not delete the existing PVC or TLS secret. Resources are adopted by name;
|
||||
administrator settings, stream keys, and the data directory are not reset.
|
||||
Leave `owncast_storage_class` blank to preserve the existing claim's class.
|
||||
5. Verify the rollout, HTTPS, administrator login, RTMP ingest, and optional
|
||||
onion URL after deployment.
|
||||
|
||||
The Deployment API is corrected from the old invalid `v1` to `apps/v1`.
|
||||
Updates use `Recreate` rather than overlapping replicas so the SQLite database
|
||||
and RWO volume have a single writer; an explicit null removes inherited
|
||||
`rollingUpdate` settings when patching the existing Deployment. Expect brief
|
||||
downtime during an update.
|
||||
Removing Ansible tasks does not delete any live cluster resources.
|
||||
|
||||
## Your friend's instance
|
||||
|
||||
Use [playbooks/deploy-friend.yml](playbooks/deploy-friend.yml). It uses separate
|
||||
namespace, resource names, PVC, and TLS secret `owncast-friend`, with the same
|
||||
image, storage sizing, resource request, and network layout as your instance.
|
||||
Tor is disabled. Two required inputs deliberately have no invented defaults:
|
||||
|
||||
- `friend_owncast_hostname`: your friend's real streaming hostname.
|
||||
- `friend_owncast_cluster_issuer`: a ClusterIssuer valid for that hostname.
|
||||
|
||||
Define these in inventory variables for localhost or an external YAML vars file.
|
||||
Run `ansible-playbook -i localhost, playbooks/deploy-friend.yml -e @/secure/friend-vars.yml`.
|
||||
Use `--check --diff` first. Add `--ask-vault-pass` for a Vault-encrypted vars file;
|
||||
do not commit secrets or pass passwords on the command line. Extra variables
|
||||
can also override the `owncast_*` settings when different sizing is needed.
|
||||
|
||||
The playbook rejects missing inputs, the eom.dev hostname, and the existing
|
||||
namespace before changing the cluster. For deployment on another cluster, use
|
||||
a separate kubeconfig/context. On a shared cluster, plan for another load
|
||||
balancer address and volume. Namespace separation is not a tenant security
|
||||
boundary by itself.
|
||||
|
||||
## Generic usage and variables
|
||||
|
||||
Invoke `ansible-role-owncast` from your own playbook or inventory; neither
|
||||
site-specific playbook is required. All defaults are in
|
||||
[defaults/main.yml](defaults/main.yml).
|
||||
|
||||
| Variable | Default / meaning |
|
||||
| --- | --- |
|
||||
| `owncast_namespace`, `owncast_name` | `owncast`; change for independent instances |
|
||||
| `owncast_image` | `owncast/owncast:0.3.0`; override with a tested tag or digest |
|
||||
| `owncast_image_pull_policy`, `owncast_image_pull_secrets` | `IfNotPresent`, `[]`; Secret references for private registries |
|
||||
| `owncast_resources` | Requests of 250m CPU and 256Mi memory; configure requests/limits for your transcoding workload |
|
||||
| `owncast_node_selector`, `owncast_tolerations` | `{}`, `[]` |
|
||||
| `owncast_pvc_name` | Resource name |
|
||||
| `owncast_existing_claim` | Empty; nonempty skips PVC creation and mounts that existing claim in this namespace |
|
||||
| `owncast_storage_size`, `owncast_storage_class` | `10Gi`, empty (default StorageClass) |
|
||||
| `owncast_service_type` | `ClusterIP`; also supports NodePort and LoadBalancer |
|
||||
| `owncast_service_annotations` | `{}`; provider-specific configuration |
|
||||
| `owncast_external_traffic_policy` | `Local`; emitted only for NodePort/LoadBalancer |
|
||||
| `owncast_load_balancer_source_ranges` | `[]`; optional provider-supported CIDR restrictions on both service ports |
|
||||
| `owncast_ingress_enabled`, `owncast_hostname` | `false`, empty; public HTTP exposure is opt-in |
|
||||
| `owncast_ingress_class`, `owncast_ingress_annotations` | Empty (controller default), `{}` |
|
||||
| `owncast_tls_enabled`, `owncast_tls_secret_name` | `true`, resource name; applies when ingress is enabled |
|
||||
| `owncast_cluster_issuer` | Empty; provide a name to use cert-manager, otherwise provision the TLS secret yourself |
|
||||
| `owncast_onion_enabled`, `owncast_onion_name` | `false`, `http` |
|
||||
|
||||
HTTP and RTMP container/service ports remain fixed at 8080 and 1935; configure
|
||||
external TCP routing separately if not using a LoadBalancer/NodePort service.
|
||||
One Owncast process per data volume is supported; horizontal scaling is not.
|
||||
Storage expansion depends on the StorageClass; never shrink an existing claim
|
||||
or change its immutable StorageClass. Changing namespaces/names creates another
|
||||
instance, not a data migration.
|
||||
|
||||
Disabling ingress or Tor stops managing it; it **does not delete a previously
|
||||
created resource**. Remove stale resources manually when revoking public access.
|
||||
The role uses only `state: present` and provides no destructive purge task.
|
||||
|
||||
## Security and operations
|
||||
|
||||
- No secrets are embedded, generated, or rotated. For a fresh instance, first
|
||||
deploy privately (`owncast_service_type=ClusterIP`, ingress disabled) and use
|
||||
Owncast's supported admin workflow to set unique administrator credentials
|
||||
and stream key **before exposing it publicly**. Existing credentials remain
|
||||
in the persistent database and are not altered by this role.
|
||||
- The site playbooks intentionally preserve existing LoadBalancer behavior:
|
||||
**unencrypted HTTP 8080 is exposed alongside RTMP 1935**, bypassing ingress
|
||||
TLS on that port. Restrict access with provider/firewall rules or source
|
||||
ranges, or use ClusterIP plus separately configured RTMP routing. RTMP itself
|
||||
is unencrypted; use trusted networking/tunnels when confidentiality matters.
|
||||
- TLS terminates at ingress. Use a trusted certificate/issuer; Kubernetes API
|
||||
certificate verification is not disabled. Storage encryption, backups, RBAC,
|
||||
NetworkPolicies, ingress admin restrictions, and renewal are operator duties.
|
||||
- Pods receive no Kubernetes API token, cannot escalate privileges, drop Linux
|
||||
capabilities, and use runtime-default seccomp. UID and data ownership are
|
||||
intentionally not changed during migration.
|
||||
- The old image version is retained for migration compatibility, not presented
|
||||
as the latest release. Review and test image upgrades separately after a
|
||||
backup; a digest-pinned image can be supplied via `owncast_image`.
|
||||
|
||||
## Tests (no cluster needed)
|
||||
|
||||
[tests/test.yml](tests/test.yml) renders templates with actual playbook vars,
|
||||
checks migration identity/settings and friend isolation, exercises optional
|
||||
fields, and checks invalid-input rejection. [tests/run.sh](tests/run.sh) also
|
||||
syntax-checks both playbooks and tests friend preflight failures before any
|
||||
cluster access.
|
||||
|
||||
Run `sh tests/run.sh` on a configured controller, or build
|
||||
[tests/Dockerfile](tests/Dockerfile) with
|
||||
`docker build -f tests/Dockerfile -t owncast-role-tests .` and run
|
||||
`docker run --rm --network none --cap-drop ALL --security-opt no-new-privileges owncast-role-tests`.
|
||||
The image runs tests as a non-root user and requires no kubeconfig or secrets.
|
||||
|
||||
## License
|
||||
|
||||
BSD-3-Clause; see [LICENSE](LICENSE).
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
---
|
||||
# Kubernetes authentication uses the controller's kubeconfig/K8S_AUTH_* settings.
|
||||
owncast_namespace: owncast
|
||||
owncast_name: owncast
|
||||
owncast_image: owncast/owncast:0.3.0
|
||||
owncast_image_pull_policy: IfNotPresent
|
||||
owncast_image_pull_secrets: []
|
||||
owncast_resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
owncast_node_selector: {}
|
||||
owncast_tolerations: []
|
||||
|
||||
owncast_pvc_name: "{{ owncast_name }}"
|
||||
owncast_existing_claim: ""
|
||||
owncast_storage_size: 10Gi
|
||||
# Empty uses the default StorageClass; do not change an existing claim's class.
|
||||
owncast_storage_class: ""
|
||||
|
||||
owncast_service_type: ClusterIP
|
||||
owncast_service_annotations: {}
|
||||
owncast_external_traffic_policy: Local
|
||||
owncast_load_balancer_source_ranges: []
|
||||
|
||||
# Public exposure is opt-in. Container ports are HTTP 8080 and RTMP 1935.
|
||||
owncast_ingress_enabled: false
|
||||
owncast_hostname: ""
|
||||
owncast_ingress_class: ""
|
||||
owncast_ingress_annotations: {}
|
||||
owncast_tls_enabled: true
|
||||
owncast_tls_secret_name: "{{ owncast_name }}"
|
||||
# Empty means an externally managed TLS secret, without cert-manager.
|
||||
owncast_cluster_issuer: ""
|
||||
|
||||
# Requires the Tor operator and OnionService CRD.
|
||||
owncast_onion_enabled: false
|
||||
owncast_onion_name: http
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
---
|
||||
galaxy_info:
|
||||
role_name: owncast
|
||||
author: Eric Meehan
|
||||
description: Deploy a persistent Owncast instance on Kubernetes
|
||||
license: BSD-3-Clause
|
||||
min_ansible_version: "2.16"
|
||||
dependencies: []
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
---
|
||||
# Runs on the controller using its existing kubeconfig; no SSH target required.
|
||||
# Preserve names to adopt existing resources without migrating persistent data.
|
||||
- name: Deploy eom.dev Owncast
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
vars:
|
||||
owncast_namespace: owncast
|
||||
owncast_name: owncast
|
||||
owncast_pvc_name: owncast
|
||||
owncast_image: owncast/owncast:0.3.0
|
||||
owncast_storage_size: 128Gi
|
||||
owncast_resources:
|
||||
requests:
|
||||
cpu: "1.5"
|
||||
owncast_service_type: LoadBalancer
|
||||
owncast_external_traffic_policy: Local
|
||||
owncast_ingress_enabled: true
|
||||
owncast_hostname: owncast.eom.dev
|
||||
owncast_ingress_class: nginx
|
||||
owncast_tls_enabled: true
|
||||
owncast_cluster_issuer: ca-issuer
|
||||
owncast_tls_secret_name: owncast
|
||||
# Formerly managed by the shared EOM onion task. Requires the Tor operator;
|
||||
# override to false if that optional integration was never installed.
|
||||
owncast_onion_enabled: true
|
||||
owncast_onion_name: http
|
||||
roles:
|
||||
- ansible-role-owncast
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
---
|
||||
# Supply friend_owncast_hostname and friend_owncast_cluster_issuer in inventory
|
||||
# or an external vars file. Do not guess a domain or reuse eom.dev's ingress.
|
||||
- name: Deploy an isolated Owncast instance for a friend
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
vars:
|
||||
owncast_namespace: owncast-friend
|
||||
owncast_name: owncast-friend
|
||||
owncast_pvc_name: owncast-friend
|
||||
owncast_image: owncast/owncast:0.3.0
|
||||
owncast_storage_size: 128Gi
|
||||
owncast_resources:
|
||||
requests:
|
||||
cpu: "1.5"
|
||||
owncast_service_type: LoadBalancer
|
||||
owncast_external_traffic_policy: Local
|
||||
owncast_ingress_enabled: true
|
||||
owncast_hostname: "{{ friend_owncast_hostname | default('') }}"
|
||||
owncast_ingress_class: nginx
|
||||
owncast_tls_enabled: true
|
||||
owncast_cluster_issuer: "{{ friend_owncast_cluster_issuer | default('') }}"
|
||||
owncast_tls_secret_name: owncast-friend
|
||||
owncast_onion_enabled: false
|
||||
pre_tasks:
|
||||
- name: Require friend-specific settings before changing the cluster
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- friend_owncast_hostname | default('') | length > 0
|
||||
- friend_owncast_hostname | default('') != 'owncast.eom.dev'
|
||||
- friend_owncast_cluster_issuer | default('') | length > 0
|
||||
- owncast_namespace != 'owncast'
|
||||
- owncast_hostname != 'owncast.eom.dev'
|
||||
fail_msg: Supply friend_owncast_hostname and friend_owncast_cluster_issuer for a separate instance.
|
||||
roles:
|
||||
- ansible-role-owncast
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
ansible-core>=2.16,<2.20
|
||||
kubernetes>=24.2.0,<35
|
||||
PyYAML>=6.0,<7
|
||||
jsonpatch>=1.32,<2
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
---
|
||||
collections:
|
||||
- name: kubernetes.core
|
||||
version: 6.0.0
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
---
|
||||
- name: Validate Owncast configuration
|
||||
ansible.builtin.import_tasks: validate.yml
|
||||
|
||||
- name: Create Owncast namespace
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition:
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ owncast_namespace }}"
|
||||
|
||||
- name: Create Owncast persistent volume claim
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition: "{{ lookup('ansible.builtin.template', 'pvc.yml.j2') | from_yaml }}"
|
||||
when: owncast_existing_claim | length == 0
|
||||
|
||||
- name: Deploy Owncast workload and service
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition: "{{ lookup('ansible.builtin.template', item) | from_yaml }}"
|
||||
loop:
|
||||
- deployment.yml.j2
|
||||
- service.yml.j2
|
||||
|
||||
- name: Deploy Owncast ingress
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition: "{{ lookup('ansible.builtin.template', 'ingress.yml.j2') | from_yaml }}"
|
||||
when: owncast_ingress_enabled | bool
|
||||
|
||||
- name: Deploy Owncast onion service
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition: "{{ lookup('ansible.builtin.template', 'onion.yml.j2') | from_yaml }}"
|
||||
when: owncast_onion_enabled | bool
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
---
|
||||
- name: Validate Owncast identity and service settings
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- owncast_namespace is match('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')
|
||||
- owncast_namespace | length <= 63
|
||||
- owncast_name is match('^[a-z]([-a-z0-9]*[a-z0-9])?$')
|
||||
- owncast_name | length <= 63
|
||||
- owncast_pvc_name is match('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$')
|
||||
- owncast_pvc_name | length <= 253
|
||||
- owncast_image | length > 0
|
||||
- owncast_service_type in ['ClusterIP', 'NodePort', 'LoadBalancer']
|
||||
- owncast_external_traffic_policy in ['Local', 'Cluster']
|
||||
- owncast_image_pull_policy in ['Always', 'IfNotPresent', 'Never']
|
||||
- owncast_resources is mapping
|
||||
- owncast_service_annotations is mapping
|
||||
- owncast_ingress_annotations is mapping
|
||||
- owncast_load_balancer_source_ranges is sequence
|
||||
- owncast_load_balancer_source_ranges is not string
|
||||
fail_msg: Invalid Owncast resource identity, image, resources, or service settings.
|
||||
|
||||
- name: Validate Owncast ingress settings
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- owncast_hostname | length > 0
|
||||
- owncast_hostname | length <= 253
|
||||
- owncast_hostname is match('^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$')
|
||||
- not (owncast_tls_enabled | bool) or owncast_tls_secret_name | length > 0
|
||||
- owncast_cluster_issuer | length == 0 or owncast_tls_enabled | bool
|
||||
fail_msg: Ingress requires a DNS hostname and, for TLS, a secret name. An issuer requires TLS.
|
||||
when: owncast_ingress_enabled | bool
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ owncast_name | to_json }}
|
||||
namespace: {{ owncast_namespace | to_json }}
|
||||
labels:
|
||||
app: {{ owncast_name | to_json }}
|
||||
spec:
|
||||
replicas: 1
|
||||
# The SQLite database and RWO volume must have only one writer.
|
||||
strategy:
|
||||
type: Recreate
|
||||
rollingUpdate: null
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ owncast_name | to_json }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ owncast_name | to_json }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
imagePullSecrets: {{ owncast_image_pull_secrets | to_json }}
|
||||
nodeSelector: {{ owncast_node_selector | to_json }}
|
||||
tolerations: {{ owncast_tolerations | to_json }}
|
||||
containers:
|
||||
- name: owncast
|
||||
image: {{ owncast_image | to_json }}
|
||||
imagePullPolicy: {{ owncast_image_pull_policy | to_json }}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
resources: {{ owncast_resources | to_json }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /app/data
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
- name: rtmp
|
||||
containerPort: 1935
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ (owncast_existing_claim if owncast_existing_claim | length > 0 else owncast_pvc_name) | to_json }}
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ owncast_name | to_json }}
|
||||
namespace: {{ owncast_namespace | to_json }}
|
||||
annotations: {{ (owncast_ingress_annotations | combine({'cert-manager.io/cluster-issuer': owncast_cluster_issuer} if owncast_cluster_issuer | length > 0 else {})) | to_json }}
|
||||
spec:
|
||||
{% if owncast_ingress_class | length > 0 %}
|
||||
ingressClassName: {{ owncast_ingress_class | to_json }}
|
||||
{% endif %}
|
||||
rules:
|
||||
- host: {{ owncast_hostname | to_json }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ owncast_name | to_json }}
|
||||
port:
|
||||
number: 8080
|
||||
{% if owncast_tls_enabled | bool %}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ owncast_hostname | to_json }}
|
||||
secretName: {{ owncast_tls_secret_name | to_json }}
|
||||
{% endif %}
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
apiVersion: tor.k8s.torproject.org/v1alpha2
|
||||
kind: OnionService
|
||||
metadata:
|
||||
name: {{ owncast_onion_name | to_json }}
|
||||
namespace: {{ owncast_namespace | to_json }}
|
||||
spec:
|
||||
version: 3
|
||||
rules:
|
||||
- port:
|
||||
number: 80
|
||||
backend:
|
||||
service:
|
||||
name: {{ owncast_name | to_json }}
|
||||
port:
|
||||
number: 8080
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ owncast_pvc_name | to_json }}
|
||||
namespace: {{ owncast_namespace | to_json }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
{% if owncast_storage_class | length > 0 %}
|
||||
storageClassName: {{ owncast_storage_class | to_json }}
|
||||
{% endif %}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ owncast_storage_size | to_json }}
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ owncast_name | to_json }}
|
||||
namespace: {{ owncast_namespace | to_json }}
|
||||
annotations: {{ owncast_service_annotations | to_json }}
|
||||
spec:
|
||||
selector:
|
||||
app: {{ owncast_name | to_json }}
|
||||
type: {{ owncast_service_type | to_json }}
|
||||
{% if owncast_service_type in ['LoadBalancer', 'NodePort'] %}
|
||||
externalTrafficPolicy: {{ owncast_external_traffic_policy | to_json }}
|
||||
{% endif %}
|
||||
{% if owncast_service_type == 'LoadBalancer' and owncast_load_balancer_source_ranges | length > 0 %}
|
||||
loadBalancerSourceRanges: {{ owncast_load_balancer_source_ranges | to_json }}
|
||||
{% endif %}
|
||||
ports:
|
||||
- name: rtmp
|
||||
port: 1935
|
||||
targetPort: rtmp
|
||||
protocol: TCP
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
FROM python:3.13-slim-trixie
|
||||
COPY requirements.txt requirements.yml /tmp/
|
||||
RUN pip install --no-cache-dir -r /tmp/requirements.txt \
|
||||
&& ansible-galaxy collection install -r /tmp/requirements.yml -p /usr/share/ansible/collections \
|
||||
&& useradd --create-home tester
|
||||
WORKDIR /roles/ansible-role-owncast
|
||||
COPY . ./
|
||||
ENV ANSIBLE_ROLES_PATH=/roles
|
||||
USER tester
|
||||
CMD ["sh", "tests/run.sh"]
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
ansible-playbook -i localhost, playbooks/deploy-eom.yml --syntax-check
|
||||
ansible-playbook -i localhost, playbooks/deploy-friend.yml --syntax-check
|
||||
ansible-playbook -i localhost, tests/test.yml
|
||||
|
||||
# These must fail at the preflight assertion, before trying to access a cluster.
|
||||
if ansible-playbook -i localhost, playbooks/deploy-friend.yml > /tmp/owncast-friend-missing.log 2>&1; then
|
||||
echo 'Friend deployment accepted missing settings' >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'Supply friend_owncast_hostname and friend_owncast_cluster_issuer' /tmp/owncast-friend-missing.log
|
||||
if ansible-playbook -i localhost, playbooks/deploy-friend.yml \
|
||||
-e friend_owncast_hostname=owncast.eom.dev \
|
||||
-e friend_owncast_cluster_issuer=fixture > /tmp/owncast-friend-collision.log 2>&1; then
|
||||
echo 'Friend deployment accepted the existing hostname' >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'Supply friend_owncast_hostname and friend_owncast_cluster_issuer' /tmp/owncast-friend-collision.log
|
||||
echo 'Owncast offline checks passed'
|
||||
Executable
+164
@@ -0,0 +1,164 @@
|
||||
---
|
||||
- name: Validate Owncast templates without a cluster
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Load role defaults and validate them without deploying
|
||||
ansible.builtin.include_role:
|
||||
name: ansible-role-owncast
|
||||
tasks_from: validate.yml
|
||||
public: true
|
||||
|
||||
- name: Render generic default resources
|
||||
ansible.builtin.set_fact:
|
||||
deployment: "{{ lookup('template', '../templates/deployment.yml.j2') | from_yaml }}"
|
||||
service: "{{ lookup('template', '../templates/service.yml.j2') | from_yaml }}"
|
||||
pvc: "{{ lookup('template', '../templates/pvc.yml.j2') | from_yaml }}"
|
||||
|
||||
- name: Check private defaults and single-writer safeguards
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- deployment.apiVersion == 'apps/v1'
|
||||
- deployment.spec.strategy.type == 'Recreate'
|
||||
- deployment.spec.replicas == 1
|
||||
- not deployment.spec.template.spec.automountServiceAccountToken
|
||||
- not deployment.spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation
|
||||
- service.spec.type == 'ClusterIP'
|
||||
- "'externalTrafficPolicy' not in service.spec"
|
||||
- "'storageClassName' not in pvc.spec"
|
||||
- pvc.spec.resources.requests.storage == '10Gi'
|
||||
- not owncast_ingress_enabled
|
||||
- not owncast_onion_enabled
|
||||
|
||||
- name: Load the actual eom playbook variables
|
||||
ansible.builtin.set_fact:
|
||||
"{{ item.key }}": "{{ item.value }}"
|
||||
loop: "{{ (lookup('file', '../playbooks/deploy-eom.yml') | from_yaml)[0].vars | dict2items }}"
|
||||
|
||||
- name: Validate eom settings
|
||||
ansible.builtin.include_role:
|
||||
name: ansible-role-owncast
|
||||
tasks_from: validate.yml
|
||||
|
||||
- name: Render eom resources
|
||||
ansible.builtin.set_fact:
|
||||
deployment: "{{ lookup('template', '../templates/deployment.yml.j2') | from_yaml }}"
|
||||
service: "{{ lookup('template', '../templates/service.yml.j2') | from_yaml }}"
|
||||
pvc: "{{ lookup('template', '../templates/pvc.yml.j2') | from_yaml }}"
|
||||
ingress: "{{ lookup('template', '../templates/ingress.yml.j2') | from_yaml }}"
|
||||
onion: "{{ lookup('template', '../templates/onion.yml.j2') | from_yaml }}"
|
||||
|
||||
- name: Check preservation of existing resource identities and settings
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- deployment.metadata.name == 'owncast'
|
||||
- deployment.metadata.namespace == 'owncast'
|
||||
- deployment.spec.selector.matchLabels == {'app':'owncast'}
|
||||
- deployment.spec.template.spec.volumes[0].persistentVolumeClaim.claimName == 'owncast'
|
||||
- deployment.spec.template.spec.containers[0].image == 'owncast/owncast:0.3.0'
|
||||
- deployment.spec.template.spec.containers[0].resources.requests.cpu == '1.5'
|
||||
- deployment.spec.template.spec.containers[0].volumeMounts[0].mountPath == '/app/data'
|
||||
- service.spec.type == 'LoadBalancer'
|
||||
- service.spec.externalTrafficPolicy == 'Local'
|
||||
- service.spec.ports | map(attribute='port') | list == [1935, 8080]
|
||||
- pvc.spec.resources.requests.storage == '128Gi'
|
||||
- ingress.spec.ingressClassName == 'nginx'
|
||||
- ingress.spec.rules[0].host == 'owncast.eom.dev'
|
||||
- ingress.spec.tls[0].secretName == 'owncast'
|
||||
- ingress.metadata.annotations['cert-manager.io/cluster-issuer'] == 'ca-issuer'
|
||||
- onion.metadata.name == 'http'
|
||||
- onion.metadata.namespace == 'owncast'
|
||||
- onion.spec.rules[0].backend.service.name == 'owncast'
|
||||
- onion.spec.rules[0].backend.service.port.number == 8080
|
||||
|
||||
- name: Set fake friend settings for validation
|
||||
ansible.builtin.set_fact:
|
||||
friend_owncast_hostname: stream.friend.example.org
|
||||
friend_owncast_cluster_issuer: friend-issuer
|
||||
|
||||
- name: Load the actual friend playbook variables
|
||||
ansible.builtin.set_fact:
|
||||
"{{ item.key }}": "{{ item.value }}"
|
||||
loop: "{{ (lookup('template', '../playbooks/deploy-friend.yml') | from_yaml)[0].vars | dict2items }}"
|
||||
|
||||
- name: Validate friend settings
|
||||
ansible.builtin.include_role:
|
||||
name: ansible-role-owncast
|
||||
tasks_from: validate.yml
|
||||
|
||||
- name: Render friend resources
|
||||
ansible.builtin.set_fact:
|
||||
deployment: "{{ lookup('template', '../templates/deployment.yml.j2') | from_yaml }}"
|
||||
pvc: "{{ lookup('template', '../templates/pvc.yml.j2') | from_yaml }}"
|
||||
ingress: "{{ lookup('template', '../templates/ingress.yml.j2') | from_yaml }}"
|
||||
|
||||
- name: Check friend isolation
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- deployment.metadata.namespace == 'owncast-friend'
|
||||
- deployment.spec.selector.matchLabels == {'app':'owncast-friend'}
|
||||
- deployment.spec.template.spec.volumes[0].persistentVolumeClaim.claimName == 'owncast-friend'
|
||||
- pvc.metadata.name == 'owncast-friend'
|
||||
- ingress.spec.rules[0].host == friend_owncast_hostname
|
||||
- ingress.spec.tls[0].secretName == 'owncast-friend'
|
||||
- ingress.metadata.annotations['cert-manager.io/cluster-issuer'] == friend_owncast_cluster_issuer
|
||||
- not owncast_onion_enabled
|
||||
|
||||
- name: Exercise existing storage and optional non-TLS ingress
|
||||
ansible.builtin.set_fact:
|
||||
owncast_existing_claim: external-data
|
||||
owncast_storage_class: encrypted-storage
|
||||
owncast_service_type: NodePort
|
||||
owncast_tls_enabled: false
|
||||
owncast_cluster_issuer: ""
|
||||
owncast_ingress_class: ""
|
||||
owncast_ingress_annotations:
|
||||
example.org/test: "true"
|
||||
|
||||
- name: Render optional configuration
|
||||
ansible.builtin.set_fact:
|
||||
deployment: "{{ lookup('template', '../templates/deployment.yml.j2') | from_yaml }}"
|
||||
pvc: "{{ lookup('template', '../templates/pvc.yml.j2') | from_yaml }}"
|
||||
service: "{{ lookup('template', '../templates/service.yml.j2') | from_yaml }}"
|
||||
ingress: "{{ lookup('template', '../templates/ingress.yml.j2') | from_yaml }}"
|
||||
|
||||
- name: Check optional configuration
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- deployment.spec.template.spec.volumes[0].persistentVolumeClaim.claimName == 'external-data'
|
||||
- pvc.spec.storageClassName == 'encrypted-storage'
|
||||
- service.spec.type == 'NodePort'
|
||||
- service.spec.externalTrafficPolicy == 'Local'
|
||||
- "'tls' not in ingress.spec"
|
||||
- "'ingressClassName' not in ingress.spec"
|
||||
- ingress.metadata.annotations == {'example.org/test':'true'}
|
||||
|
||||
- name: Set restricted load balancer settings
|
||||
ansible.builtin.set_fact:
|
||||
owncast_service_type: LoadBalancer
|
||||
owncast_load_balancer_source_ranges: [192.0.2.0/24]
|
||||
|
||||
- name: Check load balancer restrictions
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (lookup('template', '../templates/service.yml.j2') | from_yaml).spec.loadBalancerSourceRanges == ['192.0.2.0/24']
|
||||
|
||||
- name: Set invalid DNS hostname
|
||||
ansible.builtin.set_fact:
|
||||
owncast_hostname: https://invalid.example.org/
|
||||
|
||||
- name: Check invalid-input rejection
|
||||
block:
|
||||
- name: Run validation with an invalid hostname
|
||||
ansible.builtin.include_role:
|
||||
name: ansible-role-owncast
|
||||
tasks_from: validate.yml
|
||||
- name: Fail if invalid configuration was accepted
|
||||
ansible.builtin.fail:
|
||||
msg: Invalid hostname was accepted
|
||||
rescue:
|
||||
- name: Confirm the expected validation failure
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_failed_task.name == 'Validate Owncast ingress settings'
|
||||
Reference in New Issue
Block a user