App Catalog¶
The App Catalog is a Headlamp plugin (maintained in headlamp-k8s/plugins) that provides a Helm-based application marketplace inside the Headlamp UI. As of 2025 it supports both Artifact Hub and vanilla Helm repos as sources, and can run entirely in-cluster using the service proxy.
Prerequisites¶
- Headlamp deployed with
--enable-helm(see --enable-helm) - OIDC authentication (service account token auth has a known issue with Helm release listing)
Install the App Catalog plugin¶
Deploy an in-cluster Helm repo¶
The App Catalog needs a Helm repo it can reach. The simplest self-hosted option is ChartMuseum. For production, consider Harbor (OCI + Helm) or a private registry.
helm repo add chartmuseum https://chartmuseum.github.io/charts
helm repo update
helm install my-catalog chartmuseum/chartmuseum \
--namespace default \
--set env.open.STORAGE=local \
--set persistence.enabled=false
# verify
kubectl get svc -n default | grep my-catalog
# → my-catalog-chartmuseum ClusterIP 10.96.x.x 80/TCP
Harbor provides OCI and Helm repo support with authentication. Deploy via the Harbor Helm chart and configure App Catalog with the Helm repo URL from Harbor's UI.
Configure App Catalog to use the in-cluster repo¶
Once the plugin loads, navigate to Settings → App Catalog in the Headlamp UI. Add a new repository:
| Field | Value |
|---|---|
| Name | My Catalog (arbitrary label) |
| URL | /serviceproxy/default/my-catalog-chartmuseum/ |
| Type | Helm |
Base URL
If Headlamp runs with a --base-url prefix (e.g. /headlamp), the URL must include it:
The App Catalog plugin calls:
{repo-url}/api/charts— enumerate all charts{repo-url}/api/charts/{name}— fetch chart metadata and versions{repo-url}/api/charts/{name}/{version}.tgz— download for install/upgrade
Pushing charts to your in-cluster repo¶
# package your chart
helm package ./my-chart
# push to ChartMuseum (requires DISABLE_API=false)
curl -X POST \
--data-binary "@my-chart-0.1.0.tgz" \
http://localhost:8080/serviceproxy/default/my-catalog-chartmuseum/api/charts
# or via kubectl port-forward to push directly
kubectl port-forward -n default service/my-catalog-chartmuseum 8888:80 &
helm cm-push my-chart-0.1.0.tgz http://localhost:8888
What App Catalog shows¶
- All charts in the configured repos with name, description, and icon
- Current installed version vs latest available version for each chart
- Installation form with configurable values
- Upgrade/rollback controls for installed releases
- Helm release history
Troubleshooting¶
App Catalog shows "failed to fetch charts"
- Confirm
--enable-helmis active:kubectl logs deployment/headlamp -n kube-system | grep helm - Confirm the service proxy URL is correct (namespace, service name, base URL prefix)
- Test the service proxy directly:
- Check Headlamp RBAC includes
services/proxyverbs
Helm release listing returns 403
This is the known issue #4788 with service account token auth. Switch to OIDC authentication.
Plugin not loading
kubectl logs -n kube-system -l app.kubernetes.io/component=plugins-manager
# look for download or extraction errors
The Elephant in the Room: Silent Failure Analysis¶
The App Catalog is shipped and functional in the desktop application but silently non-operational in every default in-cluster deployment. There are 2 code bugs and 5 undocumented deployment requirements. None produce error messages.
This section documents findings from a deep codebase investigation and call-graph analysis. See Technical Debt for the full danger zone inventory.
The 7 Gaps¶
| # | Category | What's Broken | Impact | Fix |
|---|---|---|---|---|
| 1 | Code Bug | helmRouteReleaseHandler guards setTokenFromCookie() behind OIDC check |
ALL Helm release operations return system:anonymous in non-OIDC deployments |
Make setTokenFromCookie() unconditional (tracked upstream) |
| 2 | Code Bug | RouteSwitcher.tsx uses identical React key getCluster() for all <AuthRoute> components |
Dynamically registered routes (from plugin async callbacks) never resolve — 404 on click | Use route.path in key |
| 3 | Deployment | app-catalog plugin not in container image | Plugin never loads in-cluster. No "Apps" sidebar section. | Add to container/build-manifest.json or install via sidecar |
| 4 | Deployment | --proxy-urls not set (desktop sets it automatically) |
External proxy rejects ArtifactHub API requests | Add --proxy-urls=https://artifacthub.io/* |
| 5 | Deployment | No catalog Service template in Helm chart | Plugin discovers zero catalogs, registers nothing | Create Service with catalog.headlamp.dev/is-catalog label |
| 6 | Deployment | catalog.headlamp.dev/protocol annotation undocumented |
Sidebar entry appears but page route never registered — 404 | Add annotation (helm or artifacthub) |
| 7 | Architecture | ExternalName Service doesn't work through service proxy | TLS/SNI failures when proxying to external hostnames | Deploy in-cluster reverse proxy |
Why It Matters¶
7 failure modes. 0 error messages in the default case. 0 log lines in 5 of the 7 cases.
The cumulative effect: an operator deploys Headlamp with enableHelm: true, sees no "Apps" section, no errors, no logs, and concludes the feature does not exist in the in-cluster version. This is exactly what happened during the VKS team evaluation.
Desktop vs. In-Cluster Parity Gap¶
The desktop app ships with all 7 items pre-configured:
| Requirement | Desktop | In-Cluster |
|---|---|---|
| Plugin installed | Bundled in app-build-manifest.json |
Must install via sidecar or initContainer |
--proxy-urls |
Set automatically from build manifest | Must configure manually |
| Catalog discovery | Uses different code path (no Service labels needed) | Requires Service with specific label |
| Protocol annotation | Not needed (uses hardcoded ArtifactHub path) | Required but undocumented |
| Reverse proxy | Direct outbound HTTP from Electron | Need in-cluster nginx for external sources |
| OIDC cookie auth | Bypassed (desktop auth model) | Bug #1: cookie auth gated behind OIDC check |
| Route registration | Static (plugin loaded at build time) | Bug #2: React key collision breaks dynamic routes |
The Discovery Protocol (Undocumented)¶
The app-catalog plugin discovers catalogs via a K8s Service label convention that is documented nowhere in the repository:
For each discovered Service, the plugin reads annotations:
| Annotation | Required | Values | Purpose |
|---|---|---|---|
catalog.headlamp.dev/is-catalog |
Yes (label) | "" (empty) |
Triggers plugin discovery |
catalog.headlamp.dev/name |
Yes | Any string | Internal identifier |
catalog.headlamp.dev/displayName |
Yes | Any string | Shown in UI |
catalog.headlamp.dev/protocol |
Yes | helm or artifacthub |
Determines which handler registers routes |
Without the protocol annotation, the sidebar entry registers (unconditional) but the page route does not (conditional on protocol). Clicking the sidebar entry shows a 404.
Root Cause: Code Bug #1¶
backend/cmd/headlamp.go line 1381-1383, function helmRouteReleaseHandler:
// BEFORE (buggy): only extracts token when OIDC is configured
if c.UseInCluster && context.OidcConf != nil {
setTokenFromCookie(r, clusterName)
}
// AFTER (fixed): unconditional, matches helmRouteRepositoryHandler behavior
setTokenFromCookie(r, clusterName)
The setTokenFromCookie() function is a no-op when no cookie exists. The repository handler (immediately below in the same file) already calls it unconditionally without issues. The inconsistency went unnoticed because desktop and OIDC deployments bypass this path.
Root Cause: Code Bug #2¶
frontend/src/components/App/RouteSwitcher.tsx line 72-73:
// BEFORE: all routes share same key — React can't distinguish them
key={getCluster()} // e.g., "main" for every route
// AFTER: each route has unique key — React reconciliation works
key={`${route.path}-${getCluster()}`}
Static routes work because they exist from the initial render. Dynamic routes (registered inside async callbacks after fetchCatalogs() resolves) fail because React cannot reconcile them as new children when all siblings share the same key.
Complete Fix Deployment¶
See VKS Deployment Guide for the full step-by-step deployment including code fixes, catalog Service, and reverse proxy.