From 3b99065307a92be928b3fd72a0f1c34a8e6f77a1 Mon Sep 17 00:00:00 2001 From: Tomas Kral Date: Fri, 27 Feb 2026 12:20:47 +0100 Subject: [PATCH] docs: add architecture documentation for dynamic plugin loading and catalog index Add two new documents under docs/architecture/ covering: - Dynamic plugin loading: init container pattern, install-dynamic-plugins.py walkthrough, plugin installation mechanics, configuration merging - Catalog index build: build pipeline, input sources across repos, individual plugin OCI image builds, runtime consumption flow Co-Authored-By: Claude Opus 4.6 --- docs/architecture/catalog-index-build.md | 370 ++++++++++++++++++++ docs/architecture/dynamic-plugin-loading.md | 194 ++++++++++ 2 files changed, 564 insertions(+) create mode 100644 docs/architecture/catalog-index-build.md create mode 100644 docs/architecture/dynamic-plugin-loading.md diff --git a/docs/architecture/catalog-index-build.md b/docs/architecture/catalog-index-build.md new file mode 100644 index 0000000000..5bfc07d659 --- /dev/null +++ b/docs/architecture/catalog-index-build.md @@ -0,0 +1,370 @@ +# Catalog Index Image: Build Process and Architecture + +This document explains how the `plugin-catalog-index` OCI image is built, what feeds into it, and how it is consumed across the RHDH ecosystem. + +## What is the Catalog Index Image? + +The catalog index image (`quay.io/rhdh/plugin-catalog-index:`) is a minimal `FROM scratch` OCI image that serves as a **distribution mechanism** for two things: + +1. **Default plugin configurations** (`dynamic-plugins.default.yaml`) — the master list of all available dynamic plugins, their OCI image references, default enabled/disabled state, and default `pluginConfig` +2. **Extensions catalog entities** (`catalog-entities/extensions/`) — Backstage catalog YAML files (kind `Plugin` and `Package`) that power the RHDH Extensions UI + +By packaging these as a standalone OCI image, the default plugin list and catalog metadata can be updated **independently** of the main RHDH container image. + +## Image Contents + +``` +/ (image root, FROM scratch) +├── dynamic-plugins.default.yaml # Master plugin configuration +└── catalog-entities/ + └── extensions/ + ├── plugins/ + │ ├── all.yaml # Location entity listing all plugins + │ ├── 3scale.yaml # Plugin entity (UI metadata, description, icon) + │ ├── argocd.yaml + │ └── ... + ├── packages/ + │ ├── all.yaml # Location entity listing all packages + │ ├── backstage-community-plugin-3scale-backend.yaml # Package entity (OCI ref, version) + │ └── ... + └── collections/ + ├── all.yaml # Location entity listing all collections + ├── featured.yaml # Curated plugin collections + ├── recommended.yaml + └── ... +``` + +## Build Pipeline Overview + +The catalog index image is assembled from **three distinct input sources** spread across multiple repositories. The actual image build happens in Red Hat's internal CI infrastructure (Konflux/Tekton), but all source content is produced in the public repositories. + +```mermaid +flowchart TB + rhdh["rhdh repo
default.packages.yaml
(enabled/disabled list)"] + overlays_meta["rhdh-plugin-export-overlays
workspaces/*/metadata/*.yaml"] + overlays_cat["rhdh-plugin-export-overlays
catalog-entities/extensions/
plugins, packages, collections"] + extcli["rhdh-plugins repo
extensions-cli: generate command"] + + subgraph ci["Red Hat Internal CI — Konflux/Tekton"] + s1["1. Read default.packages.yaml"] + s2["2. Read metadata/*.yaml"] + s3["3. Read catalog-entities/"] + s4["4. Generate dynamic-plugins.default.yaml"] + s5["5. Assemble final image"] + s6["6. Push to quay.io/rhdh/plugin-catalog-index"] + s1 --> s4 + s2 --> s4 + s3 --> s5 + s4 --> s5 --> s6 + end + + output["OCI Image
quay.io/rhdh/plugin-catalog-index:1.10"] + + rhdh --> s1 + overlays_meta --> s2 + overlays_cat --> s3 + extcli -.->|generates Package entities| overlays_cat + s6 --> output +``` + +## Input Sources in Detail + +### 1. `default.packages.yaml` (rhdh repo) + +**Path:** `rhdh/default.packages.yaml` + +This is the **master manifest** declaring which plugins are part of RHDH and their default enabled/disabled state. It contains two lists: + +```yaml +packages: + enabled: + - package: '@backstage/plugin-techdocs' + - package: '@red-hat-developer-hub/backstage-plugin-extensions' + # ... plugins enabled by default + disabled: + - package: '@backstage-community/plugin-3scale-backend' + - package: '@backstage/plugin-kubernetes' + # ... plugins available but disabled by default +``` + +This file drives which plugins appear in the generated `dynamic-plugins.default.yaml`. The package names here correspond to npm packages that have been exported as dynamic plugins (either embedded in the RHDH image or published as OCI images). + +### 2. Per-Plugin Metadata (rhdh-plugin-export-overlays repo) + +**Path:** `rhdh-plugin-export-overlays/workspaces/*/metadata/*.yaml` + +Each plugin workspace in the overlays repo has a `metadata/` directory containing one YAML file per exported plugin package. These files follow the `extensions.backstage.io/v1alpha1` `Package` kind: + +```yaml +apiVersion: extensions.backstage.io/v1alpha1 +kind: Package +metadata: + name: backstage-community-plugin-3scale-backend + namespace: rhdh + title: "3Scale" +spec: + packageName: "@backstage-community/plugin-3scale-backend" + dynamicArtifact: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-3scale-backend:bs_1.45.3__3.10.0!backstage-community-plugin-3scale-backend + version: 3.10.0 + backstage: + role: backend-plugin + supportedVersions: 1.45.3 + author: Red Hat + support: community + lifecycle: active +``` + +These metadata files contain the OCI image references, version information, and Backstage compatibility data. They are validated by the `validate-metadata` action during the plugin export CI pipeline. + +### 3. Extensions Catalog Entities (rhdh-plugin-export-overlays repo) + +**Path:** `rhdh-plugin-export-overlays/catalog-entities/extensions/` + +This directory contains the Backstage catalog entities that power the RHDH Extensions UI: + +- **`plugins/*.yaml`** — `Plugin` kind entities containing user-facing metadata: title, description, icon, categories, highlights, documentation, and links to constituent packages +- **`packages/*.yaml`** — `Package` kind entities generated from `dynamic-plugins.default.yaml` using the `extensions-cli generate` command; contain OCI artifact references and version info +- **`collections/*.yaml`** — Curated groupings of plugins (e.g., "featured", "recommended", "CI/CD", "OpenShift") + +The `all.yaml` files in each subdirectory are Backstage `Location` entities that enumerate all entities for catalog ingestion. + +### 4. Extensions CLI (rhdh-plugins repo) + +**Path:** `rhdh-plugins/workspaces/extensions/packages/cli/` + +The `@red-hat-developer-hub/extensions-cli` provides a `generate` command that creates `Package` entity YAML files from a `dynamic-plugins.default.yaml`: + +```bash +npx @red-hat-developer-hub/extensions-cli generate \ + --namespace rhdh \ + -p dynamic-plugins.default.yaml \ + -o catalog-entities/extensions/packages +``` + +This command: +1. Reads the `dynamic-plugins.default.yaml` file +2. For each plugin entry, resolves the wrapper directory and reads `package.json` +3. Extracts metadata: npm package name, version, author, links, backstage role, keywords +4. Generates a `Package` entity YAML file per plugin +5. Creates an `all.yaml` Location entity referencing all generated files + +## Individual Plugin Image Build Pipeline + +While the catalog index image is a *manifest*, the individual plugin OCI images it references are built by a separate pipeline in the overlays ecosystem: + +```mermaid +flowchart TB + subgraph inputs["Source Repos"] + overlays["rhdh-plugin-export-overlays
• source.json — upstream repo+commit
• plugins-list.yaml — plugins to export
• patches/*.patch — workspace patches
• plugins/name/overlay/ — file overlays"] + utils["rhdh-plugin-export-utils
Reusable Actions: override-sources,
export-dynamic, validate-metadata
Reusable Workflows: export-dynamic.yaml,
export-workspaces-as-dynamic.yaml"] + end + + subgraph pipeline["GitHub Actions CI Pipeline — per workspace"] + s1["1. Checkout upstream at pinned commit"] + s2["2. Apply patches"] + s3["3. Apply overlays"] + s4["4. yarn install + tsc"] + s5["5. rhdh-cli export-dynamic"] + s6["6. podman build + push"] + s7["7. Validate metadata"] + s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7 + end + + output["Published OCI Images at ghcr.io
e.g. backstage-community-plugin-3scale-backend:bs_1.45.3__3.10.0"] + + overlays --> pipeline + utils --> pipeline + pipeline --> output +``` + +### Per-Workspace Build Steps + +For each workspace (e.g., `workspaces/3scale/`): + +1. **Source Resolution**: Read `source.json` to get the upstream repo URL and pinned commit SHA +2. **Checkout**: Clone the upstream plugin repo at the pinned commit +3. **Override Sources**: Apply workspace-level `.patch` files (sorted alphabetically, auto-detects `-p0`/`-p1`), then copy per-plugin overlay files into the source tree +4. **Build**: `yarn install --immutable` + `yarn tsc` to ensure the workspace compiles +5. **Export**: For each plugin listed in `plugins-list.yaml`, run `rhdh-cli export-dynamic` which produces: + - Backend plugins: `dist-dynamic/` directory with production dependencies + - Frontend plugins: `dist-scalprum/` directory with webpack module federation assets +6. **Package & Push**: Build a `FROM scratch` OCI image containing the plugin files and push to `ghcr.io` +7. **Validate Metadata**: Run `validate-metadata` to ensure `metadata/*.yaml` files are consistent with the published images + +### Image Tag Convention + +Plugin OCI images use the tag format: `bs___` + +Example: `backstage-community-plugin-3scale-backend:bs_1.45.3__3.10.0` + +This encodes both the Backstage version the plugin was built against and the plugin's own version. + +## Catalog Index Assembly (Internal CI) + +The final catalog index image is built in Red Hat's internal CI infrastructure. While the exact build scripts are not in the public repositories, the process conceptually works as follows: + +```mermaid +flowchart TB + i1["default.packages.yaml — rhdh repo
Plugin list, enabled/disabled"] + i2["workspaces/*/metadata/*.yaml — overlays repo
OCI refs, versions, pluginConfig"] + i3["catalog-entities/extensions/ — overlays repo
Plugin/Package/Collection entities"] + + subgraph assembly["Catalog Index Assembly — Konflux/Tekton"] + a1["1. Merge packages + metadata
→ dynamic-plugins.default.yaml"] + a2["2. Collect catalog-entities/extensions/"] + a3["3. Build FROM scratch OCI image"] + a4["4. Push to registry"] + a1 --> a3 + a2 --> a3 + a3 --> a4 + end + + output["quay.io/rhdh/plugin-catalog-index:1.10
• dynamic-plugins.default.yaml
• catalog-entities/extensions/"] + + i1 --> a1 + i2 --> a1 + i3 --> a2 + a4 --> output +``` + +## Sync Back to RHDH Repo + +A daily GitHub Actions workflow keeps the RHDH repo's `dynamic-plugins.default.yaml` in sync with the latest catalog index image: + +```mermaid +flowchart TB + trigger["Trigger: daily at 02:48 UTC,
push to main/release-**, or manual"] + s1["1. Read RHDH version from package.json"] + s2["2. Download catalog index image via skopeo"] + s3["3. Extract all layers"] + s4["4. Replace local dynamic-plugins.default.yaml
with extracted version"] + s5["5. Create PR via peter-evans/create-pull-request"] + trigger --> s1 --> s2 --> s3 --> s4 --> s5 +``` + +This ensures that `dynamic-plugins.default.yaml` in the RHDH repo stays consistent with the published catalog index image, even though the authoritative source is the image itself. + +## Consumption Flow at Runtime + +Once the catalog index image is published, here is how it flows through to a running RHDH instance: + +```mermaid +flowchart TB + config["Deployment Config
Operator CR / Helm values / Compose

Sets CATALOG_INDEX_IMAGE=
quay.io/rhdh/plugin-catalog-index:1.10"] + + subgraph init["Init Container: install-dynamic-plugins"] + e1["1. extract_catalog_index
skopeo copy → extract layers →
find dynamic-plugins.default.yaml →
copy catalog-entities/extensions/"] + e2["2. Merge configuration
Read user dynamic-plugins.yaml,
replace includes with catalog index version,
apply user plugin overrides"] + e3["3. Install plugins
OCI: skopeo + extract
NPM: npm pack + extract
Local: copy from embedded dist"] + e4["4. Write app-config.dynamic-plugins.yaml
merged pluginConfig from all plugins"] + e1 --> e2 --> e3 --> e4 + end + + vol1["Shared Volume: dynamic-plugins-root
plugin files + app-config"] + vol2["Shared Volume: /extensions/
catalog entities"] + + subgraph main["Main Container: backstage-backend"] + m1["Starts with --config
dynamic-plugins-root/app-config.dynamic-plugins.yaml"] + m2["Scalprum loads frontend plugins
from dynamic-plugins-root/"] + m3["Extensions backend reads catalog entities
from /extensions/catalog-entities/
→ Serves Extensions UI"] + end + + config --> init + e4 --> vol1 + e1 --> vol2 + vol1 --> main + vol2 --> main +``` + +## Deployment-Specific Configuration + +### Operator + +In `rhdh-operator/pkg/model/deployment.go`: + +```go +const CatalogIndexImageEnvVar = "RELATED_IMAGE_catalog_index" + +// Set CATALOG_INDEX_IMAGE from operator env var BEFORE extraEnvs +if catalogIndexImage := os.Getenv(CatalogIndexImageEnvVar); catalogIndexImage != "" { + if i, _ := DynamicPluginsInitContainer(b.podSpec().InitContainers); i >= 0 { + b.setOrAppendEnvVar(&b.podSpec().InitContainers[i], "CATALOG_INDEX_IMAGE", catalogIndexImage) + } +} +``` + +The operator's `RELATED_IMAGE_catalog_index` env var is injected into the `install-dynamic-plugins` init container as `CATALOG_INDEX_IMAGE`. This is set **before** user-specified `extraEnvs`, allowing users to override it via the Backstage CR. + +Default in `config/profile/rhdh/default-config/deployment.yaml`: +```yaml +- name: CATALOG_INDEX_IMAGE + value: "quay.io/rhdh/plugin-catalog-index:1.9" +``` + +### Helm Chart + +In `rhdh-chart/charts/backstage/values.yaml`: +```yaml +global: + catalogIndex: + image: + registry: quay.io + repository: rhdh/plugin-catalog-index + tag: "1.10" +``` + +### Docker Compose (rhdh-local) + +Set via `.env` file or `environment:` block in `compose.yaml`. + +## Version Alignment + +The catalog index image version is aligned with the RHDH release version: + +| RHDH Version | Catalog Index Image Tag | Backstage Version | +|---|---|---| +| 1.10 | `plugin-catalog-index:1.10` | 1.45.x | +| 1.9 | `plugin-catalog-index:1.9` | 1.42.x | + +The `versions.json` in the overlays repo pins the exact Backstage version and CLI version used to build the plugins: + +```json +{ + "backstage": "1.45.3", + "node": "22.19.0", + "cli": "1.9.1", + "cliPackage": "@red-hat-developer-hub/cli" +} +``` + +## Repository Roles Summary + +| Repository | Role in Catalog Index Pipeline | +|---|---| +| **rhdh** | Defines `default.packages.yaml` (which plugins, enabled/disabled). Contains the `install-dynamic-plugins.py` consumer script. Has sync workflow to pull latest `dynamic-plugins.default.yaml` from the published image. | +| **rhdh-plugin-export-overlays** | Contains per-plugin metadata (`workspaces/*/metadata/*.yaml`), catalog entities (`catalog-entities/extensions/`), and CI workflows that build individual plugin OCI images. Source-of-truth for plugin descriptions, icons, and UI metadata. | +| **rhdh-plugin-export-utils** | Provides reusable GitHub Actions and workflows used by the overlays repo: `override-sources`, `export-dynamic`, `validate-metadata`, and orchestration workflows. | +| **rhdh-cli** | Provides `export-dynamic` command that packages plugins into `dist-dynamic/` or `dist-scalprum/` directories, and `package-dynamic-plugins` command that builds `FROM scratch` OCI images. | +| **rhdh-plugins** | Houses the `extensions-cli` (`generate` command) that produces `Package` entity YAML files from `dynamic-plugins.default.yaml`. Also contains the extensions frontend/backend plugins that consume catalog entities at runtime. | +| **rhdh-operator** | Injects `CATALOG_INDEX_IMAGE` env var into the init container from `RELATED_IMAGE_catalog_index`. | +| **rhdh-chart** | Configures `CATALOG_INDEX_IMAGE` via `global.catalogIndex.image` Helm values. | + +## Key Files Reference + +| File | Repository | Purpose | +|------|------------|---------| +| `default.packages.yaml` | rhdh | Master plugin enable/disable manifest | +| `dynamic-plugins.default.yaml` | rhdh (generated) | Synced copy from catalog index image | +| `.github/workflows/update-dynamic-plugins-default.yaml` | rhdh | Daily sync workflow | +| `scripts/install-dynamic-plugins/install-dynamic-plugins.py` | rhdh | Runtime catalog index extraction logic | +| `workspaces/*/metadata/*.yaml` | rhdh-plugin-export-overlays | Per-plugin Package metadata | +| `workspaces/*/source.json` | rhdh-plugin-export-overlays | Upstream repo + commit pin | +| `workspaces/*/plugins-list.yaml` | rhdh-plugin-export-overlays | Plugins to export per workspace | +| `catalog-entities/extensions/plugins/*.yaml` | rhdh-plugin-export-overlays | Plugin entities for Extensions UI | +| `catalog-entities/extensions/packages/*.yaml` | rhdh-plugin-export-overlays | Package entities (generated) | +| `catalog-entities/extensions/collections/*.yaml` | rhdh-plugin-export-overlays | Plugin collection groupings | +| `versions.json` | rhdh-plugin-export-overlays | Backstage/Node/CLI version pins | +| `.github/workflows/export-dynamic.yaml` | rhdh-plugin-export-utils | Reusable per-workspace export workflow | +| `workspaces/extensions/packages/cli/` | rhdh-plugins | Extensions CLI (`generate` command) | +| `pkg/model/deployment.go` | rhdh-operator | `CATALOG_INDEX_IMAGE` injection logic | +| `charts/backstage/values.yaml` | rhdh-chart | `global.catalogIndex.image` config | diff --git a/docs/architecture/dynamic-plugin-loading.md b/docs/architecture/dynamic-plugin-loading.md new file mode 100644 index 0000000000..c6a2844478 --- /dev/null +++ b/docs/architecture/dynamic-plugin-loading.md @@ -0,0 +1,194 @@ +# Dynamic Plugin Loading Architecture + +This document explains how dynamic plugins are loaded into RHDH at runtime, how the catalog index image is used, and how the `install-dynamic-plugins.py` script orchestrates the process. + +## Architecture Overview + +Dynamic plugin loading follows an **init container pattern**: a dedicated `install-dynamic-plugins` init container runs before the main RHDH backend starts, downloading, extracting, and configuring plugins into a shared volume. + +```mermaid +flowchart TB + subgraph pod["Pod"] + init["Init Container: install-dynamic-plugins

Runs: install-dynamic-plugins.sh → .py"] + main["Main Container: backstage-backend

Reads: dynamic-plugins-root/
app-config.dynamic-plugins.yaml"] + init -->|runs before| main + vol1["Shared Volume: dynamic-plugins-root
ephemeral PVC / emptyDir

• plugin-dir/ — extracted plugin files
• app-config.dynamic-plugins.yaml — merged config
• install-dynamic-plugins.lock"] + vol2["Shared Volume: extensions-catalog — emptyDir

• catalog-entities/ — from catalog index image"] + init -.->|writes| vol1 + main -.->|reads| vol1 + main -.->|reads| vol2 + end +``` + +## The `install-dynamic-plugins.py` Script + +The script at `scripts/install-dynamic-plugins/install-dynamic-plugins.py` is the core engine. It is copied into the RHDH container image during the build (`build/containerfiles/Containerfile`) and invoked by the shell wrapper `install-dynamic-plugins.sh`. + +### Step 1: Locking + +Creates a file lock (`install-dynamic-plugins.lock`) in the `dynamic-plugins-root` directory to prevent concurrent installations. This is important when multiple pods share a persistent volume. Uses `atexit` to clean up the lock on exit. If a lock already exists, it polls every second until released. + +If the init container is killed with `SIGKILL` (e.g., OOM, pod eviction), the lock file will not be cleaned up and must be removed manually. See [installing-plugins.md](../dynamic-plugins/installing-plugins.md#storage-of-dynamic-plugins) for details. + +### Step 2: Catalog Index Image Extraction + +If the `CATALOG_INDEX_IMAGE` environment variable is set, the script calls `extract_catalog_index()`, which: + +1. Uses **skopeo** to download the OCI image to a temporary directory +2. Reads the `manifest.json` and extracts all layers +3. Looks for a `dynamic-plugins.default.yaml` file inside the extracted content +4. Extracts catalog entities from `catalog-entities/extensions/` (or `catalog-entities/marketplace/` for backward compatibility) into the directory specified by `CATALOG_ENTITIES_EXTRACT_DIR` (defaults to `/tmp/extensions/catalog-entities`) +5. Returns the path to the extracted `dynamic-plugins.default.yaml` + +### Step 3: Configuration Merging + +Reads `dynamic-plugins.yaml` which has two sections: + +- **`includes`**: list of YAML files to load as base defaults (typically `dynamic-plugins.default.yaml`) +- **`plugins`**: user-defined plugin overrides + +**Catalog index integration**: If the catalog index was extracted, the script **replaces** `dynamic-plugins.default.yaml` in the `includes` list with the file extracted from the catalog index image. This is how the catalog index overrides the embedded defaults. + +Plugins are merged in two passes: + +- **Level 0**: All plugins from `includes` files +- **Level 1**: All plugins from the main `plugins` list (overrides level 0) + +Duplicate detection prevents the same plugin from being defined twice at the same level. + +Two merger classes handle plugin key generation: + +- **`NPMPackageMerger`**: Strips version info from package names to create stable keys (e.g., `@scope/pkg@1.0` becomes `@scope/pkg`). Also handles aliases, git URLs, and GitHub shorthand. +- **`OciPackageMerger`**: Strips the tag/digest to create keys like `oci://registry/image:!plugin-path`. Supports `{{inherit}}` to inherit version from included configs, and auto-detection of plugin paths from the `io.backstage.dynamic-packages` OCI annotation. + +### Step 4: Plugin Installation + +For each enabled plugin, the script creates a SHA-256 hash of the plugin configuration (excluding `pluginConfig` and `version`) for change detection. Then it iterates through all plugins and installs them. + +#### Package Types and Installers + +| Type | Prefix/Format | Installer Class | Tool Used | +|------|---------------|-----------------|-----------| +| OCI images | `oci://registry/image:tag!path` | `OciPluginInstaller` | `skopeo copy` then extract layer tarball | +| NPM packages | `@scope/pkg@version` | `NpmPluginInstaller` | `npm pack` then extract `.tgz` | +| Local packages | `./path/to/plugin` | `NpmPluginInstaller` | `npm pack` from local path | + +#### Pull Policies + +- **`IfNotPresent`** (default): Skip download if hash file matches the current configuration +- **`Always`** (default for OCI packages with `:latest!`): Check remote digest against the stored `dynamic-plugin-image.hash` file; only re-download if the digest has changed + +#### OCI Download Flow + +The `OciDownloader` class handles OCI image downloads: + +1. `skopeo copy docker://registry/image dir:/tmp/...` — downloads image layers to a local directory +2. Reads `manifest.json` to get the first layer's digest and locate the tarball +3. Extracts only the plugin subdirectory from the tarball into `dynamic-plugins-root/` +4. Saves the image digest to `dynamic-plugin-image.hash` for future comparison +5. Caches downloaded tarballs so that multiple plugins from the same image avoid redundant downloads + +#### Registry Fallback + +For images from `registry.access.redhat.com/rhdh/`, if the image doesn't exist there, the script falls back to `quay.io/rhdh/`. This is checked via `skopeo inspect`. + +### Step 5: Config Output + +Merges all `pluginConfig` fragments from enabled plugins into a single `app-config.dynamic-plugins.yaml` file in the `dynamic-plugins-root` directory. The backstage-backend container is started with `--config dynamic-plugins-root/app-config.dynamic-plugins.yaml` to load this merged config. + +If two plugins define conflicting values for the same config key, an `InstallException` is raised. + +### Step 6: Cleanup + +Removes any plugin directories that were previously installed but are no longer in the current configuration. Detection uses hash file tracking: plugins whose hashes are still in `plugin_path_by_hash` after installation have been removed from the config. + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `CATALOG_INDEX_IMAGE` | OCI image reference for the plugin catalog index | Not set | +| `CATALOG_ENTITIES_EXTRACT_DIR` | Directory for extracting catalog entities | `/tmp/extensions` | +| `MAX_ENTRY_SIZE` | Maximum size of a file in an archive (zip bomb protection) | `20000000` (20MB) | +| `SKIP_INTEGRITY_CHECK` | Set to `"true"` to skip integrity check of remote NPM packages | Not set | +| `REGISTRY_AUTH_FILE` | Path to container registry auth config for private OCI registries | Not set | +| `NPM_CONFIG_USERCONFIG` | Path to `.npmrc` for custom NPM registry configuration | Not set | + +## Catalog Index Image + +### What It Contains + +The catalog index image (e.g., `quay.io/rhdh/plugin-catalog-index:1.10`) is an OCI `FROM scratch` image containing: + +``` +/ (image root) +├── dynamic-plugins.default.yaml # Default plugin configurations +└── catalog-entities/ + └── extensions/ # (or marketplace/ for backward compat) + ├── plugin-a.yaml # Backstage catalog entities + ├── plugin-b.yaml # for the Extensions UI + └── ... +``` + +### Purpose + +The catalog index image **decouples the default plugin list from the RHDH container image**. This allows updating which plugins are available (and their default configs) without rebuilding the RHDH image itself. + +The `dynamic-plugins.default.yaml` inside the catalog index replaces the one embedded in the RHDH container image when the catalog index is configured. + +The catalog entities extracted to `/extensions/catalog-entities/` are consumed by the **extensions backend plugin**, which serves them in the RHDH Extensions UI (a marketplace-like view for browsing available plugins). + +### How It Is Configured Across Deployment Methods + +| Deployment Method | How `CATALOG_INDEX_IMAGE` Is Set | +|---|---| +| **Operator** (`rhdh-operator`) | `RELATED_IMAGE_catalog_index` env var on the operator pod is injected as `CATALOG_INDEX_IMAGE` on the init container (`pkg/model/deployment.go`). Default in `config/profile/rhdh/default-config/deployment.yaml`: `quay.io/rhdh/plugin-catalog-index:1.9` | +| **Helm chart** (`rhdh-chart`) | `global.catalogIndex.image.{registry,repository,tag}` in `values.yaml` is rendered into the init container's env vars | +| **Docker Compose** (`rhdh-local`) | Set via `.env` file or `environment:` block in `compose.yaml` | + +### Operator-Specific Behavior + +In the operator (`rhdh-operator/pkg/model/deployment.go:115-120`), the `RELATED_IMAGE_catalog_index` env var is read at reconciliation time and injected into the `install-dynamic-plugins` init container **before** user-specified `extraEnvs` are applied. This allows users to override the catalog index image via the Backstage CR's `extraEnvs`. + +## Cross-Repository Relationships + +```mermaid +flowchart LR + subgraph rhdh["rhdh — main repo"] + r1["install-dynamic-plugins.py — core script
install-dynamic-plugins.sh — shell wrapper"] + end + subgraph operator["rhdh-operator"] + o1["deployment.go — Sets CATALOG_INDEX_IMAGE
dynamic-plugins.go — Mounts ConfigMap
deployment.yaml — Default init container spec"] + end + subgraph chart["rhdh-chart"] + c1["values.yaml — global.catalogIndex.image"] + end + subgraph local["rhdh-local"] + l1["compose.yaml — Two-container arch
prepare-and-install-dynamic-plugins.sh"] + end + subgraph cli["rhdh-cli"] + cl1["export-dynamic-plugin/ — OCI annotations
package-dynamic-plugins/ — FROM scratch images"] + end + subgraph overlays["rhdh-plugin-export-overlays"] + ov1["plugins-list.yaml → ghcr.io via oci://"] + end + operator -->|configures| rhdh + chart -->|configures| rhdh + local -->|runs| rhdh + cli -->|packages| overlays + overlays -->|consumed by| rhdh +``` + +## Key Files Reference + +| File | Repository | Purpose | +|------|------------|---------| +| `scripts/install-dynamic-plugins/install-dynamic-plugins.py` | rhdh | Core plugin installation script | +| `scripts/install-dynamic-plugins/install-dynamic-plugins.sh` | rhdh | Shell wrapper for the Python script | +| `build/containerfiles/Containerfile` | rhdh | Copies the scripts into the RHDH container image | +| `dynamic-plugins.default.yaml` | rhdh | Embedded default plugin configurations | +| `pkg/model/dynamic-plugins.go` | rhdh-operator | Operator logic for init container and ConfigMap mounting | +| `pkg/model/deployment.go` | rhdh-operator | Operator logic for `CATALOG_INDEX_IMAGE` injection | +| `config/profile/rhdh/default-config/deployment.yaml` | rhdh-operator | Default deployment manifest with init container spec | +| `charts/backstage/values.yaml` | rhdh-chart | Helm values including `global.catalogIndex.image` | +| `compose.yaml` | rhdh-local | Docker Compose two-container setup | +| `prepare-and-install-dynamic-plugins.sh` | rhdh-local | Entrypoint wrapper for the init container in Compose |