Skip to content

feat(deploy): Template Entries decide what a template deployment opens and shares (ADR-0081) - #346

Draft
aimeritething wants to merge 3 commits into
mainfrom
feat/template-entries
Draft

feat(deploy): Template Entries decide what a template deployment opens and shares (ADR-0081)#346
aimeritething wants to merge 3 commits into
mainfrom
feat/template-entries

Conversation

@aimeritething

@aimeritething aimeritething commented Sep 8, 2026

Copy link
Copy Markdown
Member

Decision (product owner, 2026-09-08 — documented in ADR-0081)

Sealos templates may declare Template Entries in their header:

spec:
  entries:
    open: https://${{ defaults.app_host }}.${{ SEALOS_CLOUD_DOMAIN }}/admin
    share: https://${{ defaults.app_host }}.${{ SEALOS_CLOUD_DOMAIN }}/?server=wss://${{ defaults.app_host }}.${{ SEALOS_CLOUD_DOMAIN }}/

Both are full URLs with the template's normal ${{ }} substitution. Open is what the Open control opens after a template deployment; Share is what the Success Record's share strip copies / QR-encodes / posts. Entries are not probed (decided 2026-09-09, below): an entry is kept only when an Ingress of the same deployment serves its host, and that host is a required Deployment Access Endpoint whose probe already gates the record; whether the app answers on a path is not routing health (CONTEXT.md, Public Address Health). The Open Entry is the record's first entry, headed by the App Listening Port it reaches when an AP observed that address (de-duplicated by full URL with the Ingress card's entry); the Share Entry is the record's share address and is never listed as an entry. When the Open URL maps to an App Listening Port (URL host = Ingress rule host → longest-prefix path → backend Service + port), Brain writes brain.io/default-open-port: "<port>" on that Service unless the template preset it, so the AP Public Access Node's Open and the record's Open agree.

This deliberately revises CONTEXT.md's Default Open Port sentence "there is no Project-level or Template-Instance-level open link"; ADR-0079 and ADR-0080 get Status sections pointing at the amendment.

Fallback chains

Control 1st 2nd 3rd
Open entries.open App CR spec.data.url (app.sealos.io/v1 App) automatic Default Open Port rule (unchanged)
Share entries.share the resolved Open URL (today's behaviour)
Service annotation template's own brain.io/default-open-port (kept) Brain preset from the Open URL when it matches one Ingress host + path → Service port nothing written

Share never falls back to the App CR URL on its own (catalog App CR urls include #token=… and /invite/<code>). An entry whose host no Ingress of the deployment serves is dropped; credentials and fragments disqualify an entry.

Where it runs

  • Brain's renderer (sealos-template artifacts): the rules run over the rendered docs before YAML is dumped, so the annotation ships with the apply. RenderedTemplateDeployment gains entries?.
  • Provider-applied templates (template-instance-pending / template-instance, i.e. the catalog path): after the Ingress read-back, the runner reads the template source (spec.entries off templateYaml), the resolved defaults off the Instance CR, this run's in-memory inputs, and the Ingresses / Services / App CR from the cluster, applies the same pure rules, and PATCHes the matched Service. Any failure there degrades to "no declared entry" (console warning), never a failed deployment.

Files changed

New

  • apps/ui/src/features/deploy/template-entries.ts — pure rules over rendered docs (+ .test.ts, 13 tests)
  • apps/ui/src/features/deploy/task/template-provider-entries.ts — provider-path read-back + Service patch (+ .test.ts, 4 tests, globalThis.fetch stub)
  • docs/adr/0081-declare-template-entries-for-open-and-share.md

Changed

  • apps/ui/src/lib/brain-labels.tsBRAIN_DEFAULT_OPEN_PORT_ANNOTATION
  • apps/ui/src/features/deploy/template-renderer.ts — render entries with the resource context, preset annotation, renderTemplateEntryExpressions for the provider path
  • apps/ui/src/features/deploy/task/timeline.tsDeploymentTemplateEntryUrls, shareUrl on the record (sanitizer + signature), deploymentTaskSuccessFromTimeline({ templateEntries, templateOpenEntryLabel }) takes the entries as declared, deploymentTaskSuccessShareUrl
  • apps/ui/src/features/deploy/task/result-readiness.tsaccessEndpointPortLabelForUrl exported (the Ingress-host naming rule, reused to head the Open Entry)
  • apps/ui/src/features/deploy/task/runner.ts — wire both paths, head the Open Entry by its port, pass entries to the success builder; no cards for entries
  • apps/ui/src/features/deploy/deployment-task-success-section.tsx — share strip uses shareUrl (fallback: primary entry for old records)
  • CONTEXT.md — Default Open Port paragraph revised; new glossary term Template Entry (Open Entry / Share Entry); Deployment Access Endpoint and Deployment Task Success Record updated
  • docs/adr/README.md, docs/adr/0079-*.md, docs/adr/0080-*.md — index line and Status sections

Verification

Run from the repo root unless noted.

Command Result
bun check Checked 1641 files … No fixes applied. — clean
bun typecheck 5/6 tasks pass; @sealai/ui#typecheck fails on one pre-existing error in src/app/preview-canvas-perf/page.tsx(27,3) ('calendar' does not exist in type …ContainerNodeQuickActionKey…). Reproduced identically on a clean origin/main (git stash -u && bun run typecheck); not touched by this PR.
cd apps/ui && bun test src/features/deploy/template-entries.test.ts src/features/deploy/template-renderer.test.ts src/features/deploy/task/direct-timeline.test.ts src/features/deploy/task/timeline.test.ts 90 pass, 0 fail
cd apps/ui && bun test src/features/deploy/task/result-readiness.test.ts 16 pass, 0 fail
cd apps/ui && bun test src/features/deploy/task/template-provider-entries.test.ts 4 pass, 0 fail
cd apps/ui && bun test src/features/deploy/task/runner.template-preserve.test.ts 5 pass, 0 fail
cd apps/ui && bun test src/features/deploy (whole tree, one process) 506 pass / 42 fail vs. baseline origin/main 485 pass / 42 fail — the same 42 pre-existing failures (bun mock.module leaking across files in result-readiness and the GitHub connection suites). No baseline-passing test regressed.

Tests cover: entries parsing + ${{ }} substitution; the Open chain (entries.open → App CR → automatic); Share never derived from the App CR; Service annotation written on host+longest-prefix-path match (numbered and named ports), not written on no match, kept when preset; the Open Entry leading the record (de-duplicated with an Ingress card's entry, or added headed by its port, or by nothing when no AP observed it); the Share Entry as shareUrl only, never an entry, and never a WS(S) URL; record shareUrl snapshot with fallback and old records without it; the share strip helper; provider-path read-back (Instance defaults, Service PATCH, preset respected, degrade on provider failure).

Not verified (no live cluster)

  • The provider path end to end: that POST /api/v2alpha/templates/instances leaves resolved spec.defaults[*].value on the Instance CR (Brain's own renderer does; if the provider does not, entries render empty, fail the Ingress-host gate, and Open falls back to the App CR url — the code path is tested, the provider behaviour is not).
  • That the API's k8s/get resolves kind=apps to apps.app.sealos.io (discovery matches plural/singular/shortname across lists; instances already works the same way).
  • The mergeDefaultOpenPort read of the presetted annotation on a real AP Product View.
  • Visual check of the share strip with a shareUrl that differs from the primary entry.

Decisions (product owner, 2026-09-09)

The four open questions this PR first carried were settled with the product owner:

  1. Template Entries are not probed. Their host is an Ingress host of the same deployment, which is a required Deployment Access Endpoint already verified before the record exists; an app's response on a path is not routing health (the Public Address Health definition already excludes 404/500). So there are no entry cards, nothing gates, nothing falls back silently. ADR-0081, ADR-0079's Status, and CONTEXT.md (Template Entry, Deployment Access Endpoint, Deployment Task Success Record) rewritten accordingly.
  2. The Share Entry is not a record entry. It feeds the share strip only. The Open Entry is the record's first entry.
  3. shareUrl is written on every new record — the record is a snapshot; readers keep the primary-entry fallback for records written before the field.
  4. The App CR fallback stays a fallback, documented in ADR-0081 only; template authors are pointed at spec.entries. (No template-author docs live in this repo.)

🤖 Generated with Claude Code

https://claude.ai/code/session_012nPEVVc7yiShA1SrfuTUqM

aimeritething and others added 2 commits September 8, 2026 22:24
…s and shares

A Sealos Template may now declare `spec.entries.open` and
`spec.entries.share`, full URLs rendered with the template's own `${{ }}`
substitution. Open is what the Open control opens after the deployment;
Share is what the Success Record's share strip copies, shows as a QR code
and posts. Open falls back to the template's Sealos App CR `spec.data.url`
(212 catalog templates carry one; Brain applied it and never read it), then
to the automatic Default Open Port rule. Share falls back to the Open URL
only — never to the App CR url, which may embed `#token=` or
`/invite/<code>`.

Both entries are Deployment Access Endpoints with a declared URL (ADR
0079's contract): probed verbatim, query string kept, de-duplicated by full
URL, admitted to the record only once verified. They are optional
evidence — a declared link that fails its probe stays out of the record
instead of failing a deployment the Ingress-derived entries already prove
usable. When the Open URL matches an Ingress host and its longest-prefix
path to a Service port, Brain presets `brain.io/default-open-port` on that
Service unless the template already set it, so the AP Public Access Node's
Open and the record's Open agree.

The rules live in one pure module over rendered documents. Brain's own
renderer runs them before dumping the YAML, so the annotation ships with
the apply; for provider-applied templates the runner reads the same facts
back (template source, Instance defaults, in-memory inputs, Ingresses,
Services, App CR) once the Ingresses exist and patches the matched Service.
An entry no Ingress of the deployment serves is dropped, which also keeps
an unresolved expression from ever leaking into a URL.

The Success Record snapshots its share address as `shareUrl`; the section
shares that, falling back to the primary entry for records written before
the field existed. Old records keep loading unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012nPEVVc7yiShA1SrfuTUqM
Records the 2026-09-08 decision that a template may declare its entries,
with the fallback chains, the probe-verified-but-optional gate, the
Service annotation preset, and the Ingress-host boundary. CONTEXT.md's
Default Open Port no longer says a Template Instance has no open link and
gains the Template Entry glossary term (Open Entry / Share Entry); the
Deployment Access Endpoint and Success Record entries name the new source
and the `shareUrl` snapshot. ADR-0079 and ADR-0080 get Status sections
pointing at the amendment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012nPEVVc7yiShA1SrfuTUqM
Decided with the product owner on 2026-09-09. A Template Entry's host is
an Ingress host of the same deployment, and every such host is a required
Deployment Access Endpoint whose probe already gates the Success Record. A
second probe of the full URL would only test whether the application
answers on that path, which the Public Address Health definition already
excludes from health; and as optional evidence it silently swapped the
declared Open for the automatic rule while the Service annotation named the
declared port. So entries are no longer Deployment Access Endpoint cards.

The record takes them as declared: the Open Entry is its first entry,
sharing the Ingress card's entry when one lists the same URL, else added
and headed by the App Listening Port it reaches when an AP observed that
address. The Share Entry is only the record's share address and is never
listed as an entry. shareUrl is still written on every new record, and the
App CR fallback stays a fallback recorded only in ADR-0081.

ADR-0081's verification section, ADR-0079's Status, the ADR index line,
and CONTEXT.md's Template Entry, Deployment Access Endpoint, and Success
Record entries say the same.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZuqvxhtEq1d99vxZSaiLE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant