mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
refactor(web-app-dashboard): bake the port-ui content catalogs into the image
A bind source only exists on the node that rendered it, so swarm rejected the service with "bind source path does not exist: .../volumes/i18n-content". The catalogs are generated from core.po at deploy time and no operator ever edits them, so they are image content, not host state. files/Dockerfile copies the build context's i18n-content/ into /app/i18n/content, the path port-ui derives from app/utils/i18n.py. The upstream image ships no catalog of its own, so nothing is shadowed. meta/services.yml declares custom: true, which makes the image lookup resolve dashboard_custom and leaves image:/version: as the Dockerfile's FROM. The compose template takes the shared build block from sys-svc-container rather than spelling one out, so it also gets the compose-only pull_policy: never. The catalogs move from directories.volumes to directories.instance because that is the build context. Only the stack host has one, so the render drops render_replicated_templates for stack_host_template, and the filter plugin that produced the replicated-template entries goes with it. A find/absent pair removes the catalog of a language core no longer translates. In the mounted design a stale file was host state an operator could delete; baked into an image it would survive every rebuild. The render also notifies container-recreate. The tag is constant across catalog changes, and docker stack deploy --resolve-image never computes no diff from a re-pushed identical tag, so in swarm only a forced service update moves tasks onto the new image. In compose the topic resolves to the same compose up --force-recreate handler that compose-up already triggers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
50091194d5
commit
0e4ed57c3c
9 files changed
+47
-43
No files matched your search
@@ -15,7 +15,7 @@ The consumer repositories change afterwards and are not part of this requirement
|
||||
- One built version needs about 2.3 GB. Building every translated language up front multiplies that per language; `min_storage` MUST say so. `min_storage` also gates which roles a host deploys at all, so it counts the sites every deployment builds and names the growth per further language.
|
||||
- The documentation is built from the git mirror of `DOCS_SOURCE_REPO`. Catalogs that exist only in a local working tree never reach it.
|
||||
- [logout-panel.js.j2](../../roles/web-app-keycloak/templates/logout-panel.js.j2) inlines [logout_i18n.yml](../../roles/web-app-keycloak/files/logout_i18n.yml) as a `{language: {key: text, dir}}` object, and [logout-panel.js](../../roles/web-app-keycloak/files/javascript/logout-panel.js) picks the language from `<html lang>` and `navigator.language`. Its 29 non-English blocks are the only human translations core owns.
|
||||
- port-ui translates the rendered `config.yaml` through `app/i18n/content/<code>.yaml`, a flat mapping from the English source string to its translation for the keys `description`, `info`, `name`, `subtitel`, `text`, `title` and `warning`. [web-app-dashboard](../../roles/web-app-dashboard/meta/volumes.yml) mounts only `config.yaml` today, so every card and menu text stays English.
|
||||
- port-ui translates the rendered `config.yaml` through `app/i18n/content/<code>.yaml`, a flat mapping from the English source string to its translation for the keys `description`, `info`, `name`, `subtitel`, `text`, `title` and `warning`. [web-app-dashboard](../../roles/web-app-dashboard/meta/volumes.yml) mounts only `config.yaml`, and the upstream image ships no catalog, so every card and menu text stays English until the role builds them in.
|
||||
- [csp_filters.py](../../plugins/filter/csp_filters.py) adds a provider origin to `connect-src` only for known feature flags such as `simpleicons`. [web-app-mig](../../roles/web-app-mig/templates/compose.yml.j2) renders no environment and no API origin.
|
||||
|
||||
## Acceptance Criteria
|
||||
@@ -29,7 +29,7 @@ The consumer repositories change afterwards and are not part of this requirement
|
||||
- [x] `logout_i18n.yml` holds only the English source strings, and the logout panel reads every other language from `core.po`.
|
||||
- [ ] For each of the 29 languages of the former `logout_i18n.yml`, the rendered panel catalogue holds the same strings as before the migration.
|
||||
- [x] The logout panel takes the text direction of every language from `meta/languages.yml`.
|
||||
- [x] web-app-dashboard mounts one port-ui content catalog per language that has a translated dashboard string, generated from `core.po` at deploy time.
|
||||
- [x] web-app-dashboard bakes one port-ui content catalog per language that has a translated dashboard string into its image, generated from `core.po` at deploy time.
|
||||
- [x] The dashboard at `/de/` shows the German translation of at least one card description and one menu category.
|
||||
- [x] A role that enables the `api` service gets the web-svc-api origin in its `connect-src`, and a role without it does not.
|
||||
- [x] web-app-mig enables the `api` service when web-svc-api is deployed and renders the API base URL into its environment.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ARG DASHBOARD_IMAGE
|
||||
ARG DASHBOARD_VERSION
|
||||
FROM ${DASHBOARD_IMAGE}:${DASHBOARD_VERSION}
|
||||
|
||||
COPY i18n-content/ /app/i18n/content/
|
||||
@@ -1,27 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def i18n_content_templates(
|
||||
catalogues: dict, directory: str, template: str
|
||||
) -> list[dict]:
|
||||
"""Return one ``render_replicated_templates`` entry per content catalog.
|
||||
|
||||
Args:
|
||||
catalogues: the ``lookup('i18n_content', ...)`` result, keyed by language.
|
||||
directory: host directory port-ui mounts as ``i18n/content``.
|
||||
template: template rendering the catalog named by ``language``.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"src": template,
|
||||
"dest": f"{directory.rstrip('/')}/{code}.yaml",
|
||||
"language": code,
|
||||
"mode": "0644",
|
||||
}
|
||||
for code in sorted(catalogues)
|
||||
]
|
||||
|
||||
|
||||
class FilterModule:
|
||||
def filters(self):
|
||||
return {"i18n_content_templates": i18n_content_templates}
|
||||
@@ -40,6 +40,7 @@ dashboard:
|
||||
name: dashboard
|
||||
image: ghcr.io/kevinveenbirkenbach/port-ui
|
||||
version: 2.2.2
|
||||
custom: true
|
||||
min_storage: 549MB
|
||||
cpus: "0.3"
|
||||
mem_reservation: 256m
|
||||
|
||||
@@ -6,9 +6,3 @@ config:
|
||||
mounts:
|
||||
- service: dashboard
|
||||
target: /app/config.yaml
|
||||
i18n_content:
|
||||
type: bind
|
||||
source: "{{ [lookup('container', 'web-app-dashboard', 'directories.volumes'), 'i18n-content'] | path_join }}"
|
||||
mounts:
|
||||
- service: dashboard
|
||||
target: /app/i18n/content
|
||||
@@ -18,17 +18,42 @@
|
||||
dashboard_i18n_content: "{{ lookup('i18n_content', 'role:', 'menu:') }}"
|
||||
run_once: true
|
||||
|
||||
- name: "📁 Ensure the port-ui content catalog directory exists on every node"
|
||||
- name: "📁 Ensure the port-ui content catalog directory exists in the build context"
|
||||
ansible.builtin.file:
|
||||
path: "{{ DASHBOARD_I18N_CONTENT_DIR }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: IS_STACK_HOST | bool
|
||||
|
||||
- name: "🌐 Render the port-ui content catalogs (local + swarm peers)"
|
||||
ansible.builtin.include_tasks:
|
||||
file: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/render_replicated_templates.yml') }}"
|
||||
vars:
|
||||
role_templates: "{{ dashboard_i18n_content | i18n_content_templates(DASHBOARD_I18N_CONTENT_DIR, 'i18n-content.yaml.j2') }}"
|
||||
- name: "🌐 Render the port-ui content catalogs into the build context"
|
||||
stack_host_template:
|
||||
src: "i18n-content.yaml.j2"
|
||||
dest: "{{ [ DASHBOARD_I18N_CONTENT_DIR, item ~ '.yaml' ] | path_join }}"
|
||||
loop: "{{ dashboard_i18n_content | list | sort }}"
|
||||
notify:
|
||||
- compose-build
|
||||
- compose-up
|
||||
- container-recreate
|
||||
|
||||
- name: "🔎 Find the port-ui content catalogs already in the build context"
|
||||
ansible.builtin.find:
|
||||
paths: "{{ DASHBOARD_I18N_CONTENT_DIR }}"
|
||||
patterns: "*.yaml"
|
||||
file_type: file
|
||||
register: dashboard_i18n_rendered
|
||||
|
||||
- name: "🗑️ Drop the catalog of a language core no longer translates"
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ dashboard_i18n_rendered.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path | basename }}"
|
||||
when: item.path | basename | regex_replace('[.]yaml$', '') not in dashboard_i18n_content
|
||||
notify:
|
||||
- compose-build
|
||||
- compose-up
|
||||
- container-recreate
|
||||
|
||||
- name: "👔 Dashboard manager-side operations (file deploys + container exec)"
|
||||
include_tasks: 02_manager_ops.yml
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
{% set service_name = 'dashboard' %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/sys-svc-container/templates/deploy.yml.j2' %}
|
||||
{{ lookup('template', 'roles/sys-svc-container/templates/build.yml.j2') | indent(4) }}
|
||||
args:
|
||||
DASHBOARD_IMAGE: "{{ DASHBOARD_IMAGE }}"
|
||||
DASHBOARD_VERSION: "{{ DASHBOARD_VERSION }}"
|
||||
{{ lookup('container_image', application_id, 'dashboard') }}
|
||||
{{ lookup('compose_only', 'container_name', DASHBOARD_CONTAINER) }}
|
||||
{{ lookup('container_ports', ['dashboard', 'http', DOCKER_BIND_HOST]) | indent(4) }}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{{ dashboard_i18n_content[_tpl.language] | to_nice_yaml(width=100000) }}
|
||||
{{ dashboard_i18n_content[item] | to_nice_yaml(width=100000) }}
|
||||
@@ -21,7 +21,9 @@ DASHBOARD_LOCATION_SILENT_CHECK: "/silent-check-sso.html"
|
||||
DASHBOARD_CONFIG_YML_SRC_FILE: "config.yaml.j2"
|
||||
DASHBOARD_CONFIG_INV_PATH: "{{ [ inventory_dir, 'files', inventory_hostname ,'docker', DASHBOARD_APPLICATION_ID, DASHBOARD_CONFIG_YML_SRC_FILE] | path_join }}"
|
||||
DASHBOARD_CONFIG_YML_DEST: "{{ [ lookup('container', DASHBOARD_APPLICATION_ID, 'directories.volumes'), 'config.yaml' ] | path_join }}"
|
||||
DASHBOARD_I18N_CONTENT_DIR: "{{ [ lookup('container', DASHBOARD_APPLICATION_ID, 'directories.volumes'), 'i18n-content' ] | path_join }}"
|
||||
DASHBOARD_I18N_CONTENT_DIR: "{{ [ lookup('container', DASHBOARD_APPLICATION_ID, 'directories.instance'), 'i18n-content' ] | path_join }}"
|
||||
DASHBOARD_IMAGE: "{{ lookup('config', DASHBOARD_APPLICATION_ID, 'services.dashboard.image') }}"
|
||||
DASHBOARD_VERSION: "{{ lookup('config', DASHBOARD_APPLICATION_ID, 'services.dashboard.version') }}"
|
||||
|
||||
DASHBOARD_APP_BASE_URL: "{{ lookup('tls', DASHBOARD_APPLICATION_ID, 'url.base') | regex_replace('/+$', '') }}/"
|
||||
DASHBOARD_KEYCLOAK_LOGIN_URL: >-
|
||||
|
||||
Reference in new issue
Block a user