Skip to content

feat: v0.8.9 UDP ports, partial PUT, CI OIDC providers, Access details - #1402

Merged
thegdsks merged 165 commits into
mainfrom
release/v0.8.9
Oct 28, 2026
Merged

thegdsks merged 165 commits into
mainfrom
release/v0.8.9

Conversation

@thegdsks

@thegdsks thegdsks commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Summary

Everything for v0.8.9, stacked on the v0.8.8 merge.

  • UDP published ports (compose, API, CLI, dashboard, docs), shm_size, run-to-completion services.
  • Crash-loop deploys end FAILED; the API sends outcome and outcome_reason, the dashboard uses them.
  • PUT /apps keeps omitted fields; an explicit empty value clears them.
  • Access details card on the app page and in the template wizard; apps record template_id (migration 3041).
  • GitLab and CircleCI CI OIDC for apps and stacks (migration 3040), apps ci token|trust|workflow, dashboard trust form.
  • Update strip, one-action first run, new templates, many security fixes from independent reviews.
  • CI: Sonar off by default, coverage never fails PRs, heavy lanes skip drafts.

Verification

Gate on the pre-merge tip: build, vet, go test ./internal/api, the short Go batch, migration and CI-plan scripts, 4 vitest shards, tsc, logos all pass. An independent review of the PUT /apps merge found no authz hole.

Not verified on a real host

Authelia start script, UDP with DOCKER-USER, the PUT /apps merge, migration 3040 on existing stack trusts, GitLab and CircleCI against the real providers.

Draft first so the cheap checks run; flip to ready for the full suite.

RetriggerView in GreptileConfidence Score: 0/5

This PR is not ready to merge because the previous public-port exposure and deployment failures remain.

Findings

  1. P1 Security Old public ports stay open ▶
  2. P1 Duplicate port claims pass ▶
  3. P1 Shared-memory edits never apply ▶
  4. P1 RustDesk TCP ports are missing ▶
  5. P1 Go updates cannot clear settings ▶
  6. P1 UDP-only settings cannot save ▶
  7. P2 Access details hide setup notes ▶
Fix with agent prompt
### Issue 1
internal/reconcile/application/port_drift.go:70-84
For a UDP-only app, changing `bind_address` from public to private leaves the public socket open. The main-port check skips `Port == 0`, and `publishedPortsDrifted` compares port numbers without checking addresses. Removing a TCP entry from `published_ports` also leaves its socket open because this check ignores TCP leftovers.

Compare the full expected bindings, including addresses, and exclude only known main-port and stream bindings.

**How this was verified:** The container name stays unchanged on these edits, and neither port check requests a replacement for these stale bindings.

### Issue 2
internal/api/app_published_ports.go:undefined-257
A fresh Compose deploy with two services declaring `5353:53/udp` passes this check. Each service passes its own duplicate check, then the second assignment overwrites the first service in `want`. The later database scan skips both services, so conflicting settings are saved and one service cannot bind its socket.

Reject a key already claimed by another service in `next` before saving the batch.

### Issue 3
internal/reconcile/application/controller.go:1017-1021
A healthy service using `recreate` bypasses `ensureReplicaRunning`, so it never reaches the new `shmDrifted` check. Redeploying the same Compose image with only a changed `shm_size` also leaves the container name unchanged: `composeConfigChanged` does not include shared memory. The app therefore keeps its old `/dev/shm` size after a successful save.

Check this difference before the `recreate` fast return, or make a shared-memory edit trigger a restart.

### Issue 4
internal/catalog/templates_catalog_batch_rustdesk.go:22-26
These TCP host ports do not survive Compose translation. `splitPorts` keeps only the first TCP container port as the routed port and adds only UDP entries to `PublishedPorts`. This template produces a random host binding for TCP 21115 plus UDP 21116, but no TCP 21116 or 21117.

Setting a public bind and opening the documented firewall ports cannot make the relay reachable. Add a supported path for all required direct TCP bindings before offering this template.

### Issue 5
internal/api/apps_update_preserve.go:42-49
`UpdateApp` cannot send clears for fields still marked `omitempty`. Getting an app, setting `HostPort = nil` or `PublishedPorts = []`, and passing it back omits those keys. The new merge then restores the old values despite returning success. Empty domains and nil resource, health, or hook settings have the same problem.

Give updates an explicit way to send empty values, as this PR already does for `Env`.

### Issue 6
web/src/components/PortEditor.tsx:118-122
UDP-only apps have `port: 0`, but `portSchema` requires a positive HTTP port before `onSubmit` runs. Changing a published UDP port or switching its bind to public therefore fails with a port error unless the user invents an HTTP port.

Allow zero for apps without a routed port, matching the API.

### Issue 7
web/src/components/templates/TemplateDescription.tsx:undefined-16
`Manifest.Template` sets `Description` and `Slogan` to the same text, so this condition hides notes for every manifest-backed template. That avoids repetition where the slogan is already shown, but `TemplateAccessCard` does not show it elsewhere. Access details therefore loses notes such as Authelia's user-file location and cookie-domain setup.

Allow the access card to show the description even when it equals the slogan.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR adds UDP published ports, partial app updates, more CI identity providers, and template access details.

  • Apps can publish UDP ports directly on the host.
  • GitLab and CircleCI jobs can request trusted deploy tokens.
  • One-shot services report completion and can hold back dependents.
  • App updates keep fields the request leaves out.
  • Template setup details stay available after deployment.
  • Compose services can set a shared-memory size.
  • CI checks and first-run screens follow updated workflows.

Reviews (2) · Last reviewed commit: "fix(mcp): app tool output no longer fail..." · Reviewed by Greptile

The token after # never reaches the server or its logs. Re-print it with setup-token.
DNS and certificate for a new domain can add about a minute.
It names the branch and the push command; --allow-unpushed overrides.
Exits like deploy and prints the failure reason; --no-wait returns early.
Pinned image with a /healthz check; no login, so restrict access.
Quick start folds into the first deploy card as import and Git links.
UDP publishing ships first; capability and sysctl grants wait on a threat model.
Pinned image, generated credentials and a real health probe.
Pinned image, generated credentials and a real health probe.
Shows the version gap, release notes, an update action and a per-version dismiss.
Single container with data, config and cache volumes.
The update dot now lives on the version line; the logo badge shows health only.
Pinned images, generated password and encryption key, health probe.
Headscale runs env-only; Authelia hashes a generated admin password at boot.
Adds a short not-secure note that links to the Enable HTTPS card.
Compose ports with /udp or protocol: udp now parse and are kept as
host-published ports beside the routed tcp port.
UDP ports bind with the service's bind address, recreate on change, and
are refused for services placed on a remote node.
Apps list their published_ports; a compose deploy fails with a coded
error on a taken udp port, a missing host port or a remote node.
…lia fallback

Open Notebook works behind one public port. Authelia boots without a domain.
Status rows, verified list and a catalog section with security notes.
…rted

Adds the UDP ports page, fixes the compose subset table and links it in the nav.
The API client carries host-published TCP and UDP ports.
The address pill already opens the app, so the duplicate Open app button is gone.
Adds the published_ports wire shape with a host-port/protocol formatter.
The card collapses to a line and a button; the inline link is gone.
Only set behind a TLS-terminating proxy, where plain-http routes exist.
Each port gets a protocol badge beside the host port; apps without extras look unchanged.
Mirrors the server rule and prefers its outcome when present.
Deploy list, cancel and failed responses now carry outcome, outcome_reason and the crash-loop threshold; the CLI prints the reason.
An env-only save no longer drops command, domains, labels or limits.
Send an empty value or null to clear a field.
Generated logins stay reachable from the overview and the header menu; the wizard grid deploy shows the same result dialog.
Stack trusts take the same provider, issuer and protected-ref rules as app trusts.
apps github-actions token and trust keep working and are marked deprecated.
The form shows each provider's fields, the issuer, and the protected-refs warning.
Stages and the first-app card prefer the server outcome and threshold, with the local rule as fallback.
…ate app

Apps now record the template they were deployed from and return it as template_id.
Clearing now sends explicit null or empty; decodeAppUpdate joins the ref guard allowlist.
Brings in the v0.8.8 merge commit so the v0.8.9 PR diff is only v0.8.9 work.
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 371 files, which is 271 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4c23d52d-36fb-4dae-8a9e-9e487f98defb
Commits

Reviewing files that changed from the base of the PR and between f5a826b and 715ed1a.

Files ignored due to path filters: 1
  • internal/agent/agentpb/agent.pb.go is excluded by !**/*.pb.go
Files selected for processing: 371
  • .github/workflows/ci.yml
  • .github/workflows/sonar.yml
  • README.md
  • adr/049-udp-ports-capabilities-sysctls.md
  • cmd/levelrail-cli/apps.go
  • cmd/levelrail-cli/apps_ci.go
  • cmd/levelrail-cli/apps_ci_test.go
  • cmd/levelrail-cli/apps_create.go
  • cmd/levelrail-cli/apps_deploy_compose.go
  • cmd/levelrail-cli/apps_deploy_spec.go
  • cmd/levelrail-cli/apps_deploys_wait.go
  • cmd/levelrail-cli/apps_github_actions.go
  • cmd/levelrail-cli/apps_github_actions_ci.go
  • cmd/levelrail-cli/apps_wait.go
  • cmd/levelrail-cli/apps_wait_crashloop_test.go
  • cmd/levelrail-cli/apps_wait_rtc_test.go
  • cmd/levelrail-cli/client.go
  • cmd/levelrail-cli/console_allowlist_drift_test.go
  • cmd/levelrail-cli/output.go
  • cmd/levelrail-cli/published_ports_hint.go
  • cmd/levelrail-cli/published_ports_hint_test.go
  • cmd/levelrail-cli/published_ports_output_test.go
  • cmd/levelrail-cli/templates.go
  • cmd/levelrail-cli/templates_test.go
  • cmd/levelrail-cli/up.go
  • cmd/levelrail-cli/up_pushcheck.go
  • cmd/levelrail-cli/up_pushcheck_test.go
  • cmd/levelrail-cli/up_test.go
  • cmd/levelrail-cli/up_wait.go
  • cmd/levelrail-cli/up_wait_test.go
  • cmd/levelrail/boot_root_handler.go
  • docs/all-pages.mdx
  • docs/app-spec-reference.md
  • docs/ci-examples/circleci.yml
  • docs/ci-examples/github-actions-oidc.yml
  • docs/ci-examples/gitlab-ci.yml
  • docs/ci.md
  • docs/cli-console.md
  • docs/cli-reference.md
  • docs/compose-apps.md
  • docs/deploying-apps.md
  • docs/docs.json
  • docs/feature-status.md
  • docs/getting-started.md
  • docs/host-firewall.md
  • docs/platform-as-code.md
  • docs/quickstart-one-command.md
  • docs/schemas/resources.schema.json
  • docs/stacks.md
  • docs/template-catalog.md
  • docs/templates-status.md
  • docs/udp-ports.md
  • docs/use-any-ci.md
  • internal/agent/convert.go
  • internal/agent/convert_exittimes_test.go
  • internal/agent/convert_security_test.go
  • internal/agent/grpc_transport.go
  • internal/agent/identity.go
  • internal/api/app_domains.go
  • internal/api/app_domains_port_test.go
  • internal/api/app_published_ports.go
  • internal/api/app_published_ports_holes_test.go
  • internal/api/app_published_ports_lock_test.go
  • internal/api/app_published_ports_mesh_test.go
  • internal/api/app_published_ports_pinned_test.go
  • internal/api/app_published_ports_put_test.go
  • internal/api/app_published_ports_reserved_test.go
  • internal/api/app_published_ports_spec_test.go
  • internal/api/app_published_ports_test.go
  • internal/api/app_udp_only_test.go
  • internal/api/app_urls.go
  • internal/api/app_urls_test.go
  • internal/api/apps.go
  • internal/api/apps_compose.go
  • internal/api/apps_compose_test.go
  • internal/api/apps_health.go
  • internal/api/apps_health_discover.go
  • internal/api/apps_health_discover_test.go
  • internal/api/apps_health_restart_test.go
  • internal/api/apps_resources.go
  • internal/api/apps_resources_test.go
  • internal/api/apps_test.go
  • internal/api/apps_update_preserve.go
  • internal/api/apps_update_preserve_test.go
  • internal/api/body_ref_authz_guard_test.go
  • internal/api/builds.go
  • internal/api/canary.go
  • internal/api/canary_published_ports_test.go
  • internal/api/ci_oidc.go
  • internal/api/ci_oidc_audit.go
  • internal/api/ci_oidc_exchange_audit_test.go
  • internal/api/ci_oidc_gitlab_test.go
  • internal/api/ci_oidc_jwt.go
  • internal/api/ci_oidc_jwt_test.go
  • internal/api/ci_oidc_providers.go
  • internal/api/ci_oidc_stack.go
  • internal/api/ci_oidc_stack_provider_test.go
  • internal/api/ci_oidc_test.go
  • internal/api/ci_oidc_trust_authz_test.go
  • internal/api/cli_console.go
  • internal/api/cli_console_test.go
  • internal/api/deploy_approvals.go
  • internal/api/deploy_approvals_race_test.go
  • internal/api/deploy_attempts.go
  • internal/api/deploy_cancel.go
  • internal/api/deploy_failure.go
  • internal/api/deploy_outcome.go
  • internal/api/deploy_outcome_crashloop_test.go
  • internal/api/deploy_outcome_rtc_test.go
  • internal/api/deploys_failed.go
  • internal/api/environment_move.go
  • internal/api/firewall_host.go
  • internal/api/firewall_host_test.go
  • internal/api/publish_lock.go
  • internal/api/publish_lock_ports_test.go
  • internal/api/quota.go
  • internal/api/quota_shm_test.go
  • internal/api/quota_test.go
  • internal/api/router.go
  • internal/api/service_templates.go
  • internal/api/service_templates_batch_description_test.go
  • internal/api/service_templates_deploy_test.go
  • internal/api/service_templates_test.go
  • internal/api/stacks_approval.go
  • internal/api/stacks_approval_test.go
  • internal/api/stacks_ports_test.go
  • internal/api/store_interfaces_deploy.go
  • internal/apiclient/app_update.go
  • internal/apiclient/app_update_test.go
  • internal/apiclient/ci_oidc.go
  • internal/apiclient/deploy_spec.go
  • internal/apiclient/published_ports_test.go
  • internal/apiclient/types.go
  • internal/catalog/catalog.go
  • internal/catalog/description_test.go
  • internal/catalog/manifest.go
  • internal/catalog/manifests/archivebox/compose.yaml
  • internal/catalog/manifests/archivebox/template.yaml
  • internal/catalog/manifests/authelia/compose.yaml
  • internal/catalog/manifests/authelia/template.yaml
  • internal/catalog/manifests/backrest/compose.yaml
  • internal/catalog/manifests/backrest/template.yaml
  • internal/catalog/manifests/crawl4ai/compose.yaml
  • internal/catalog/manifests/crawl4ai/template.yaml
  • internal/catalog/manifests/headscale/compose.yaml
  • internal/catalog/manifests/headscale/template.yaml
  • internal/catalog/manifests/open-notebook/compose.yaml
  • internal/catalog/manifests/open-notebook/template.yaml
  • internal/catalog/manifests/radicale/compose.yaml
  • internal/catalog/manifests/radicale/template.yaml
  • internal/catalog/manifests/rsshub/compose.yaml
  • internal/catalog/manifests/rsshub/template.yaml
  • internal/catalog/teable_oneshot_test.go
  • internal/catalog/templates_catalog_batch4.go
  • internal/catalog/templates_catalog_batch6.go
  • internal/catalog/templates_catalog_batch_rustdesk.go
  • internal/catalog/templates_productivity_batch.go
  • internal/catalog/verified.json
  • internal/cliconsole/allowlist.go
  • internal/cliconsole/cliconsole_test.go
  • internal/cliconsole/config.go
  • internal/cliconsole/policy.go
  • internal/cliconsole/proc_unix.go
  • internal/cliconsole/runner.go
  • internal/cliconsole/sanitize.go
  • internal/clitree/tree.go
  • internal/compose/compose.go
  • internal/compose/compose_test.go
  • internal/compose/corpus_test.go
  • internal/compose/expand.go
  • internal/compose/limits.go
  • internal/compose/limits_shm_env_test.go
  • internal/compose/limits_test.go
  • internal/compose/notices.go
  • internal/compose/notices_test.go
  • internal/compose/profiles.go
  • internal/compose/restart_parse_test.go
  • internal/compose/restart_test.go
  • internal/compose/translate.go
  • internal/compose/udp_ports_test.go
  • internal/compose/unsupported.go
  • internal/compose/yaml.go
  • internal/compose/yaml_unmarshal_test.go
  • internal/deploy/deploy.go
  • internal/deploy/order.go
  • internal/deploy/preview_host_ports_test.go
  • internal/deploy/published_ports_admission_test.go
  • internal/deploy/restart_spec_test.go
  • internal/deploy/restore.go
  • internal/deploy/restore_restart_test.go
  • internal/deploy/translate.go
  • internal/deployurls/deployurls.go
  • internal/deployurls/deployurls_test.go
  • internal/deploywatch/run_to_completion_test.go
  • internal/deploywatch/watcher.go
  • internal/diagnose/procnet.go
  • internal/diagnose/procnet_test.go
  • internal/docker/client.go
  • internal/docker/client_test.go
  • internal/docker/port_primary.go
  • internal/docker/port_primary_test.go
  • internal/docker/shm.go
  • internal/exposure/udp_test.go
  • internal/failure/rollout_crashloop.go
  • internal/failure/rollout_crashloop_test.go
  • internal/iac/app.go
  • internal/iac/apply_app.go
  • internal/iac/build.go
  • internal/iac/export.go
  • internal/iac/plan_app.go
  • internal/iac/ports_restart_test.go
  • internal/iac/wire.go
  • internal/reconcile/application/compose_stack_live_test.go
  • internal/reconcile/application/controller.go
  • internal/reconcile/application/controller_test.go
  • internal/reconcile/application/depends_on.go
  • internal/reconcile/application/dropped_command_test.go
  • internal/reconcile/application/exit_capture.go
  • internal/reconcile/application/exited_readiness_test.go
  • internal/reconcile/application/liveness.go
  • internal/reconcile/application/pinned_port.go
  • internal/reconcile/application/port_drift.go
  • internal/reconcile/application/probe_remote_target_test.go
  • internal/reconcile/application/probe_target.go
  • internal/reconcile/application/published_ports.go
  • internal/reconcile/application/published_ports_test.go
  • internal/reconcile/application/readiness_requeue.go
  • internal/reconcile/application/revive_last_ready.go
  • internal/reconcile/application/revive_last_ready_test.go
  • internal/reconcile/application/run_to_completion.go
  • internal/reconcile/application/run_to_completion_test.go
  • internal/reconcile/application/shm_drift.go
  • internal/reconcile/application/shm_drift_test.go
  • internal/reconcile/application/stability_gate.go
  • internal/reconcile/ingress/controller.go
  • internal/reconcile/ingress/controller_failover.go
  • internal/reconcile/ingress/controller_loadbalancer_test.go
  • internal/reconcile/ingress/controller_routes_family_test.go
  • internal/reconcile/ingress/controller_test.go
  • internal/reconcile/ingress/dial_port_test.go
  • internal/reconcile/ingress/loadbalancer.go
  • internal/reconcile/ingress/remote_dial.go
  • internal/spec/published_ports.go
  • internal/spec/restart.go
  • internal/spec/schema/app.schema.json
  • internal/spec/spec.go
  • internal/spec/validate.go
  • internal/store/ci_oidc_stack.go
  • internal/store/ci_oidc_trusts.go
  • internal/store/deploy_approval.go
  • internal/store/migration_3039_test.go
  • internal/store/migrations/3036_service_published_ports.sql
  • internal/store/migrations/3037_ci_oidc_trust_providers.sql
  • internal/store/migrations/3038_ci_oidc_gitlab_protected_default.sql
  • internal/store/migrations/3039_ci_oidc_trust_unique_issuer.sql
  • internal/store/migrations/3040_ci_stack_trust_providers.sql
  • internal/store/migrations/3041_service_template_id.sql
  • internal/store/quota.go
  • internal/store/service.go
  • internal/store/service_published_ports_test.go
  • internal/store/service_restart.go
  • internal/store/stacks.go
  • proto/agent/v1/agent.proto
  • scripts/ci-changes.sh
  • scripts/ci-required-verdict.sh
  • scripts/test-ci-changes.sh
  • scripts/test-ci-required-verdict.sh
  • test/e2e/run_to_completion_live_test.go
  • test/e2e/testenv/pull_retry.go
  • test/e2e/testenv/pull_retry_forward_test.go
  • test/e2e/udp_published_ports_live_test.go
  • test/execgate/allowlist.txt
  • web/src/components/AppNetworkPanel.tsx
  • web/src/components/AppSidebar.tsx
  • web/src/components/BrowseTemplatesFields.test.tsx
  • web/src/components/BrowseTemplatesFields.tsx
  • web/src/components/CIDeployCard.test.tsx
  • web/src/components/CIDeployCard.tsx
  • web/src/components/CITrustForm.tsx
  • web/src/components/CreateComposeFields.tsx
  • web/src/components/EnableHttpsCard.test.tsx
  • web/src/components/EnableHttpsCard.tsx
  • web/src/components/HostFirewallCard.test.tsx
  • web/src/components/HostFirewallCard.tsx
  • web/src/components/PendingDeployApprovalBanner.tsx
  • web/src/components/PortEditor.test.tsx
  • web/src/components/PortEditor.tsx
  • web/src/components/PublishedPortError.test.tsx
  • web/src/components/PublishedPortError.tsx
  • web/src/components/PublishedPortsField.test.tsx
  • web/src/components/PublishedPortsField.tsx
  • web/src/components/apps/AppActionBar.test.tsx
  • web/src/components/apps/AppActionBar.tsx
  • web/src/components/apps/AppPaletteActions.test.tsx
  • web/src/components/apps/AppPaletteActions.tsx
  • web/src/components/apps/AppPowerConfirm.test.tsx
  • web/src/components/apps/AppPowerConfirm.tsx
  • web/src/components/apps/appPower.test.ts
  • web/src/components/apps/appPower.ts
  • web/src/components/apps/useAppPower.test.tsx
  • web/src/components/apps/useAppPower.tsx
  • web/src/components/dashboard/DashboardHome.test.tsx
  • web/src/components/dashboard/DashboardHome.tsx
  • web/src/components/dashboard/QuickStart.tsx
  • web/src/components/dashboard/StatusStrip.tsx
  • web/src/components/dashboard/UpdateStrip.test.tsx
  • web/src/components/dashboard/UpdateStrip.tsx
  • web/src/components/deploy/DeployApprovalStatusBadge.test.tsx
  • web/src/components/deploy/DeployApprovalStatusBadge.tsx
  • web/src/components/dock/DockLauncher.test.tsx
  • web/src/components/dock/DockLauncher.tsx
  • web/src/components/getStarted/FirstAppStatus.test.tsx
  • web/src/components/getStarted/FirstAppStatus.tsx
  • web/src/components/getStarted/FirstDeploy.test.tsx
  • web/src/components/getStarted/FirstDeploy.tsx
  • web/src/components/getStarted/FirstDeployLive.test.tsx
  • web/src/components/overview/HeroActions.test.tsx
  • web/src/components/overview/HeroActions.tsx
  • web/src/components/overview/OverviewPage.tsx
  • web/src/components/overview/OverviewSuggestions.tsx
  • web/src/components/overview/useOverviewActions.ts
  • web/src/components/overview/useSuggestionActions.test.tsx
  • web/src/components/overview/useSuggestionActions.tsx
  • web/src/components/shell/SidebarStatusBadge.test.tsx
  • web/src/components/shell/SidebarStatusBadge.tsx
  • web/src/components/shell/SidebarVersionLink.test.tsx
  • web/src/components/shell/SidebarVersionLink.tsx
  • web/src/components/templates/AppAccessDetails.test.tsx
  • web/src/components/templates/AppAccessDetails.tsx
  • web/src/components/templates/TemplateAccessCard.test.tsx
  • web/src/components/templates/TemplateAccessCard.tsx
  • web/src/components/templates/TemplateCredentialRow.tsx
  • web/src/components/templates/TemplateDeployResult.tsx
  • web/src/components/templates/TemplateDescription.test.tsx
  • web/src/components/templates/TemplateDescription.tsx
  • web/src/components/templates/templateAccess.ts
  • web/src/components/ui/confirm-dialog.tsx
  • web/src/hooks/useDeployTemplateNow.ts
  • web/src/lib/ciProviders.ts
  • web/src/lib/deployApprovalStatus.ts
  • web/src/lib/deployStages.test.ts
  • web/src/lib/deployStages.ts
  • web/src/lib/firstApp.ts
  • web/src/lib/publishedPortsInput.test.ts
  • web/src/lib/publishedPortsInput.ts
  • web/src/lib/templateLogos.ts
  • web/src/lib/updateBehind.test.ts
  • web/src/lib/updateBehind.ts
  • web/src/lib/updateDismiss.ts
  • web/src/locales/en/common.json
  • web/src/locales/en/compose.json
  • web/src/locales/en/dashboard.json
  • web/src/locales/en/deploys.json
  • web/src/locales/en/getStarted.json
  • web/src/locales/en/https.json
  • web/src/locales/en/settings.json
  • web/src/locales/en/templates.json
  • web/src/queries/appUrls.ts
  • web/src/queries/ciDeploy.ts
  • web/src/queries/serviceTemplates.ts
  • web/src/routes/__root.tsx
  • web/src/routes/approvals/index.tsx
  • web/src/routes/templates/$id.tsx
  • web/src/routes/templates/index.tsx
  • web/src/types/appDetail.ts
  • web/src/types/compose.ts
  • web/src/types/deployApproval.ts
  • web/src/types/deployAttempt.ts
  • web/src/types/hostFirewall.ts
  • web/src/types/publishedPort.test.ts
  • web/src/types/publishedPort.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added area/ci .github/, scripts/ area/store internal/store (SQLite, migrations) area/reconciler internal/reconcile area/frontend web/ area/spec internal/spec (app.yaml parsing) area/api internal/api area/deploy internal/deploy, internal/deploylog area/cli cmd/levelrail-cli area/docker internal/docker (Engine API client) type/docs Documentation only area/agent internal/agent, cmd/levelrail-agent size/XXL labels Oct 28, 2026
@glinr-bot

glinr-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

glinr-bot

A protected condition applies, so a person decides.

Gate Result Detail
not a draft pass
from this repository, not a fork pass
touches no protected path fail .github/workflows/ci.yml, .github/workflows/sonar.yml, internal/store/migrations/3036_service_published_ports.sql, internal/store/migrations/3037_ci_oidc_trust_providers.sql, internal/store/migrations/3038_ci_oidc_gitlab_protected_default.sql, internal/store/migrations/3039_ci_oidc_trust_unique_issuer.sql, internal/store/migrations/3040_ci_stack_trust_providers.sql, internal/store/migrations/3041_service_template_id.sql
has no blocking label pass

No action taken. A person reviews this one.

@thegdsks
thegdsks marked this pull request as ready for review October 28, 2026 20:19
Comment on lines +70 to +84
have := map[store.PublishedPort]bool{}
for _, p := range ports {
have[store.PublishedPort{ContainerPort: p.ContainerPort, HostPort: p.HostPort, Protocol: bindingProtocol(p)}] = true
}
want := map[store.PublishedPort]bool{}
for _, p := range desired.PublishedPorts {
key := store.PublishedPort{ContainerPort: p.ContainerPort, HostPort: p.HostPort, Protocol: p.EffectiveProtocol()}
want[key] = true
if !have[key] {
return true
}
}
// Only non-tcp leftovers count: tcp bindings also come from the main port and app streams.
for k := range have {
if k.Protocol != store.PortProtocolTCP && !want[k] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Old public ports stay open

For a UDP-only app, changing bind_address from public to private leaves the public socket open. The main-port check skips Port == 0, and publishedPortsDrifted compares port numbers without checking addresses. Removing a TCP entry from published_ports also leaves its socket open because this check ignores TCP leftovers.

Compare the full expected bindings, including addresses, and exclude only known main-port and stream bindings.

How this was verified: The container name stays unchanged on these edits, and neither port check requests a replacement for these stale bindings.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/reconcile/application/port_drift.go
Line: 70-84

Comment:
**Old public ports stay open**

For a UDP-only app, changing `bind_address` from public to private leaves the public socket open. The main-port check skips `Port == 0`, and `publishedPortsDrifted` compares port numbers without checking addresses. Removing a TCP entry from `published_ports` also leaves its socket open because this check ignores TCP leftovers.

Compare the full expected bindings, including addresses, and exclude only known main-port and stream bindings.

**How this was verified:** The container name stays unchanged on these edits, and neither port check requests a replacement for these stale bindings.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

return refusePort(http.StatusBadRequest, codePublishedPortRemoteNode,
fmt.Sprintf("host port %s can only be published on the control plane's own node, service %q is placed on node %q", p, svc.Name, node))
}
want[portKey(p)] = svc.Name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Duplicate port claims pass

A fresh Compose deploy with two services declaring 5353:53/udp passes this check. Each service passes its own duplicate check, then the second assignment overwrites the first service in want. The later database scan skips both services, so conflicting settings are saved and one service cannot bind its socket.

Reject a key already claimed by another service in next before saving the batch.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/api/app_published_ports.go
Line: 257

Comment:
**Duplicate port claims pass**

A fresh Compose deploy with two services declaring `5353:53/udp` passes this check. Each service passes its own duplicate check, then the second assignment overwrites the first service in `want`. The later database scan skips both services, so conflicting settings are saved and one service cannot bind its socket.

Reject a key already claimed by another service in `next` before saving the batch.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +1017 to +1021
if shmDrifted(state, desired) {
if err := c.removeContainer(ctx, state.ID); err != nil {
return replicaOutcome{reason: reasonShmDriftRecreate}, fmt.Errorf("remove %q to apply its changed shm_size: %w", target, err)
}
state = nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Shared-memory edits never apply

A healthy service using recreate bypasses ensureReplicaRunning, so it never reaches the new shmDrifted check. Redeploying the same Compose image with only a changed shm_size also leaves the container name unchanged: composeConfigChanged does not include shared memory. The app therefore keeps its old /dev/shm size after a successful save.

Check this difference before the recreate fast return, or make a shared-memory edit trigger a restart.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/reconcile/application/controller.go
Line: 1017-1021

Comment:
**Shared-memory edits never apply**

A healthy service using `recreate` bypasses `ensureReplicaRunning`, so it never reaches the new `shmDrifted` check. Redeploying the same Compose image with only a changed `shm_size` also leaves the container name unchanged: `composeConfigChanged` does not include shared memory. The app therefore keeps its old `/dev/shm` size after a successful save.

Check this difference before the `recreate` fast return, or make a shared-memory edit trigger a restart.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +22 to +26
ports:
- "21115:21115"
- "21116:21116"
- "21116:21116/udp"
- "21117:21117"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 RustDesk TCP ports are missing

These TCP host ports do not survive Compose translation. splitPorts keeps only the first TCP container port as the routed port and adds only UDP entries to PublishedPorts. This template produces a random host binding for TCP 21115 plus UDP 21116, but no TCP 21116 or 21117.

Setting a public bind and opening the documented firewall ports cannot make the relay reachable. Add a supported path for all required direct TCP bindings before offering this template.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/catalog/templates_catalog_batch_rustdesk.go
Line: 22-26

Comment:
**RustDesk TCP ports are missing**

These TCP host ports do not survive Compose translation. `splitPorts` keeps only the first TCP container port as the routed port and adds only UDP entries to `PublishedPorts`. This template produces a random host binding for TCP 21115 plus UDP 21116, but no TCP 21116 or 21117.

Setting a public bind and opening the documented firewall ports cannot make the relay reachable. Add a supported path for all required direct TCP bindings before offering this template.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +42 to +49
keep("host_port", func() { req.HostPort = existing.HostPort })
keep("published_ports", func() { req.PublishedPorts = existing.PublishedPorts })
keep("bind_address", func() { req.BindAddress = existing.BindAddress })
keep("domains", func() { req.Domains = existing.Domains })
keep("env", func() { req.Env = existing.Env })
keep("resources", func() { req.Resources = existing.Resources })
keep("health", func() { req.Health = existing.Health })
keep("hooks", func() { req.Hooks = existing.Hooks })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Go updates cannot clear settings

UpdateApp cannot send clears for fields still marked omitempty. Getting an app, setting HostPort = nil or PublishedPorts = [], and passing it back omits those keys. The new merge then restores the old values despite returning success. Empty domains and nil resource, health, or hook settings have the same problem.

Give updates an explicit way to send empty values, as this PR already does for Env.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/api/apps_update_preserve.go
Line: 42-49

Comment:
**Go updates cannot clear settings**

`UpdateApp` cannot send clears for fields still marked `omitempty`. Getting an app, setting `HostPort = nil` or `PublishedPorts = []`, and passing it back omits those keys. The new merge then restores the old values despite returning success. Empty domains and nil resource, health, or hook settings have the same problem.

Give updates an explicit way to send empty values, as this PR already does for `Env`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +118 to +122
const { control, register, handleSubmit, watch, setError, formState } =
useForm<PortFormInput, unknown, PortFormOutput>({
resolver: zodResolver(portSchema),
values: toFieldValues(app),
resetOptions: { keepDirtyValues: true },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 UDP-only settings cannot save

UDP-only apps have port: 0, but portSchema requires a positive HTTP port before onSubmit runs. Changing a published UDP port or switching its bind to public therefore fails with a port error unless the user invents an HTTP port.

Allow zero for apps without a routed port, matching the API.

Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/components/PortEditor.tsx
Line: 118-122

Comment:
**UDP-only settings cannot save**

UDP-only apps have `port: 0`, but `portSchema` requires a positive HTTP port before `onSubmit` runs. Changing a published UDP port or switching its bind to public therefore fails with a port error unless the user invents an HTTP port.

Allow zero for apps without a routed port, matching the API.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

}: TemplateDescriptionProps) {
const { t } = useTranslation('templates')
const text = description?.trim()
if (!text || text === slogan?.trim()) return null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Access details hide setup notes

Manifest.Template sets Description and Slogan to the same text, so this condition hides notes for every manifest-backed template. That avoids repetition where the slogan is already shown, but TemplateAccessCard does not show it elsewhere. Access details therefore loses notes such as Authelia's user-file location and cookie-domain setup.

Allow the access card to show the description even when it equals the slogan.

Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/components/templates/TemplateDescription.tsx
Line: 16

Comment:
**Access details hide setup notes**

`Manifest.Template` sets `Description` and `Slogan` to the same text, so this condition hides notes for every manifest-backed template. That avoids repetition where the slogan is already shown, but `TemplateAccessCard` does not show it elsewhere. Access details therefore loses notes such as Authelia's user-file location and cookie-domain setup.

Allow the access card to show the description even when it equals the slogan.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

UpdateApp still sends an empty env so the last key can be cleared.
@thegdsks
thegdsks merged commit e163305 into main Oct 28, 2026
38 checks passed
@thegdsks
thegdsks deleted the release/v0.8.9 branch October 28, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/agent internal/agent, cmd/levelrail-agent area/api internal/api area/ci .github/, scripts/ area/cli cmd/levelrail-cli area/deploy internal/deploy, internal/deploylog area/docker internal/docker (Engine API client) area/frontend web/ area/reconciler internal/reconcile area/spec internal/spec (app.yaml parsing) area/store internal/store (SQLite, migrations) size/XXL type/docs Documentation only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant