Skip to content

plugin installer - #3289

Open
gazarenkov wants to merge 18 commits into
redhat-developer:mainfrom
gazarenkov:install-plugins-container
Open

plugin installer#3289
gazarenkov wants to merge 18 commits into
redhat-developer:mainfrom
gazarenkov:install-plugins-container

Conversation

@gazarenkov

Copy link
Copy Markdown
Member

Description

Added plugin-installer/ - a container image for downloading dynamic plugins with parallel execution support.

Image: quay.io/rhdh-community/plugin-installer:next

Script features:

  • Downloads from OCI registries, HTTP/HTTPS, and NPM
  • Parallel downloads (default: 4 concurrent)
  • Skopeo for OCI (FIPS compliant), oras as fallback
  • NPM .npmrc parsing for private registries
  • SHA integrity verification (sha256/sha384/sha512)
  • Lock file for concurrent safety
  • SIGTERM forwarding to child processes

Makefile targets: install-dp-build, install-dp-push

CI: Tests on PR, builds/pushes on merge to main

Which issue(s) does this PR fix or relate to

https://redhat.atlassian.net/browse/RHIDP-15359

PR acceptance criteria

  • Tests
  • Documentation

How to test changes / Special notes to the reviewer

Run it with 'make install run' (cluster deployment is not working yet)
Apply your CR

Building Container Images for Testing

Need to test container images from this PR?

For Maintainers: To trigger a test image build, review the code and comment /build-images.
This always builds the HEAD of the PR branch.

For Contributors: Ask a maintainer to run /build-images.

Images will be built and pushed to Quay with links posted in comments.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ShellCheck found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Comment thread plugin-installer/Dockerfile.oras Fixed
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.71429% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.55%. Comparing base (095451d) to head (a1dab01).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
pkg/model/deployment.go 84.61% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3289      +/-   ##
==========================================
+ Coverage   63.49%   63.55%   +0.05%     
==========================================
  Files          38       38              
  Lines        2356     2365       +9     
==========================================
+ Hits         1496     1503       +7     
- Misses        711      712       +1     
- Partials      149      150       +1     
Flag Coverage Δ
nightly ?
unittests 63.55% <85.71%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/model/appconfig.go 88.46% <ø> (ø)
pkg/model/dynamic-plugins.go 79.32% <ø> (ø)
pkg/model/model_tests.go 89.39% <100.00%> (+0.16%) ⬆️
pkg/model/deployment.go 81.34% <84.61%> (-0.20%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread plugin-installer/install_plugins_test.sh Fixed
Comment thread plugin-installer/install_plugins_test.sh Fixed
@sonarqubecloud

Copy link
Copy Markdown

@gazarenkov
gazarenkov marked this pull request as ready for review July 31, 2026 05:26
@gazarenkov
gazarenkov requested a review from a team as a code owner July 31, 2026 05:26
@rhdh-qodo-merge

Copy link
Copy Markdown

PR Summary by Qodo

Add plugin-installer container image and wire it into operator DP processing

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds a new plugin-installer image for downloading dynamic plugins from OCI/HTTP/NPM.
• Supports parallel downloads, integrity checks, lock-file safety, and SIGTERM forwarding.
• Wires INSTALL_DP_IMAGE into operator DP processing to override the init container image.
• Adds CI workflow and Makefile targets to test/build/push the image.
• Updates manifests/tests for renamed dynamic plugin app-config filename and new env vars.
Diagram

graph TD
  A["Backstage CR"] --> B["Operator model"] --> C["DP init container"] --> E["install_plugins.sh"] --> I["Plugins volume"]
  B --> D[("INSTALL_DP_IMAGE")]
  E --> F{{"OCI registry"}}
  E --> G{{"HTTP(S)"}}
  E --> H{{"NPM registry"}}
  subgraph Legend
    direction LR
    _svc(["Workload/component"]) ~~~ _cfg[("Config/env")] ~~~ _ext{{"External source"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reuse Backstage's existing Node-based installer tooling
  • ➕ Leverages upstream-maintained behavior and edge-case handling
  • ➕ Avoids custom parsing/transport logic (especially around NPM metadata)
  • ➖ Requires Node.js runtime in the init container, increasing size/attack surface
  • ➖ Harder to keep minimal/FIPS-aligned compared to skopeo-based UBI micro
2. Use jq (or similar) for NPM metadata parsing
  • ➕ More robust JSON parsing than grep/sed heuristics
  • ➕ Less likely to break if registry responses change formatting
  • ➖ Adds another runtime dependency to a minimal image
  • ➖ Current implementation already includes targeted tests for supported cases

Recommendation: The PR’s approach (a minimal UBI micro init-container with skopeo-first OCI support and a self-contained bash script) is a reasonable fit for a constrained, security-sensitive init container. The main risk is long-term robustness of the hand-rolled NPM registry parsing; if this grows in scope, consider moving NPM handling to a tool/library with proper JSON parsing or a small compiled helper.

Files changed (21) +1693 / -23

Enhancement (4) +689 / -6
create-local-dynamic-plugins.shRecord catalog-index provenance in generated ConfigMap annotations +4/-1

Record catalog-index provenance in generated ConfigMap annotations

• Switches heredoc quoting to allow IMAGE interpolation and adds annotations capturing the generator script and source IMAGE.

hack/create-local-dynamic-plugins.sh

deployment.goRequire and apply INSTALL_DP_IMAGE for DP init container in operator mode +20/-5

Require and apply INSTALL_DP_IMAGE for DP init container in operator mode

• Finds the install-dynamic-plugins init container and, when OPERATOR_DP_PROCESSING is enabled, requires INSTALL_DP_IMAGE and swaps the init container image; also refactors image override logic to pass the init-container index through setImage().

pkg/model/deployment.go

dynamic-plugins.goAdd INSTALL_DP_IMAGE env var constant +1/-0

Add INSTALL_DP_IMAGE env var constant

• Introduces InstallDpImageEnvVar constant used by the deployment model when operator DP processing is enabled.

pkg/model/dynamic-plugins.go

install_plugins.shImplement parallel plugin downloader with OCI/HTTP/NPM/file support +664/-0

Implement parallel plugin downloader with OCI/HTTP/NPM/file support

• Introduces the installer script with skopeo/oras OCI extraction, HTTP tarball downloads, NPM registry fetch+integrity verification (including .npmrc parsing), optional catalog-entities extraction, lock-file concurrency protection, and SIGTERM forwarding.

plugin-installer/install_plugins.sh

Refactor (1) +1 / -1
appconfig.goRename plugins app-config filename constant +1/-1

Rename plugins app-config filename constant

• Changes PluginsAppConfigFile from app-config.plugins.yaml to app-config.dynamic-plugins.yaml to match new naming.

pkg/model/appconfig.go

Tests (4) +456 / -9
rhdh-config_test.goUpdate expectations for Operator DP processing args and config mount +8/-8

Update expectations for Operator DP processing args and config mount

• Adjusts integration tests to reflect the new app-config filename and the changed container args/mount path when OPERATOR_DP_PROCESSING is enabled.

integration_tests/rhdh-config_test.go

deployment_test.goAdjust deployment image override test for operator DP processing +7/-1

Adjust deployment image override test for operator DP processing

• Updates assertions to expect the init container image to come from INSTALL_DP_IMAGE when OPERATOR_DP_PROCESSING is enabled.

pkg/model/deployment_test.go

model_tests.goSet dummy INSTALL_DP_IMAGE in test harness +2/-0

Set dummy INSTALL_DP_IMAGE in test harness

• Ensures tests that enable operator DP processing have a default INSTALL_DP_IMAGE set to avoid model validation errors.

pkg/model/model_tests.go

install_plugins_test.shAdd bash tests for NPM parsing and integrity verification +439/-0

Add bash tests for NPM parsing and integrity verification

• Adds a standalone bash test harness covering .npmrc parsing, URL encoding, integrity verification, and an opt-in integration test that downloads a real NPM package.

plugin-installer/install_plugins_test.sh

Documentation (2) +343 / -0
developer.mdDocument overriding IMAGE for local dynamic-plugins generation +5/-0

Document overriding IMAGE for local dynamic-plugins generation

• Adds an example showing how to run local-dynamic-plugins with a custom catalog-index image via IMAGE=... make local-dynamic-plugins.

docs/developer.md

README.mdDocument plugin-installer usage and supported sources +338/-0

Document plugin-installer usage and supported sources

• Adds comprehensive documentation covering supported URL formats, env vars, integrity verification, locking, signal handling, catalog-index extraction, and troubleshooting.

plugin-installer/README.md

Other (10) +204 / -7
plugin-installer.yamlAdd CI workflow to test and publish plugin-installer image +73/-0

Add CI workflow to test and publish plugin-installer image

• Introduces a dedicated GitHub Actions workflow that runs installer tests on PRs and builds/pushes a multi-platform image on merges to main.

.github/workflows/plugin-installer.yaml

MakefileAdd installer image build targets and pass INSTALL_DP_IMAGE to tests/run +20/-5

Add installer image build targets and pass INSTALL_DP_IMAGE to tests/run

• Adds INSTALL_DP_IMAGE and targets to build/buildx/push the plugin-installer image. Also threads INSTALL_DP_IMAGE through test/integration-test/run and parameterizes local-dynamic-plugins with IMAGE.

Makefile

backstage-operator.clusterserviceversion.yamlRegenerate CSV metadata timestamp (backstage.io bundle) +1/-1

Regenerate CSV metadata timestamp (backstage.io bundle)

• Updates the generated createdAt timestamp as part of bundle regeneration.

bundle/backstage.io/manifests/backstage-operator.clusterserviceversion.yaml

backstage-operator.clusterserviceversion.yamlInject INSTALL_DP_IMAGE env var into operator CSV and regenerate +3/-1

Inject INSTALL_DP_IMAGE env var into operator CSV and regenerate

• Adds INSTALL_DP_IMAGE to the operator container env list and updates generated metadata timestamps.

bundle/rhdh/manifests/backstage-operator.clusterserviceversion.yaml

rhdh-default-config_v1_configmap.yamlAdd INPUT_FILE env var and document INSTALL_DP_IMAGE substitution +3/-0

Add INPUT_FILE env var and document INSTALL_DP_IMAGE substitution

• Updates default config to include INPUT_FILE (/opt/app-root/src/packages.txt) for the init container and clarifies that the image is overridden by INSTALL_DP_IMAGE when OPERATOR_DP_PROCESSING is enabled.

bundle/rhdh/manifests/rhdh-default-config_v1_configmap.yaml

deployment.yamlAdd INPUT_FILE env var to rhdh default deployment +3/-0

Add INPUT_FILE env var to rhdh default deployment

• Mirrors the INPUT_FILE env var addition and the comment about INSTALL_DP_IMAGE-based image replacement.

config/profile/rhdh/default-config/deployment.yaml

deployment-patch.yamlAdd INSTALL_DP_IMAGE to rhdh deployment patch +2/-0

Add INSTALL_DP_IMAGE to rhdh deployment patch

• Adds INSTALL_DP_IMAGE to the operator’s deployment patch so the controller can swap the init container image when DP processing is enabled.

config/profile/rhdh/patches/deployment-patch.yaml

install.yamlPropagate INPUT_FILE and INSTALL_DP_IMAGE into generated install manifest +5/-0

Propagate INPUT_FILE and INSTALL_DP_IMAGE into generated install manifest

• Updates the generated dist manifest to reflect new env vars and updated comments in the init container and operator deployment.

dist/rhdh/install.yaml

Dockerfile.orasAdd oras-based plugin-installer image variant +56/-0

Add oras-based plugin-installer image variant

• Creates an experimental, lighter image that downloads oras in a builder stage and runs the installer script in a UBI micro runtime (amd64/arm64 only).

plugin-installer/Dockerfile.oras

Dockerfile.skopeoAdd skopeo-based plugin-installer image (recommended) +38/-0

Add skopeo-based plugin-installer image (recommended)

• Adds a multi-stage build using dnf --installroot to assemble a minimal UBI micro runtime including skopeo and required tools, then runs install_plugins.sh as non-root.

plugin-installer/Dockerfile.skopeo

@rhdh-qodo-merge

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (2) 🔗 Cross-repo conflicts (4) 📜 Skill insights (0)

Context used
⚠️ Tickets: not configured — ticket URL found in PR but could not be fetched — check ticket provider credentials
✅ Compliance rules (platform): 18 rules
✅ Cross-repo context
  Explored: repo: redhat-developer/rhdh (sha: 5fe91c8b)
  Explored: repo: redhat-developer/rhdh-plugins (sha: d623d369)

Grey Divider


Action required

1. Plugin name parsing broken 🐞 Bug ≡ Correctness
Description
download_plugin() computes plugin_name by stripping everything after the first @, which makes
scoped NPM specs (e.g. @backstage/plugin-catalog@1.10.0) resolve to an empty name and keeps OCI
:tag suffixes in the name. This can cause downloads to write into the output root (empty name) or
make OCI extraction fail because the expected extracted subfolder name won’t match.
Code

plugin-installer/install_plugins.sh[R557-566]

+    # Parse input: "url [integrity]" (space/tab separated)
+    local url integrity
+    url=$(echo "${input_line}" | awk '{print $1}')
+    integrity=$(echo "${input_line}" | awk '{print $2}')
+
+    # Extract plugin name from URL
+    local plugin_name
+    plugin_name=$(echo "${url}" | sed 's|oci://||' | sed 's|https\?://||' | sed 's|file://||' | sed 's|file:||' | sed 's|@sha256:.*||' | sed 's|@.*||' | awk -F'/' '{print $NF}')
+
+    local plugin_dir="${output_dir}/${plugin_name}"
Relevance

●●● Strong

Brittle plugin-name parsing has been fixed before; scoped NPM empty-name is clear correctness bug.

PR-#3215

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The current sed 's|@.*||' step makes scoped NPM inputs (which begin with @) become empty, and
the examples show OCI tag refs that will keep :tag in the name. A past repo bug fix (PR #3215)
documents the same root cause pattern: simplistic tag/digest stripping during name extraction.

plugin-installer/install_plugins.sh[557-566]
plugin-installer/README.md[81-106]
PR-#3215

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`plugin_name` derivation in `download_plugin()` is incorrect for scoped NPM packages and OCI references with tags.

### Issue Context
- README explicitly supports scoped NPM (`@scope/pkg@ver`) and OCI tags (`oci://.../plugin:tag`).
- Current logic removes `@.*` unconditionally, which erases the entire scoped NPM spec (it starts with `@`).

### Fix Focus Areas
- plugin-installer/install_plugins.sh[557-606]
- plugin-installer/README.md[81-106]

### Implementation notes
- Derive `plugin_name` based on URL type:
 - NPM scoped: take last path segment after `/` (e.g. `plugin-catalog`) and strip only the *last* `@version` part.
 - NPM unscoped: strip only the *last* `@version`.
 - OCI: strip digest part after last `@`, then strip `:tag` only from the final path segment (after last `/`).
 - HTTP: use URL basename (optionally strip `.tgz`/`.tar.gz` if that’s the desired directory name).
- Add regression tests for `download_plugin` name derivation (scoped npm, unscoped npm, oci digest, oci tag).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Empty packages file crashes 🐞 Bug ☼ Reliability
Description
With set -euo pipefail, computing total via grep -c and the subsequent grep | grep | xargs
pipeline exits non-zero when the input has zero non-comment lines, causing the init container to
fail instead of doing a no-op. Operator DP processing can legitimately generate an empty
packages.txt when no plugins are enabled.
Code

plugin-installer/install_plugins.sh[R646-655]

+total=$(grep -cv '^#\|^$' "${INPUT_FILE}")
+
+echo "=== Downloading ${total} plugins to ${OUTPUT_DIR} (${PARALLEL_JOBS} parallel) ==="
+echo ""
+
+# Use xargs for parallel execution
+# shellcheck disable=SC2016 # Single quotes intentional - variables expand in inner bash
+grep -v '^#' "${INPUT_FILE}" | grep -v '^$' | \
+    xargs -P "${PARALLEL_JOBS}" -I {} bash -c 'download_plugin "$1" "$2"' _ {} "${OUTPUT_DIR}"
+
Relevance

●●● Strong

They’ve accepted hardening scripts for empty/absent outputs; empty packages.txt should no-op, not
fail.

PR-#1646

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The script’s total=$(grep ...) and the piped grep ... | grep ... | xargs ... will exit non-zero
on empty inputs under set -euo pipefail. The operator-side code can generate an empty
packages.txt by joining an empty packages list when no plugins are enabled.

plugin-installer/install_plugins.sh[646-655]
pkg/model/dynamic-plugins.go[144-163]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`install_plugins.sh` fails for empty/only-comments inputs due to `grep` exit codes under `set -euo pipefail`.

### Issue Context
- `grep -c` returns exit code 1 when it matches zero lines (even though it prints `0`).
- With `pipefail`, the `grep -v ... | grep -v ...` pipeline can fail when there are no lines, preventing `xargs` from running.
- Operator DP processing can produce an empty packages list (no enabled plugins), so the init container must treat this as success.

### Fix Focus Areas
- plugin-installer/install_plugins.sh[646-655]
- pkg/model/dynamic-plugins.go[144-163]

### Implementation notes
- Make `total` robust, e.g.:
 - `total=$(grep -cv '^(#|$)' "$INPUT_FILE" || true)` (or use `awk` counting).
- If `total == 0`, print a message and exit 0.
- For the xargs invocation, avoid failure on empty input:
 - use `xargs -r` (GNU) if available, or
 - pre-filter into an array/file and conditionally run `xargs` only when non-empty.
- Add a test case with an input file containing only comments/blank lines.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Installer image name inconsistent 🐞 Bug ⚙ Maintainability
Description
The repo defaults and operator manifests set INSTALL_DP_IMAGE to quay.io/gazarenk/..., while the
plugin-installer README documents quay.io/rhdh-community/plugin-installer:next as the pre-built
image. This inconsistency risks different environments pulling/building different installer images
than what the docs describe.
Code

bundle/rhdh/manifests/backstage-operator.clusterserviceversion.yaml[R321-322]

+                - name: INSTALL_DP_IMAGE
+                  value: quay.io/gazarenk/install-plugins:skopeo
Relevance

●●● Strong

Team often aligns docs/config defaults; inconsistent image refs likely fixed to avoid confusion.

PR-#2000
PR-#2717

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The README documents quay.io/rhdh-community/plugin-installer:next as the pre-built image, but the
Makefile default and operator CSV set INSTALL_DP_IMAGE to different quay.io/gazarenk/... values,
demonstrating a concrete mismatch in the repo itself.

plugin-installer/README.md[11-16]
Makefile[11-16]
bundle/rhdh/manifests/backstage-operator.clusterserviceversion.yaml[317-323]
config/profile/rhdh/patches/deployment-patch.yaml[28-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`INSTALL_DP_IMAGE` is configured to different image names across README, Makefile defaults, and operator deployment manifests.

### Issue Context
- README advertises a canonical pre-built image.
- The Makefile default and CSV/patches point to different repositories/tags, making it unclear which image is intended for users and releases.

### Fix Focus Areas
- Makefile[13-16]
- bundle/rhdh/manifests/backstage-operator.clusterserviceversion.yaml[317-323]
- config/profile/rhdh/patches/deployment-patch.yaml[28-33]
- plugin-installer/README.md[11-16]

### Implementation notes
- Pick a single canonical image reference (matching the release process) and use it consistently in:
 - README
 - Makefile default (`INSTALL_DP_IMAGE ?= ...`)
 - CSV and deployment patches
- Keep `INSTALL_DP_IMAGE` override support for developers, but don’t ship personal defaults in mainline manifests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
4. OCI spec parsing mismatch 🔗 Cross-repo conflict ≡ Correctness
Description
The new plugin-installer downloads OCI artifacts by passing the full oci://... string (minus the
oci:// prefix) directly to skopeo copy, but rhdh-plugins’ contract allows OCI package specs with
an optional !<plugin-path> suffix and {{inherit}} semantics. This will break operator-driven
installs for OCI plugin references that follow the rhdh-plugins format (common in
dynamic-plugins.yaml and catalog-index defaults).
Code

plugin-installer/install_plugins.sh[R115-141]

+extract_oci_image() {
+    local image="$1"
+    local label="$2"  # For error messages
+
+    # Strip oci:// prefix if present
+    local clean_url="${image#oci://}"
+
+    local tmp_oci
+    tmp_oci=$(mktemp -d)
+
+    # Download using detected OCI tool
+    # Workaround: Force linux/amd64 platform for manifest list images.
+    # The catalog-index is built by Konflux as a manifest list but contains only
+    # platform-independent YAML files. Without --override-arch/--override-os,
+    # skopeo fails on non-Linux platforms (e.g., macOS). See RHDHBUGS-2747.
+    local download_err
+    case "${OCI_TOOL}" in
+        skopeo)
+            if ! download_err=$(skopeo copy --override-arch amd64 --override-os linux "docker://${clean_url}" "dir:${tmp_oci}" 2>&1); then
+                echo "[FAIL] ${label}: skopeo copy failed: ${download_err}" >&2
+                rm -rf "${tmp_oci}"
+                return 1
+            fi
+            ;;
+        oras)
+            if ! download_err=$(oras copy --platform linux/amd64 "${clean_url}" --to-oci-layout "${tmp_oci}:latest" 2>&1); then
+                echo "[FAIL] ${label}: oras copy failed: ${download_err}" >&2
Relevance

●●● Strong

Repo already uses OCI refs with !suffix; installer should parse/strip it to match established
format.

PR-#2231

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In the PR, extract_oci_image() strips only the oci:// prefix and passes the remainder directly
to skopeo copy / oras copy, so any !<plugin-path> suffix will be treated as part of the image
reference and fail. In rhdh-plugins, the supported OCI grammar explicitly includes an optional
!<plugin-path> suffix and additional semantics ({{inherit}}, auto-detection), meaning the PR
implementation is not compatible with the established contract.

plugin-installer/install_plugins.sh[115-145]
plugin-installer/install_plugins.sh[553-608]
External repo: redhat-developer/rhdh-plugins, workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/oci-key.ts [22-33]
External repo: redhat-developer/rhdh-plugins, workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/oci-key.ts [52-99]
External repo: redhat-developer/rhdh, docs/dynamic-plugins/installing-plugins.md [130-138]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`plugin-installer/install_plugins.sh` treats the whole `oci://...` package spec as a Docker image reference (after stripping `oci://`). This is incompatible with the OCI package grammar used by the canonical installer in **rhdh-plugins**, which supports `oci://<image>:<tag or digest>!<plugin-path>` and `{{inherit}}`/auto-path resolution.

## Issue Context
- The rhdh-plugins installer parses OCI specs with an optional `!<plugin-path>` suffix and uses that path to decide what to extract/install.
- The new bash installer should either (a) implement the same parsing/semantics, or (b) the operator should transform inputs into a format that the bash installer actually supports.

## Fix Focus Areas
- plugin-installer/install_plugins.sh[115-205]
- plugin-installer/install_plugins.sh[553-608]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. openssl missing in image 🔗 Cross-repo conflict ☼ Reliability
Description
plugin-installer’s install_plugins.sh performs SHA/SRI integrity verification by shelling out to
the openssl CLI, but the plugin-installer (skopeo) runtime image built from Dockerfile.skopeo
does not install openssl. Because integrity strings are part of the documented dynamic plugin
contract (required for tgz and npm packages), installs that include integrity values can hard-fail
at runtime (with set -euo pipefail) in this repo while working with the canonical rhdh-plugins
installer.
Code

plugin-installer/Dockerfile.skopeo[R6-21]

+RUN mkdir -p /mnt/rootfs && \
+    dnf install --installroot /mnt/rootfs \
+        bash \
+        coreutils-single \
+        tar \
+        gzip \
+        curl \
+        skopeo \
+        sed \
+        gawk \
+        grep \
+        findutils \
+        ca-certificates \
+        --releasever 9 --setopt=install_weak_deps=0 --nodocs -y && \
+    dnf --installroot /mnt/rootfs clean all && \
+    rm -rf /mnt/rootfs/var/cache/* /mnt/rootfs/var/log/* /mnt/rootfs/tmp/*
Relevance

●●● Strong

Script shells out to openssl; team has accepted adding openssl to images for runtime/FIPS needs.

PR-#1142

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cited installer script implements verify_integrity() by directly invoking openssl dgst and
openssl base64 whenever an integrity string is present (including in the NPM download path), so
successful plugin installation depends on the openssl binary existing in the container. However,
the referenced Dockerfile.skopeo installs various tools into the final UBI micro image but does
not include openssl, leaving the runtime image without the executable that the script requires;
since RHDH documentation/contract expects integrity fields for tgz/NPM sources, this missing
dependency becomes a real cross-repo contract and deployment failure when integrity verification
runs.

plugin-installer/install_plugins.sh[284-316]
plugin-installer/Dockerfile.skopeo[6-21]
plugin-installer/install_plugins.sh[284-310]
plugin-installer/install_plugins.sh[441-446]
External repo: redhat-developer/rhdh, docs/dynamic-plugins/installing-plugins.md [8-14]
External repo: redhat-developer/rhdh-plugins, workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/README.md [3-6]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`install_plugins.sh` calls `openssl dgst` and `openssl base64` for SRI integrity verification (via `verify_integrity()`), but `Dockerfile.skopeo` does not install/copy `openssl` into the final runtime image. With `set -euo pipefail`, this becomes a hard runtime failure when an integrity value is provided (notably for NPM, and also HTTP when integrity is supplied), breaking the documented plugin-installer contract.

## Issue Context
- `verify_integrity()` shells out to the `openssl` CLI (`dgst` and `base64`).
- The NPM download path runs integrity verification when an integrity string is present.
- `Dockerfile.skopeo` installs many tools into the UBI micro image but does not include `openssl`, so the script depends on an absent binary at runtime.
- RHDH docs / rhdh-plugins installer contract include SRI integrity (`sha256/sha384/sha512`) for tgz/NPM plugins; missing `openssl` can therefore cause cross-repo contract failures in real deployments.

## Fix Focus Areas
- plugin-installer/Dockerfile.skopeo[6-21]
- plugin-installer/install_plugins.sh[284-317]
- plugin-installer/install_plugins.sh[441-446]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Registry auth path mismatch 🔗 Cross-repo conflict ☼ Reliability
Description
plugin-installer’s image sets the UID 1001 user home directory to /, but the operator mounts
registry auth under /opt/app-root/src/.config/containers (matching the existing RHDH initContainer
convention). Since skopeo/oras typically look for auth at ~/.config/containers/auth.json (or via
REGISTRY_AUTH_FILE), private OCI plugin pulls that work with the rhdh image/installer may fail
with plugin-installer.
Code

plugin-installer/Dockerfile.skopeo[R23-38]

+# Add non-root user
+RUN echo "runner:x:1001:0:runner:/:/sbin/nologin" >> /mnt/rootfs/etc/passwd
+
+# =========================================================================
+# STAGE 2: Runtime (Red Hat UBI Micro)
+# =========================================================================
+FROM registry.access.redhat.com/ubi9/ubi-micro:latest
+
+COPY --from=rpm-builder /mnt/rootfs /
+
+COPY plugin-installer/install_plugins.sh /usr/local/bin/install_plugins.sh
+RUN chmod +x /usr/local/bin/install_plugins.sh
+
+USER 1001
+
+ENTRYPOINT ["/bin/bash", "/usr/local/bin/install_plugins.sh"]
Relevance

●● Moderate

Auth-path mismatch is plausible, but depends on operator mount/env setup; likely discussed before
changing defaults.

PR-#2434

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR’s plugin-installer image creates user 1001 with home directory /, so
~/.config/containers/auth.json resolves under /. The operator template mounts container auth
under /opt/app-root/src/.config/containers, and rhdh documentation describes using
REGISTRY_AUTH_FILE or the ~/.config/containers/auth.json convention—without aligning
HOME/REGISTRY_AUTH_FILE, skopeo/oras in plugin-installer will not find the auth material where the
operator places it.

plugin-installer/Dockerfile.skopeo[23-38]
config/profile/rhdh/default-config/deployment.yaml[37-92]
External repo: redhat-developer/rhdh, docs/dynamic-plugins/installing-plugins.md [140-142]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The operator’s deployment template mounts registry auth to `/opt/app-root/src/.config/containers`, but the new plugin-installer image’s user has home directory `/` and the image does not set `HOME` or `REGISTRY_AUTH_FILE`. This makes it likely that skopeo/oras will not discover the mounted credentials.

## Issue Context
RHDH dynamic plugins frequently pull OCI artifacts from registries that may require auth, and the existing docs/contracts assume auth is discoverable at `~/.config/containers/auth.json` or explicitly via `REGISTRY_AUTH_FILE`.

## Fix Focus Areas
- plugin-installer/Dockerfile.skopeo[23-38]
- config/profile/rhdh/default-config/deployment.yaml[37-92]
- plugin-installer/install_plugins.sh[115-145]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

7. remove_lock rm not guarded 📘 Rule violation ☼ Reliability
Description
The EXIT trap calls remove_lock, which runs rm -f "${LOCK_FILE}" under set -e without guarding
against cleanup failure. If rm fails (e.g., permission/FS error), it can cause an unintended
non-zero exit behavior from the trap.
Code

plugin-installer/install_plugins.sh[R78-87]

+remove_lock() {
+    if [[ -f "${LOCK_FILE}" ]]; then
+        rm -f "${LOCK_FILE}"
+        echo "======= Removed lock file: ${LOCK_FILE}"
+    fi
+}
+
+# Ensure lock is removed on exit (normal, error, or signal)
+trap 'remove_lock' EXIT
+
Relevance

●●● Strong

EXIT-trap cleanup is commonly required to be best-effort under set -e; strong accepted precedent.

PR-#2645
PR-#2870

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 3 requires cleanup commands executed in an EXIT trap to be guarded so they cannot
fail the script under set -e. The new script defines trap 'remove_lock' EXIT and remove_lock
executes rm -f "${LOCK_FILE}" without || true / set +e scoping.

Rule 3: Cleanup commands in EXIT traps must not cause script failure
plugin-installer/install_plugins.sh[78-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`remove_lock` is executed via an `EXIT` trap while `set -euo pipefail` is enabled, but the cleanup `rm` is not neutralized (`|| true`) and `-e` is not disabled inside the trap handler. Cleanup failures should not impact script termination behavior.

## Issue Context
This script enables strict mode and installs an `EXIT` trap (`trap 'remove_lock' EXIT`). Per compliance, cleanup commands executed from `EXIT` traps must not be able to fail the script.

## Fix Focus Areas
- plugin-installer/install_plugins.sh[78-87]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. README uses secret123 token 📘 Rule violation § Compliance
Description
The new README includes NPM_AUTH_TOKEN=secret123, which is not an explicitly non-sensitive dummy
placeholder and could be mistaken for a real credential. Example secret-like values must be clearly
fake (e.g., example-npm-token, changeme-*).
Code

plugin-installer/README.md[R265-269]

+```bash
+NPM_REGISTRY=https://npm.mycompany.com \
+NPM_AUTH_TOKEN=secret123 \
+./install_plugins.sh packages.txt ./plugins
+```
Relevance

●●● Strong

Repo is sensitive about secret handling/examples; replace secret-like placeholder with clearly dummy
value.

PR-#3169
PR-#1567

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 18 requires example secret values to be clearly dummy and non-sensitive. The README
snippet for a private NPM registry sets NPM_AUTH_TOKEN=secret123, which reads like a plausible
real token rather than an explicit placeholder.

Rule 18: Use only clearly non-sensitive dummy values in example secrets and dependent resources
plugin-installer/README.md[265-269]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The README uses a token value (`secret123`) that is not clearly a dummy/non-sensitive placeholder.

## Issue Context
Compliance requires that example secrets/tokens in documentation be obviously fake and not plausibly real credentials.

## Fix Focus Areas
- plugin-installer/README.md[265-269]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Oras Dockerfile build fails 🐞 Bug ≡ Correctness
Description
Dockerfile.oras uses curl to download the oras binary and later copies /usr/bin/curl into the
runtime image, but it never installs curl in the builder stage. Building the oras variant will fail
because curl (and /usr/bin/curl) are missing.
Code

plugin-installer/Dockerfile.oras[R10-45]

+RUN microdnf install -y tar gzip coreutils-single grep findutils bash sed gawk && \
+    microdnf clean all && \
+    rm -rf /var/cache/yum && \
+    mkdir -p /runtime-bin && \
+    cp /bin/sh /runtime-bin/ && \
+    cp /bin/coreutils /runtime-bin/ && \
+    for cmd in echo mkdir cat chmod rm test ls cp mv ln env printf tr head tail basename dirname wc date; do \
+      ln -s coreutils /runtime-bin/$cmd; \
+    done && \
+    mkdir -p /runtime-lib && \
+    ldd /usr/bin/curl | awk '/=>/ {print $3}' | xargs -I{} cp -L {} /runtime-lib/ 2>/dev/null || true && \
+    ldd /usr/bin/grep | awk '/=>/ {print $3}' | xargs -I{} cp -L {} /runtime-lib/ 2>/dev/null || true && \
+    ldd /usr/bin/bash | awk '/=>/ {print $3}' | xargs -I{} cp -L {} /runtime-lib/ 2>/dev/null || true && \
+    ldd /usr/bin/sed | awk '/=>/ {print $3}' | xargs -I{} cp -L {} /runtime-lib/ 2>/dev/null || true && \
+    ldd /usr/bin/gawk | awk '/=>/ {print $3}' | xargs -I{} cp -L {} /runtime-lib/ 2>/dev/null || true && \
+    ldd /usr/bin/tar | awk '/=>/ {print $3}' | xargs -I{} cp -L {} /runtime-lib/ 2>/dev/null || true
+
+# Download oras
+RUN ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") && \
+    curl -sSL --proto '=https' "https://github.com/oras-project/oras/releases/download/v1.2.0/oras_1.2.0_linux_${ARCH}.tar.gz" | \
+    tar xzf - -C /usr/local/bin oras
+
+# =========================================================================
+# STAGE 2: Runtime (Red Hat UBI Micro)
+# =========================================================================
+FROM registry.access.redhat.com/ubi9/ubi-micro:latest
+
+COPY --from=builder /runtime-bin/ /bin/
+COPY --from=builder /usr/bin/curl /usr/bin/curl
+COPY --from=builder /usr/bin/grep /usr/bin/grep
+COPY --from=builder /usr/bin/xargs /usr/bin/xargs
+COPY --from=builder /usr/bin/bash /usr/bin/bash
+COPY --from=builder /usr/bin/sed /usr/bin/sed
+COPY --from=builder /usr/bin/gawk /usr/bin/awk
+COPY --from=builder /usr/bin/tar /usr/bin/tar
+COPY --from=builder /usr/bin/gzip /usr/bin/gzip
Relevance

●●● Strong

Deterministic build-break: missing curl in builder while used/copied; likely promptly corrected.

PR-#1222
PR-#1142

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The builder stage installs several packages but not curl, yet later RUN steps use curl and the
runtime stage copies /usr/bin/curl from the builder, which will not exist.

plugin-installer/Dockerfile.oras[10-30]
plugin-installer/Dockerfile.oras[37-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The oras Dockerfile references `curl` but does not install it, making the image build fail.

### Issue Context
- `curl` is used to download oras and is also copied into the runtime stage.

### Fix Focus Areas
- plugin-installer/Dockerfile.oras[10-45]

### Implementation notes
- Add `curl` to the `microdnf install` package list.
- Keep/adjust the runtime-lib copying step to include curl’s shared libs.
- Consider adding a minimal build test in CI (even if this Dockerfile is “experimental”) so it can’t regress silently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
10. Catalog entities layout mismatch 🔗 Cross-repo conflict ≡ Correctness
Description
plugin-installer only extracts catalog entities from catalog-entities/extensions (or copies the
whole catalog-entities/ tree), but the canonical rhdh-plugins installer flattens either
catalog-entities/extensions or catalog-entities/marketplace into
<CATALOG_ENTITIES_EXTRACT_DIR>/catalog-entities. If the catalog-index image uses the marketplace
layout, plugin-installer will place YAMLs under an extra marketplace/ subdir and the Extensions UI
may not see them.
Code

plugin-installer/install_plugins.sh[R511-545]

+extract_catalog_entities() {
+    local image="$1"
+    local dest_dir="$2"
+
+    if [[ -z "${image}" ]]; then
+        return 0
+    fi
+
+    echo "=== Extracting catalog entities from ${image} ==="
+
+    EXTRACT_DIR=""
+    if ! extract_oci_image "${image}" "catalog-index"; then
+        echo "WARNING: Failed to extract catalog index" >&2
+        return 1
+    fi
+
+    # Look for catalog-entities/extensions
+    local entities_src=""
+    if [[ -d "${EXTRACT_DIR}/catalog-entities/extensions" ]]; then
+        entities_src="${EXTRACT_DIR}/catalog-entities/extensions"
+    elif [[ -d "${EXTRACT_DIR}/catalog-entities" ]]; then
+        entities_src="${EXTRACT_DIR}/catalog-entities"
+    fi
+
+    if [[ -n "${entities_src}" && -d "${entities_src}" ]]; then
+        local entities_dest="${dest_dir}/catalog-entities"
+        mkdir -p "${dest_dir}"
+        rm -rf "${entities_dest}"
+        cp -r "${entities_src}" "${entities_dest}"
+        local count
+        count=$(find "${entities_dest}" -type f \( -name "*.yaml" -o -name "*.yml" \) 2>/dev/null | wc -l | tr -d ' ')
+        echo "Catalog entities extracted to ${entities_dest} (${count} files)"
+    else
+        echo "WARNING: No catalog-entities found in ${image}" >&2
+    fi
Relevance

●● Moderate

Layout expectations depend on real catalog-index contents; may need confirmation before changing
extraction logic.

PR-#2000

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR implementation only checks for catalog-entities/extensions and otherwise copies the entire
catalog-entities directory, which preserves subdirectories like marketplace/. The rhdh-plugins
installer explicitly supports both extensions and marketplace layouts and flattens the chosen
subdir into <entitiesDir>/catalog-entities, so the PR behavior can create a different on-disk
structure than other repos expect.

plugin-installer/install_plugins.sh[511-545]
External repo: redhat-developer/rhdh-plugins, workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/catalog-index.ts [62-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new extractor does not match the catalog entity extraction layout used by rhdh-plugins: it doesn’t check `catalog-entities/marketplace`, and its fallback copies the whole `catalog-entities/` directory rather than flattening marketplace contents into the destination `catalog-entities/` root.

## Issue Context
rhdh-plugins treats `catalog-entities/extensions` and `catalog-entities/marketplace` as alternative layouts and always writes YAMLs to `<entitiesDir>/catalog-entities/`.

## Fix Focus Areas
- plugin-installer/install_plugins.sh[511-545]
- /cross_repos/rhdh-plugins/workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/catalog-index.ts[62-76]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

@rhdh-qodo-merge rhdh-qodo-merge Bot added documentation Improvements or additions to documentation enhancement New feature or request Tests labels Jul 31, 2026
@openshift-ci

openshift-ci Bot commented Aug 1, 2026

Copy link
Copy Markdown

PR needs rebase.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request needs-rebase Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants