Skip to content

[ADD] runbot_attach_vscode: data layer code-server (POC task #67684)#49

Closed
jjscarafia wants to merge 20 commits into
ingadhoc:18.0from
adhoc-dev:18.0-t-67684-jjs
Closed

[ADD] runbot_attach_vscode: data layer code-server (POC task #67684)#49
jjscarafia wants to merge 20 commits into
ingadhoc:18.0from
adhoc-dev:18.0-t-67684-jjs

Conversation

@jjscarafia
Copy link
Copy Markdown
Member

Summary

  • Nuevo módulo runbot_attach_vscode con un runbot.docker_layer reusable (xml_id runbot_attach_vscode.docker_layer_code_server) que instala code-server sobre cualquier Dockerfile del runbot.
  • Layer genérico sin auto-enchufe: el admin lo asigna a un Dockerfile target creando un layer de tipo reference_layer que apunta al xml_id.
  • POC inicial de tarea #67684. Spec base: ingadhoc/adhoc-wayspecs/10_draft/imagen-cli-vscode-server-attacheable.md.

Scope de este PR

  • Scaffold módulo + data XML.
  • No incluye lógica Python — el módulo no extiende runbot.build/runbot.build_config todavía.
  • No asigna el layer a ningún Dockerfile (decisión: manual, para no acoplar el módulo a la nomenclatura interna adhoc_odoo_19).

Follow-up (mismo PR, próximos commits)

  • Extender runbot.build con action_open_vscode (docker exec post-wake → arranca code-server en 0.0.0.0:8080 con --auth none para spike).
  • Helper de URL _get_vscode_url(build) retornando vscode-{dest}-{db_suffix}.{host}.
  • Botón "Open VS Code" en views/build_views.xml.
  • Validar issue de network_mode='none' en build_config (riesgo principal flagged por la spec).

Test plan

  • Install local (odoo -d 18.0-runbot-67684 -i runbot_attach_vscode --stop-after-init --without-demo=all --no-http) — 51 módulos cargados OK, record con xml_id creado.
  • Build de runbot productivo del PR levanta verde.
  • Admin asigna reference_layer al Dockerfile adhoc_odoo_19 y rebuild de la imagen pasa.
  • docker run --rm odoo:adhoc_odoo_19 code-server --version responde (validación de bake-in).

Adds a new module exposing a reusable runbot.docker_layer that installs
code-server (VS Code in the browser) on top of any runbot Dockerfile.

The layer is generic and not attached to any Dockerfile by default - the
admin assigns it via UI to the target Dockerfile by creating a
reference_layer pointing to runbot_attach_vscode.docker_layer_code_server.
This keeps the module reusable across runbots and matches the bake-in
opcional approach of the spec.

Follow-up: extend runbot.build with an Open VS Code action that starts
code-server on wake-up via docker exec and returns a routable URL.

Change note:
Nuevo modulo runbot_attach_vscode (POC tarea #67684): expone una
runbot.docker_layer reusable con xml_id que instala code-server sobre
cualquier Dockerfile del runbot. Por ahora solo la capa de bake-in,
asignable manualmente desde la UI; la extension del build con el boton
Open VS Code + helper de URL queda para los proximos commits del mismo
PR.
@roboadhoc
Copy link
Copy Markdown

Pull request status dashboard

jjscarafia and others added 19 commits May 15, 2026 15:30
…k #67684)

Iteration on the scaffold to validate UX before the productive runbot
build picks up the PR.

- Layer turned into a 'template' with CODE_SERVER_VERSION placeholder
  (default 4.96.4) and the install.sh --version flag. Pins
  code-server per Dockerfile and lets the consuming reference_layer
  override via its own values JSON.
- New ir.config_parameter records for runbot_attach_vscode.url_suffix
  (default 'vscode') and runbot_attach_vscode.scheme (default 'https').
- runbot.build extended with a computed vscode_url field
  (<scheme>://<dest>-<suffix>.<host>) and an action_open_vscode that
  returns ir.actions.act_url. Internal-user-only via _is_internal().
- Build form view: 'Open VS Code' button in a new header
  (groups='base.group_user', invisible when vscode_url is empty) and
  vscode_url shown as widget='url' next to build_url.
- Minimal README.rst documenting Phase 1 scope and Phase 2 backlog
  (proxy routing, port mapping, docker exec code-server, auth token).
- runbot_docker_layer.xml dropped 'noupdate=1' so subsequent module
  updates re-apply the layer content/values; ir_config_parameter.xml
  keeps 'noupdate=1' so the admin can override values at runtime.

Change note:
Iteracion sobre el scaffold antes que el runbot productivo agarre el
PR. Convierte la capa code-server a 'template' (parametrizable via
CODE_SERVER_VERSION), expone dos parametros de sistema para el patron
de URL, suma un campo computed vscode_url + boton 'Open VS Code' en el
form del build (restringido a usuarios internos) y un README minimo.
El boton solo construye la URL siguiendo el patron Nginx existente
(<dest>-<suffix>.<host>) — el routing real del proxy, el port mapping
y el arranque de code-server via docker exec quedan para la Fase 2.
- views/runbot_frontend_templates.xml: inherit runbot.build_button and
  add a "code" icon button next to the existing "Sign in" / "Wake up"
  buttons in the public build action bar. Visible to base.group_user
  when local_state == 'running' and vscode_url is computable. Target
  _blank for browser tab.
- tests/test_runbot_attach_vscode.py: 10 tests covering layer template
  metadata + rendered substitution, reference_layer composition with
  and without values override, vscode_url compute (default + ICP
  overrides + empty-when-missing), action_open_vscode happy path,
  guard when url missing, and guard for portal users.

No bump of __manifest__ version: runbot updates are manual and this is
not a migration. Bump only when worth it.

Change note:
Suma boton "code" en el action bar publico del build (frontend de
runbot, junto a Sign in / Wake up) — el cliente PO/dev lo ve ahi al
estar el build wakeado. Tests unitarios sobre el render del template,
el patron reference_layer (default + override per-Dockerfile) y la
proteccion del action_open_vscode contra portal users / dest faltante.
Dropped 4 of the 10 unit tests added in the prior commit and fused the
three vscode_url compute tests into one. Final coverage: 4 tests.

Dropped:
- test_source_layer_metadata: echoed the XML data record, no logic.
- test_source_layer_renders_default_version: exercised upstream's
  template engine, not our code.
- test_reference_layer_inherits_default_version: redundant with the
  override case, which already proves the chain works.
- test_action_open_vscode_returns_act_url: shape-check of the
  ir.actions.act_url dict literal, caught by the first manual click.

Kept and consolidated:
- test_reference_layer_overrides_version: only smoke test of the
  documented Dockerfile composition.
- test_vscode_url: defaults + ICP override + missing-dest short
  circuit, sequential — ICP leakage is irrelevant because the
  defaults assertion runs first.
- test_action_open_vscode_blocks_when_url_missing.
- test_action_open_vscode_blocks_portal_user.

Also translated remaining docstrings/comments to English and renamed
the fake host runbot.example.com -> ci.example.com to silence the
spell-checker.

Change note:
Limpia los tests del modulo: de 10 quedamos en 4. Borramos los que
asertaban contra el XML, los que ejercitaban el motor de templates de
runbot upstream y el happy path del action (solo chequea forma del
dict). Mantenemos el smoke del reference_layer con override, el compute
vscode_url (defaults + override de ICP + short-circuit sin dest, todo
en un solo metodo) y los dos guards del action_open_vscode (url
ausente, usuario portal).
Self-contained set of demo records that exercise the runbot tree
end-to-end so the form and frontend views can be inspected locally
without a real runbot in place. Lives in runbot_ux so it can be
reused by any future runbot-ux iteration; wired up via the
manifest's 'demo' key.

Records:
- 1 runbot.version (18.0).
- 1 repo + remote ('acme-addons', mode=disabled to keep the demo
  inert: no fetch, no clone).
- 1 trigger ('All') so builds render with a '[18.0] All' label in
  the frontend.
- 2 bundles: a base '18.0' (is_base, sticky=True via compute) and a
  feature-like branch (surfaces under the 'nosticky' filter).
- 4 commits with invented authors and SHAs.
- 4 build.params with unique commit_link_ids (distinct fingerprints).
- 4 builds covering done/ok, done/ko, running and pending states.
- 4 batches: 2 on the base bundle + 2 on the feature bundle so each
  build sits in its own batch (matches real runbot: every new commit
  on a PR opens a fresh batch).
- 4 batch.slot records wiring batches -> builds (frontend traversal,
  drives the per-trigger grouping in the bundle view).
- last_batch on both bundles patched via <function> after creation
  (chicken-and-egg: bundle exists before its batch). The feature
  bundle's last_batch is the newer one containing the pending build.

The pending build is intentionally left without host, so its
vscode_url stays False and the 'Open VS Code' button stays hidden
— matching the real runbot state machine and the guard already
covered by test_action_open_vscode_blocks_when_url_missing.

Change note:
Suma demo data en runbot_ux para inspeccionar las vistas (backend
form y frontend bundle/build) sin tener un runbot real montado.
La estructura es la minima que reproduce un arbol completo: 1
version, 1 repo + remote inerte (mode=disabled), 1 trigger 'All',
2 bundles (base + feature), 4 commits con autores inventados, 4
build.params, 4 builds con estados variados (done/ok, done/ko,
running, pending) repartidos en 4 batches (uno por build, como en
runbot real) y los batch.slot que conectan todo. Util tambien para
futuras iteraciones de runbot_ux.
…u (task #67684)

Frontend: the 'Open VS Code' entry now lives inside the build action
menu (the cog dropdown, runbot.build_menu) right after Rebuild,
instead of being an icon next to Sign in / Wake up in the action
bar. Visibility matches Rebuild's own condition
(global_state in ['done', 'running']) plus our vscode_url guard,
so a pending build (no host yet) does not surface the option.

The inherit is written as <record model='ir.ui.view'> with type=qweb
explicitly. The short-form <template inherit_id='...'> in this case
ate the parent's children at render time (Rebuild, Kill, etc.
vanished and only our injected element survived) even though the
xpath itself matched correctly when validated standalone with lxml.
Likely a quirk in how the short-form is converted internally when
the parent template has multiple root elements (button + div), but
not chased down further: the <record> form just works.

The xpath anchor is //a[i[hasclass('fa-refresh')]] — the fa-refresh
icon is exclusive to Rebuild within build_menu (Force Build uses
fa-level-up). The obvious anchor by @title was rejected: title is
on Odoo's TRANSLATED_ATTRS blocklist for view inheritance selectors.

Backend: drop the read-only <field name='vscode_url' widget='url'/>
from the build form. Visual noise next to the URL already exposed
by the 'Open VS Code' button.

Change note:
Mueve la entrada 'Open VS Code' del frontend al menu de acciones
(la ruedita) del build, justo despues de Rebuild. Visible con la
misma condicion que Rebuild (build done o running) mas el guard
de vscode_url (necesita host). En el form del backend saca el
campo vscode_url que estaba al lado del boton y no aportaba. La
herencia del template tuvo que escribirse como <record model=
'ir.ui.view'> explicito porque la sintaxis corta <template
inherit_id='...'> en este caso reemplazaba el contenido del padre
en lugar de extenderlo.
…task #67684)

The 'Open VS Code' entry already hides when the build is pending
(no host yet). It now also hides when the build's Dockerfile does
not include our code-server reference layer, since in that case
the container does not have code-server baked in and the URL
would resolve to nothing.

vscode_url:
- becomes stored. The compute fires once when dest and host are
  set (= build pickup) and the resulting value is frozen
  afterwards. The Dockerfile state is read at that moment and
  later mutations to the layer set do NOT flip the field. This
  matches the physical reality: once a build's container is
  baked, adding or removing the reference layer on the Dockerfile
  cannot change the running container's contents — so the URL
  availability should not change either.
- @api.depends stays on dest + host only; no chain into
  params_id.dockerfile_id.layer_ids, since that would defeat the
  freeze.
- env.ref's runbot_attach_vscode.docker_layer_code_server with
  raise_if_not_found=False (defensive — should always be there as
  long as the module is installed, but the check is cheap).
- iterates the build's dockerfile layers and matches any layer of
  type reference_layer pointing at the source layer.
- only sets the URL when dest, host and the layer are all present
  at compute time.

Test setup is extended so the URL tests have a real Dockerfile +
reference layer + params chain. A new test covers the negative
path (dest + host but no layer -> False) so the layer guard does
not silently rot.

Change note:
Ahora la opcion 'Open VS Code' tambien queda oculta si el
Dockerfile del build no incluye nuestra capa de code-server. La
condicion compuesta queda: dest + host + reference_layer hacia
runbot_attach_vscode.docker_layer_code_server presente en el
Dockerfile al momento del primer compute. El campo es stored y
solo depende de dest/host: una vez evaluado al asignarse el host
(= build pickup), mutaciones posteriores al layer set del
Dockerfile no flipean el campo. Tests actualizados con un
Dockerfile + capa de referencia + params reales; un test nuevo
cubre el caso 'sin capa -> vscode_url False'.
…ult (task #67684)

Pairs with the previous commit that hides 'Open VS Code' when the
build's Dockerfile lacks our code-server reference layer. Without
this update the demo would never surface the option: the demo
build.params use runbot.docker_default, which by itself carries no
reference to the source layer.

The demo file (renamed runbot_attach_vscode_demo.xml) attaches the
code-server reference layer to runbot.docker_default. noupdate=1
so the admin can delete the record without it re-appearing on
update. Because the file lives in /demo/ it only loads when demo
data is requested, so a production install (without demo) does
not opt every build into code-server automatically.

vscode_url is stored and its @api.depends is narrow (dest, host)
by design: admin-time mutations on the Dockerfile must not flip
the field on already-built builds. The flip side is that the
reference layer added by this demo would not propagate to builds
created before the demo file loaded (typical case: runbot_ux's
demo loads first and creates its builds against a layer-less
docker_default). The demo file therefore ends with a <function>
call to runbot.build._compute_vscode_url on every existing build,
which nudges the stored field once, right after the reference
layer is in place.

No cross-module dependency: runbot_attach_vscode still depends
only on runbot. runbot_ux's demo no longer creates a dedicated
Dockerfile; its build.params use the default Dockerfile so the
reference layer added here actually applies to them.

Change note:
La demo del modulo adjunta code-server al Dockerfile default de
runbot (runbot.docker_default) mediante una reference layer, de
modo que cualquier build del demo la herede automaticamente. Al
final del archivo de demo una llamada <function> fuerza el
recompute de vscode_url sobre los builds existentes, para que los
ya creados antes del demo (caso tipico: runbot_ux cargando antes)
tomen el layer recien agregado. Sin dependencia cruzada:
runbot_attach_vscode sigue dependiendo solo de runbot, y
runbot_ux saca el Dockerfile propio del demo para usar el default.
End-to-end runtime wiring so the 'Open VS Code' entry resolves to a
live code-server session inside the build container. Lazy start by
design: code-server is launched on first click via docker exec, not at
every wake-up, so the cost is paid only for builds where someone
actually attaches.

Pieces:

- models/runbot_build_config.py (new): inherit
  runbot.build.config.step._run_run_odoo. When the build's Dockerfile
  carries the code-server reference layer, append build.port + 2 to
  exposed_ports so Docker maps container port 8071 to that host port
  at wake-up time. The port mapping has to be declared at docker run
  time (Docker can't add port mappings to a running container later)
  but code-server itself is NOT started here.

- models/runbot_build.py: extend action_open_vscode. After the existing
  internal-user / vscode_url guards, ensure the build container is in
  RUNNING state, then docker exec (detached) into it with a shell
  guard 'pgrep -x code-server >/dev/null || exec code-server ...'.
  Idempotent: a second click does nothing if code-server is already
  up. After kicking off the exec, poll the host port for up to 5s so
  the browser does not race against the bind.

  Refactored the layer detection out of _compute_vscode_url into a
  public _has_vscode_layer() helper used by both the compute and the
  run-step inherit.

- views/runbot_nginx.xml (new): QWeb inherit of runbot.nginx_config.
  Injects a per-build server block at the server_build_anchor that
  routes <dest>-vscode.<host> to 127.0.0.1:<build.port + 2>, with the
  WebSocket headers code-server needs. Placed before the generic
  <dest>(-suffix)?.<host> block so the more specific vscode regex
  matches first.

README updated: Phase 1 + Phase 2 marked as shipped, Phase 3 (real
auth token, writable scratch dir) noted as pending.

Change note:
Suma el cableado runtime para que 'Open VS Code' efectivamente abra
un code-server vivo adentro del container del build. Lazy start: el
code-server arranca recien cuando el usuario clickea el boton (via
docker exec idempotente), no en cada wake-up. Asi los run steps
automaticos de las configs CI no pagan los ~150 MB extra cuando
nadie va a attachear. El mapeo de puerto (container 8071 -> host
build.port + 2) si se reserva en el docker run porque Docker no
permite agregar mapeos a un container ya corriendo. Nginx routea
<dest>-vscode.<host> al puerto via un server block injectado por
QWeb inherit del runbot.nginx_config.
…task #67684)

Sumo Node 20 (NodeSource) + Claude Code, OpenAI Codex, Google Gemini y
OpenCode al `runbot.docker_layer` template `code_server`. Todo en un solo
RUN para mantener bajo el conteo de image layers. Costo en imagen:
+~1.1 GB de /usr/lib/node_modules (encima de los 430 MB de code-server).

Resuelve el acceptance criteria "CLIs disponibles: Claude Code, Codex CLI,
Gemini CLI, OpenCode. Bake-in en imagen, no install runtime" de la spec
imagen-cli-vscode-server-attacheable.
…cookie + nginx auth_request (task #67684)

Cierra el agujero de que cualquier URL `<dest>-vscode.<host>` adivinada
entraba al editor sin pasar por Odoo.

Approach (Opción C — ver D9 en la spec imagen-cli-vscode-server-attacheable):
- Al clickear "Open VS Code", la route `/runbot/vscode/<id>` valida usuario
  interno + container running, emite un token HMAC-SHA256 sobre
  `database.secret` con payload `build_id|user_id|exp` (TTL 4h), lo setea
  como cookie `vscode_token` con `Domain=<build.host>` (cubre el
  subdominio por RFC 6265), HttpOnly + Secure + SameSite=Lax, y redirige
  al `vscode_url`.
- Nueva route `auth='public'` `/runbot/vscode/auth_check?build=<id>` que
  valida el cookie HMAC + expiry + build_id. La consume nginx vía
  `auth_request` antes de proxiar cada request al subdominio.
- El bloque nginx interno per-build agrega `auth_request /__vscode_auth_check`
  y `error_page 401 403 = @vscode_login_redirect` que rebota a
  `/runbot/vscode/<id>` (que re-emite cookie si hay sesión, o tira a
  /web/login si no). code-server sigue corriendo `--auth none`: la auth
  está toda en la capa nginx.
- El botón backend `action_open_vscode` ahora redirige a la route HTTP
  (no a `vscode_url` directo) porque una action que devuelve
  ir.actions.act_url no puede setear cookies en la response.

Aprovecho para bajar al XML mejoras al bloque nginx que estaban solo en
DB: Connection condicional upgrade/close (en vez de hardcoded "Upgrade"),
`proxy_read_timeout 7d` + `proxy_send_timeout 7d`, y saco los headers
`X-Forwarded-For 127.0.0.42` espurios.

Cambio infra acompañante (one-off, fuera del módulo): el wildcard
`*.runbot.dev-adhoc.com` en /etc/nginx/sites-available/nginx.conf
necesita forwardear Upgrade/Connection en location /. Documentado en
mensaje interno de la task.
@jjscarafia jjscarafia closed this May 21, 2026
@fw-bot-adhoc fw-bot-adhoc deleted the 18.0-t-67684-jjs branch May 28, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants