mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-09-09 11:47:14 +02:00
feat(frontend): rename inj roles to sys-front-*, add sys-svc-cdn, cache-busting lookup
Introduce sys-svc-cdn (cdn_paths/cdn_urls/cdn_dirs) and ensure CDN directories + latest symlink. Rename sys-srv-web-inj-* → sys-front-inj-*; update includes/templates; serve shared/per-app CSS & JS via CDN. Add lookup_plugins/local_mtime_qs.py for mtime-based cache busting; split CSS into default.css/bootstrap.css + optional per-app style.css. CSP: use style-src-elem; drop unsafe-inline for styles. Services: fix SYS_SERVICE_ALL_ENABLED bool and controlled flush. BREAKING CHANGE: role names changed; replace includes and references accordingly. Conversation: https://chatgpt.com/share/68b55494-9ec4-800f-b559-44707029141d
This commit is contained in:
29
roles/sys-front-inj-all/README.md
Normal file
29
roles/sys-front-inj-all/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Nginx Global Matomo & Theming Modifier Role 🚀
|
||||
|
||||
This role enhances your Nginx configuration by conditionally injecting global Matomo tracking and theming elements into your HTML responses. It uses Nginx sub-filters to seamlessly add tracking scripts and CSS links to your web pages.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Global Matomo Tracking**
|
||||
The role includes Matomo tracking configuration and injects the corresponding tracking script into your HTML.
|
||||
|
||||
- **Global Theming**
|
||||
The role injects a global CSS link for consistent theming across your site.
|
||||
|
||||
- **Smart Injection**
|
||||
Uses Nginx's `sub_filter` to insert the tracking and theming snippets right before the closing `</head>` tag of your HTML documents.
|
||||
|
||||
|
||||
This will automatically activate Matomo tracking and/or global theming based on your configuration.
|
||||
|
||||
---
|
||||
|
||||
## Author
|
||||
|
||||
Developed by [Kevin Veen-Birkenbach](https://www.veen.world) 😎
|
||||
|
||||
---
|
||||
|
||||
Happy automating! 🎉
|
0
roles/sys-front-inj-all/__init__.py
Normal file
0
roles/sys-front-inj-all/__init__.py
Normal file
0
roles/sys-front-inj-all/filter_plugins/__init__.py
Normal file
0
roles/sys-front-inj-all/filter_plugins/__init__.py
Normal file
34
roles/sys-front-inj-all/filter_plugins/inj_enabled.py
Normal file
34
roles/sys-front-inj-all/filter_plugins/inj_enabled.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# Usage in tasks:
|
||||
# - set_fact:
|
||||
# inj_enabled: "{{ applications | inj_enabled(application_id, ['javascript','logout','css','matomo','desktop']) }}"
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# allow imports from module_utils (same trick as your get_app_conf filter)
|
||||
base = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
||||
mu = os.path.join(base, 'module_utils')
|
||||
for p in (base, mu):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
from module_utils.config_utils import get_app_conf
|
||||
|
||||
def inj_enabled_filter(applications, application_id, features, prefix="features", default=False):
|
||||
"""
|
||||
Build a dict {feature: value} by reading the feature flags under the given prefix for the selected application.
|
||||
Uses get_app_conf with strict=False so missing keys just return the default.
|
||||
"""
|
||||
result = {}
|
||||
for f in features:
|
||||
path = f"{prefix}.{f}" if prefix else f
|
||||
result[f] = get_app_conf(applications, application_id, path, strict=False, default=default)
|
||||
return result
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
"inj_enabled": inj_enabled_filter,
|
||||
}
|
55
roles/sys-front-inj-all/filter_plugins/inj_snippets.py
Normal file
55
roles/sys-front-inj-all/filter_plugins/inj_snippets.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Jinja filter: `inj_features(kind)` filters a list of features to only those
|
||||
that actually provide the corresponding snippet template file.
|
||||
|
||||
- kind='head' -> roles/sys-front-inj-<feature>/templates/head_sub.j2
|
||||
- kind='body' -> roles/sys-front-inj-<feature>/templates/body_sub.j2
|
||||
|
||||
If the feature's role directory (roles/sys-front-inj-<feature>) does not
|
||||
exist, this filter raises FileNotFoundError.
|
||||
|
||||
Usage in a template:
|
||||
{% set head_features = SRV_WEB_INJ_COMP_FEATURES_ALL | inj_features('head') %}
|
||||
{% set body_features = SRV_WEB_INJ_COMP_FEATURES_ALL | inj_features('body') %}
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# This file lives at: roles/sys-front-inj-all/filter_plugins/inj_snippets.py
|
||||
_THIS_DIR = os.path.dirname(__file__)
|
||||
_ROLE_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..")) # roles/sys-front-inj-all
|
||||
_ROLES_DIR = os.path.abspath(os.path.join(_ROLE_DIR, "..")) # roles
|
||||
|
||||
def _feature_role_dir(feature: str) -> str:
|
||||
return os.path.join(_ROLES_DIR, f"sys-front-inj-{feature}")
|
||||
|
||||
def _has_snippet(feature: str, kind: str) -> bool:
|
||||
if kind not in ("head", "body"):
|
||||
raise ValueError("kind must be 'head' or 'body'")
|
||||
|
||||
role_dir = _feature_role_dir(feature)
|
||||
if not os.path.isdir(role_dir):
|
||||
raise FileNotFoundError(
|
||||
f"[inj_snippets] Expected role directory not found for feature "
|
||||
f"'{feature}': {role_dir}"
|
||||
)
|
||||
|
||||
path = os.path.join(role_dir, "templates", f"{kind}_sub.j2")
|
||||
return os.path.exists(path)
|
||||
|
||||
def inj_features_filter(features, kind: str = "head"):
|
||||
if not isinstance(features, (list, tuple)):
|
||||
return []
|
||||
# Validation + filtering in one pass; will raise if a role dir is missing.
|
||||
valid = []
|
||||
for f in features:
|
||||
name = str(f)
|
||||
if _has_snippet(name, kind):
|
||||
valid.append(name)
|
||||
return valid
|
||||
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
"inj_features": inj_features_filter,
|
||||
}
|
22
roles/sys-front-inj-all/meta/main.yml
Normal file
22
roles/sys-front-inj-all/meta/main.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Core role for Nginx HTML injection of Matomo, theming, iFrame and JS snippets based on application feature flags."
|
||||
license: "Infinito.Nexus NonCommercial License"
|
||||
license_url: "https://s.infinito.nexus/license"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
galaxy_tags:
|
||||
- nginx
|
||||
- injector
|
||||
- matomo
|
||||
- theming
|
||||
repository: "https://s.infinito.nexus/code"
|
||||
issue_tracker_url: "https://s.infinito.nexus/issues"
|
||||
documentation: "https://s.infinito.nexus/code/tree/main/roles/sys-front-inj-all"
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Any
|
||||
versions:
|
||||
- all
|
49
roles/sys-front-inj-all/tasks/main.yml
Normal file
49
roles/sys-front-inj-all/tasks/main.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
- name: Build inj_enabled
|
||||
set_fact:
|
||||
inj_enabled: "{{ applications | inj_enabled(application_id, SRV_WEB_INJ_COMP_FEATURES_ALL) }}"
|
||||
|
||||
- name: "Load CDN Service for '{{ domain }}'"
|
||||
include_role:
|
||||
name: sys-svc-cdn
|
||||
public: true # Expose variables so that they can be used in all injection roles
|
||||
|
||||
- name: Reinitialize 'inj_enabled' for '{{ domain }}', after modification by CDN
|
||||
set_fact:
|
||||
inj_enabled: "{{ applications | inj_enabled(application_id, SRV_WEB_INJ_COMP_FEATURES_ALL) }}"
|
||||
inj_head_features: "{{ SRV_WEB_INJ_COMP_FEATURES_ALL | inj_features('head') }}"
|
||||
inj_body_features: "{{ SRV_WEB_INJ_COMP_FEATURES_ALL | inj_features('body') }}"
|
||||
|
||||
- name: "Activate Desktop iFrame notifier for '{{ domain }}'"
|
||||
include_role:
|
||||
name: sys-front-inj-desktop
|
||||
public: true # Vars used in templates
|
||||
when: inj_enabled.desktop
|
||||
|
||||
- name: "Activate Corporate CSS for '{{ domain }}'"
|
||||
include_role:
|
||||
name: sys-front-inj-css
|
||||
when: inj_enabled.css
|
||||
|
||||
- name: "Activate Matomo Tracking for '{{ domain }}'"
|
||||
include_role:
|
||||
name: sys-front-inj-matomo
|
||||
when: inj_enabled.matomo
|
||||
|
||||
- name: "Activate Javascript for '{{ domain }}'"
|
||||
include_role:
|
||||
name: sys-front-inj-javascript
|
||||
when: inj_enabled.javascript
|
||||
|
||||
- name: "Activate logout proxy for '{{ domain }}'"
|
||||
include_role:
|
||||
name: sys-front-inj-logout
|
||||
public: true # Vars used in templates
|
||||
when: inj_enabled.logout
|
||||
|
||||
- block:
|
||||
- name: Include dependency 'srv-core'
|
||||
include_role:
|
||||
name: srv-core
|
||||
when: run_once_srv_core is not defined
|
||||
- include_tasks: utils/run_once.yml
|
||||
when: run_once_sys_front_inj_all is not defined
|
91
roles/sys-front-inj-all/templates/location.lua.j2
Normal file
91
roles/sys-front-inj-all/templates/location.lua.j2
Normal file
@@ -0,0 +1,91 @@
|
||||
{# Jinja macro: expands feature snippets into Lua array pushes at render time #}
|
||||
{% macro push_snippets(list_name, features) -%}
|
||||
{% set kind = list_name | regex_replace('_snippets$','') %}
|
||||
{% for f in features if inj_enabled.get(f) -%}
|
||||
{{ list_name }}[#{{ list_name }} + 1] = [=[
|
||||
{%- include 'roles/sys-front-inj-' ~ f ~ '/templates/' ~ kind ~ '_sub.j2' -%}
|
||||
]=]
|
||||
{% endfor -%}
|
||||
{%- endmacro %}
|
||||
|
||||
lua_need_request_body on;
|
||||
|
||||
header_filter_by_lua_block {
|
||||
local ct = ngx.header.content_type or ""
|
||||
if ct:lower():find("^text/html") then
|
||||
ngx.ctx.is_html = true
|
||||
-- IMPORTANT: body will be modified → drop Content-Length to avoid mismatches
|
||||
ngx.header.content_length = nil
|
||||
else
|
||||
ngx.ctx.is_html = false
|
||||
end
|
||||
}
|
||||
|
||||
body_filter_by_lua_block {
|
||||
-- Only process HTML responses
|
||||
if not ngx.ctx.is_html then
|
||||
return
|
||||
end
|
||||
|
||||
-- Buffer all chunks until EOF
|
||||
ngx.ctx.buf = ngx.ctx.buf or {}
|
||||
local chunk, eof = ngx.arg[1], ngx.arg[2]
|
||||
|
||||
if chunk ~= "" then
|
||||
table.insert(ngx.ctx.buf, chunk)
|
||||
end
|
||||
|
||||
if not eof then
|
||||
-- Swallow intermediate chunks; emit once at EOF
|
||||
ngx.arg[1] = nil
|
||||
return
|
||||
end
|
||||
|
||||
-- Concatenate the full HTML
|
||||
local whole = table.concat(ngx.ctx.buf)
|
||||
ngx.ctx.buf = nil
|
||||
|
||||
-- Remove inline CSP <meta http-equiv="Content-Security-Policy"> (case-insensitive)
|
||||
local meta_re = [[<meta[^>]+http-equiv=["']Content-Security-Policy["'][^>]*>\s*]]
|
||||
whole = ngx.re.gsub(whole, meta_re, "", "ijo")
|
||||
|
||||
-- Build head snippets (rendered by Jinja at template time)
|
||||
local head_snippets = {}
|
||||
{{ push_snippets('head_snippets', inj_head_features) }}
|
||||
local head_payload = table.concat(head_snippets, "\n") .. "</head>"
|
||||
|
||||
-- Inject before </head> (first occurrence)
|
||||
local function repl_head(_) return head_payload end
|
||||
local new, n, err = ngx.re.sub(whole, [[</head\s*>]], repl_head, "ijo")
|
||||
if new then
|
||||
whole = new
|
||||
else
|
||||
ngx.log(ngx.WARN, "No </head> found; trying <body> fallback: ", err or "nil")
|
||||
-- Fallback: inject right AFTER the opening <body ...> tag
|
||||
local body_open_re = [[<body\b[^>]*>]]
|
||||
new, n, err = ngx.re.sub(whole, body_open_re, "$0\n" .. table.concat(head_snippets, "\n"), "ijo")
|
||||
if new then
|
||||
whole = new
|
||||
else
|
||||
ngx.log(ngx.ERR, "Head-fallback failed: ", err or "nil")
|
||||
end
|
||||
end
|
||||
|
||||
-- Build body snippets (rendered by Jinja at template time)
|
||||
local body_snippets = {}
|
||||
{{ push_snippets('body_snippets', inj_body_features) }}
|
||||
local body_payload = table.concat(body_snippets, "\n") .. "</body>"
|
||||
|
||||
-- Inject before </body> (first occurrence), or append if missing
|
||||
local function repl_body(_) return body_payload end
|
||||
new, n, err = ngx.re.sub(whole, [[</body\s*>]], repl_body, "ijo")
|
||||
if new then
|
||||
whole = new
|
||||
else
|
||||
ngx.log(ngx.WARN, "No </body> found; appending body snippets at end: ", err or "nil")
|
||||
whole = whole .. table.concat(body_snippets, "\n")
|
||||
end
|
||||
|
||||
-- Emit the modified HTML
|
||||
ngx.arg[1] = whole or ""
|
||||
}
|
3
roles/sys-front-inj-all/templates/server.conf.j2
Normal file
3
roles/sys-front-inj-all/templates/server.conf.j2
Normal file
@@ -0,0 +1,3 @@
|
||||
{% if inj_enabled.logout %}
|
||||
{% include 'roles/web-svc-logout/templates/logout-proxy.conf.j2' %}
|
||||
{% endif %}
|
9
roles/sys-front-inj-all/vars/main.yml
Normal file
9
roles/sys-front-inj-all/vars/main.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
# Docker
|
||||
docker_pull_git_repository: false # Deactivated here to don't inhire this
|
||||
|
||||
SRV_WEB_INJ_COMP_FEATURES_ALL:
|
||||
- 'javascript'
|
||||
- 'logout'
|
||||
- 'css'
|
||||
- 'matomo'
|
||||
- 'desktop'
|
Reference in New Issue
Block a user