feat(lasuite): migrate integration service to La Gaufre v2

Replace the inline gaufre.js/nginx.conf ConfigMap approach with a
purpose-built custom image (sunbeam/integration-service) that builds
the lagaufre.js v2 widget from the suitenumerique/integration source
and serves it via nginx.

Changes:
- Rewrite integration-deployment.yaml: custom image, v2 services.json
  format, only actually-deployed services (docs, meet, people)
- Add people-frontend nginx sub_filter overlay to rewrite the hardcoded
  production integration URL baked into the Next.js bundle at build time
- Register integration image in local overlay kustomization
This commit is contained in:
2026-03-03 16:08:48 +00:00
parent 8113e504ba
commit 897013bcb7
4 changed files with 108 additions and 272 deletions

View File

@@ -1,7 +1,14 @@
# La Gaufre integration service — O Estúdio app launcher. # La Gaufre integration service — O Estúdio app launcher (La Gaufre v2).
# Serves a custom gaufre.js widget and services.json for our app list. # Serves the lagaufre.js v2 widget, SVG logos, and the v2 services API.
# Apps set GAUFREJS_URL=https://integration.DOMAIN_SUFFIX/api/v1/gaufre.js # Apps load gaufre.js (via people-frontend sub_filter) which in turn initialises
# which loads this widget; clicking it fetches /api/v1/services.json. # the v2 widget with the button already rendered by @gouvfr-lasuite/ui-kit.
#
# Image: src.DOMAIN_SUFFIX/studio/integration:latest
# Built from sunbeam/integration-service/ (context: sunbeam/ root)
# Baked in: lagaufre.js v2, official La Suite logos, custom logos, gaufre.js, nginx.conf
#
# ConfigMap: only services.json (v2 format) — the one thing that varies per env
# DOMAIN_SUFFIX substituted at deploy time.
--- ---
apiVersion: v1 apiVersion: v1
@@ -11,259 +18,26 @@ metadata:
namespace: lasuite namespace: lasuite
data: data:
services.json: | services.json: |
[ {
{ "services": [
"id": "documentos", {
"name": "Documentos", "name": "Documentos",
"url": "https://docs.DOMAIN_SUFFIX" "url": "https://docs.DOMAIN_SUFFIX",
}, "logo": "https://integration.DOMAIN_SUFFIX/logos/docs.svg"
{ },
"id": "ficheiros", {
"name": "Ficheiros", "name": "Reuniões",
"url": "https://drive.DOMAIN_SUFFIX" "url": "https://meet.DOMAIN_SUFFIX",
}, "logo": "https://integration.DOMAIN_SUFFIX/logos/visio.svg"
{ },
"id": "reunioes", {
"name": "Reuniões", "name": "Humans",
"url": "https://meet.DOMAIN_SUFFIX" "url": "https://people.DOMAIN_SUFFIX",
}, "logo": "https://integration.DOMAIN_SUFFIX/logos/people.svg"
{
"id": "conversas",
"name": "Conversas",
"url": "https://chat.DOMAIN_SUFFIX"
},
{
"id": "correio",
"name": "Correio",
"url": "https://mail.DOMAIN_SUFFIX"
},
{
"id": "pessoas",
"name": "Pessoas",
"url": "https://people.DOMAIN_SUFFIX"
},
{
"id": "descobrir",
"name": "Descobrir",
"url": "https://find.DOMAIN_SUFFIX"
},
{
"id": "administrar",
"name": "Administrar",
"url": "https://admin.DOMAIN_SUFFIX"
}
]
gaufre.js: |
/**
* O Estúdio — La Gaufre widget (self-hosted).
* Drop-in replacement for the official integration.lasuite.numerique.gouv.fr widget.
* Fetches services.json from its own origin and renders a DSFR-style popup.
*/
(function () {
'use strict';
const origin = (function () {
const s = document.currentScript;
if (s && s.src) {
const u = new URL(s.src);
return u.origin;
}
return window.location.origin;
})();
const SERVICES_URL = origin + '/api/v1/services.json';
const SUITE_NAME = 'O Estúdio';
const STYLE = `
#estudio-gaufre-overlay {
display: none;
position: fixed;
inset: 0;
z-index: 9999;
}
#estudio-gaufre-overlay.open {
display: block;
}
#estudio-gaufre-backdrop {
position: absolute;
inset: 0;
}
#estudio-gaufre-panel {
position: absolute;
top: 56px;
left: 16px;
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 24px rgba(0,0,0,.15);
padding: 16px;
width: 320px;
z-index: 1;
}
#estudio-gaufre-panel h2 {
font-size: 14px;
font-weight: 700;
color: #666;
margin: 0 0 12px;
text-transform: uppercase;
letter-spacing: .05em;
}
#estudio-gaufre-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.estudio-gaufre-app {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 8px;
border-radius: 6px;
text-decoration: none;
color: #333;
font-size: 12px;
font-weight: 500;
transition: background .15s;
}
.estudio-gaufre-app:hover {
background: #f5f5f5;
}
.estudio-gaufre-icon {
width: 40px;
height: 40px;
border-radius: 8px;
background: #e8eaf6;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
`;
const ICONS = {
documentos: '📄',
ficheiros: '📁',
reunioes: '🎥',
conversas: '💬',
correio: '✉️',
pessoas: '👥',
descobrir: '🔍',
administrar: '⚙️',
};
function injectStyles() {
const el = document.createElement('style');
el.textContent = STYLE;
document.head.appendChild(el);
}
function buildPanel(services) {
const overlay = document.createElement('div');
overlay.id = 'estudio-gaufre-overlay';
const backdrop = document.createElement('div');
backdrop.id = 'estudio-gaufre-backdrop';
backdrop.addEventListener('click', close);
const panel = document.createElement('div');
panel.id = 'estudio-gaufre-panel';
const title = document.createElement('h2');
title.textContent = SUITE_NAME;
panel.appendChild(title);
const grid = document.createElement('div');
grid.id = 'estudio-gaufre-grid';
services.forEach(function (svc) {
const a = document.createElement('a');
a.className = 'estudio-gaufre-app';
a.href = svc.url;
const icon = document.createElement('div');
icon.className = 'estudio-gaufre-icon';
icon.textContent = ICONS[svc.id] || '🔲';
const label = document.createElement('span');
label.textContent = svc.name;
a.appendChild(icon);
a.appendChild(label);
grid.appendChild(a);
});
panel.appendChild(grid);
overlay.appendChild(backdrop);
overlay.appendChild(panel);
document.body.appendChild(overlay);
return overlay;
}
let overlay = null;
let services = null;
function open() {
if (!services) {
fetch(SERVICES_URL)
.then(function (r) { return r.json(); })
.then(function (data) {
services = data;
overlay = buildPanel(services);
overlay.classList.add('open');
});
} else {
if (!overlay) overlay = buildPanel(services);
overlay.classList.add('open');
}
}
function close() {
if (overlay) overlay.classList.remove('open');
}
function init() {
injectStyles();
document.querySelectorAll('.js-lasuite-gaufre-btn').forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.stopPropagation();
overlay && overlay.classList.contains('open') ? close() : open();
});
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') close();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
nginx.conf: |
server {
listen 80;
server_name _;
location = /api/v1/services.json {
alias /etc/integration/services.json;
add_header Content-Type "application/json; charset=utf-8";
add_header Access-Control-Allow-Origin "*";
}
location = /api/v1/gaufre.js {
alias /etc/integration/gaufre.js;
add_header Content-Type "application/javascript; charset=utf-8";
add_header Access-Control-Allow-Origin "*";
}
location / {
return 404;
} }
]
} }
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
@@ -282,16 +56,14 @@ spec:
spec: spec:
containers: containers:
- name: integration - name: integration
image: nginx:alpine image: integration
ports: ports:
- name: http - name: http
containerPort: 80 containerPort: 80
volumeMounts: volumeMounts:
- name: config - name: config
mountPath: /etc/integration mountPath: /etc/integration/services.json
- name: nginx-conf subPath: services.json
mountPath: /etc/nginx/conf.d/default.conf
subPath: nginx.conf
resources: resources:
limits: limits:
memory: 32Mi memory: 32Mi
@@ -302,17 +74,6 @@ spec:
- name: config - name: config
configMap: configMap:
name: integration-config name: integration-config
items:
- key: services.json
path: services.json
- key: gaufre.js
path: gaufre.js
- name: nginx-conf
configMap:
name: integration-config
items:
- key: nginx.conf
path: nginx.conf
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service

View File

@@ -12,6 +12,7 @@ kind: Kustomization
# replace DOMAIN_SUFFIX with <LIMA_IP>.sslip.io before kubectl apply. # replace DOMAIN_SUFFIX with <LIMA_IP>.sslip.io before kubectl apply.
resources: resources:
- people-frontend-nginx-configmap.yaml
- ../../base/ingress - ../../base/ingress
- ../../base/ory - ../../base/ory
- ../../base/data - ../../base/data
@@ -22,12 +23,17 @@ resources:
- ../../base/vso - ../../base/vso
images: images:
# Pulled from our Gitea registry. Built and pushed by: sunbeam.py --build # Pulled from our Gitea registry. Built and pushed by: sunbeam build <target>
# imagePullPolicy: Always in values-pingora.yaml ensures each rollout pulls fresh. # imagePullPolicy: Always in values-pingora.yaml ensures each rollout pulls fresh.
- name: sunbeam-proxy - name: sunbeam-proxy
newName: src.DOMAIN_SUFFIX/studio/sunbeam-proxy newName: src.DOMAIN_SUFFIX/studio/sunbeam-proxy
newTag: latest newTag: latest
# La Gaufre v2 integration service — lagaufre.js widget + SVG logos + nginx
- name: integration
newName: src.DOMAIN_SUFFIX/studio/integration
newTag: latest
# amd64-only La Suite images — mirrored to our Gitea registry with a patched # amd64-only La Suite images — mirrored to our Gitea registry with a patched
# OCI index that adds an arm64 alias so Rosetta can run them on the Lima VM. # OCI index that adds an arm64 alias so Rosetta can run them on the Lima VM.
# DOMAIN_SUFFIX is substituted by local-up.py at deploy time (sed replacement). # DOMAIN_SUFFIX is substituted by local-up.py at deploy time (sed replacement).
@@ -63,5 +69,8 @@ patches:
kind: Service kind: Service
name: livekit-server-turn name: livekit-server-turn
# Rewrite hardcoded production integration URL in people-frontend static build
- path: patch-people-frontend-nginx.yaml
# Apply §10.7 memory limits to all Deployments # Apply §10.7 memory limits to all Deployments
- path: values-resources.yaml - path: values-resources.yaml

View File

@@ -0,0 +1,20 @@
# Patch: mount the nginx ConfigMap into people-frontend to rewrite the
# hardcoded production integration URL at serve time.
apiVersion: apps/v1
kind: Deployment
metadata:
name: people-frontend
namespace: lasuite
spec:
template:
spec:
containers:
- name: desk
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
volumes:
- name: nginx-conf
configMap:
name: people-frontend-nginx-conf

View File

@@ -0,0 +1,46 @@
# nginx config for people-frontend that rewrites the hardcoded production
# integration URL baked into the desk static Next.js build.
#
# The people-frontend image has integration.lasuite.numerique.gouv.fr compiled
# in. sub_filter rewrites it to our local instance so the gaufre.js and
# services.json come from integration.DOMAIN_SUFFIX instead of the official
# government service.
#
# gzip must be off for sub_filter to operate on JS responses.
apiVersion: v1
kind: ConfigMap
metadata:
name: people-frontend-nginx-conf
namespace: lasuite
data:
default.conf: |
server {
listen 3000;
listen 8080;
server_name localhost;
server_tokens off;
root /usr/share/nginx/html;
gzip off;
sub_filter 'integration.lasuite.numerique.gouv.fr' 'integration.DOMAIN_SUFFIX';
sub_filter_once off;
sub_filter_types text/html application/javascript;
location / {
try_files $uri index.html $uri/ =404;
}
location /teams/ {
error_page 404 /teams/[id]/;
}
location /mail-domains/ {
error_page 404 /mail-domains/[slug]/;
}
error_page 404 /404.html;
location = /404.html {
internal;
}
}