Skip to content

Instantly share code, notes, and snippets.

@dmc5179
Created July 31, 2026 17:53
Show Gist options
  • Select an option

  • Save dmc5179/9dc63190dd4ad3c4e99a8bc5e7f370b1 to your computer and use it in GitHub Desktop.

Select an option

Save dmc5179/9dc63190dd4ad3c4e99a8bc5e7f370b1 to your computer and use it in GitHub Desktop.
Non-Authoritative Script to check OLM dependencies within an oc-mirror v2 imageset-config.yaml

OLM Operator Dependency Checker

check-operator-deps.sh validates that an imageset-config.yaml for oc-mirror includes all of the operator dependencies required by the operators it lists. It catches missing dependencies before you mirror, so you don't end up with a disconnected cluster that can't install an operator because a required dependency was left out of the image set.

Problem

When using oc-mirror to mirror a subset of operators from the redhat-operator-index into a disconnected environment, you must explicitly list every operator package you want. OLM operators can declare dependencies on other operators, but oc-mirror does not resolve or warn about those dependencies for you. If you mirror web-terminal without also mirroring devworkspace-operator, the install will fail at runtime.

How it works

  1. Parses the imageset-config.yaml to extract each catalog image reference and the list of operator packages requested under it.
  2. Extracts the file-based catalog (FBC) from the operator-index container image by running podman create + podman cp /configs. Alternatively, you can point it at an already-extracted configs directory with --local-configs.
  3. Scans every requested operator's catalog entries for three types of dependency declarations:
    • olm.package.required — a direct dependency on another operator package by name and version range.
    • olm.gvk.required — a dependency on a Kubernetes API (group/version/kind) that must be provided by some other operator.
    • olm.constraint — a general OLM constraint that can contain arbitrarily nested package references (e.g. all.constraints[].package.packageName).
  4. Builds a GVK provider index across the entire catalog so it can resolve olm.gvk.required dependencies to specific operator package names.
  5. Reports the results: either all dependencies are satisfied ("ALL OK", exit code 0) or it prints a detailed report of what is missing along with copy-paste suggestions (exit code 1).

The script handles both FBC directory layouts found in the Red Hat operator index:

  • Single-file: <operator>/catalog.json (concatenated JSON objects, one per line-delimited blob)
  • Split: <operator>/bundles/*.json + channels/ + package.json (one JSON file per bundle version)

Prerequisites

Tool Required when Install
jq Always dnf install jq or brew install jq
yq Always pip install yq (python-yq) or brew install yq (mikefarah/yq)
podman Default mode (extracting from index image) dnf install podman

You also need pull access to the operator-index image (e.g. registry.redhat.io/redhat/redhat-operator-index:v4.21). Run podman login registry.redhat.io first if you haven't already.

Usage

# Basic — pull the index image and check dependencies
./check-operator-deps.sh imageset-config.yaml

# Keep the extracted configs directory for re-use
./check-operator-deps.sh imageset-config.yaml --keep-configs

# Use a previously extracted configs directory (no podman needed)
./check-operator-deps.sh imageset-config.yaml --local-configs /path/to/configs

# Show help
./check-operator-deps.sh --help

Options

Flag Description
--keep-configs Do not delete the extracted configs directory on exit. The path is printed at the start of the run. Useful when iterating on the same index version.
--local-configs <path> Skip the podman extraction entirely and use an existing configs directory. <path> should be the directory containing operator subdirectories (e.g. web-terminal/, mtc-operator/).
-h, --help Print usage information and exit.

Extracting configs manually

If you prefer to extract the configs directory yourself (or already have one from a prior run), you can do so with:

podman create --name index registry.redhat.io/redhat/redhat-operator-index:v4.21
podman cp index:/configs ./configs
podman rm index

Then pass it to the script:

./check-operator-deps.sh imageset-config.yaml --local-configs ./configs

Input format

The script expects a standard oc-mirror ImageSetConfiguration:

kind: ImageSetConfiguration
apiVersion: mirror.openshift.io/v1alpha2
mirror:
  operators:
  - catalog: registry.redhat.io/redhat/redhat-operator-index:v4.21
    packages:
    - name: web-terminal
    - name: odf-operator
    - name: mtc-operator
    - name: service-telemetry-operator

Multiple catalog entries under .mirror.operators[] are supported — each is processed independently.

Output

When dependencies are missing (exit code 1)

==========================================
 MISSING OPERATOR PACKAGE DEPENDENCIES
==========================================
  - amq7-interconnect-operator
    needed by: service-telemetry-operator
  - devworkspace-operator
    needed by: web-terminal
  - redhat-oadp-operator
    needed by: mtc-operator
  - smart-gateway-operator
    needed by: service-telemetry-operator

==========================================
 MISSING GVK (API) DEPENDENCIES
==========================================
 These GVKs are required but the providing
 operator is not in your imageset-config.
==========================================
  - controller.devfile.io/v1alpha1/DevWorkspaceRouting
    web-terminal (provided by: devworkspace-operator)
  - workspace.devfile.io/v1alpha1/DevWorkspace
    web-terminal (provided by: devworkspace-operator)

==========================================
 SUGGESTED ADDITIONS to imageset-config
==========================================
 Add these packages under .mirror.operators[].packages:

    - name: amq7-interconnect-operator
    - name: devworkspace-operator
    - name: redhat-oadp-operator
    - name: smart-gateway-operator

The output has up to four sections:

Section Meaning
MISSING OPERATOR PACKAGE DEPENDENCIES An operator in your config declares olm.package.required or olm.constraint on another operator package that is not in your config.
MISSING GVK (API) DEPENDENCIES An operator requires a Kubernetes API (group/version/kind) via olm.gvk.required, and the script found which operator in the index provides it — but that operator is not in your config.
UNRESOLVABLE GVK DEPENDENCIES An operator requires a GVK that no operator in the entire index provides. This usually means the CRD is installed outside of OLM (e.g. by the platform itself or a Helm chart). These are informational and may not require action.
SUGGESTED ADDITIONS A consolidated list of - name: entries you can copy directly into your imageset-config to resolve the missing dependencies.

When all dependencies are satisfied (exit code 0)

ALL OK — all operator dependencies are satisfied.

Exit codes

Code Meaning
0 All operator dependencies are satisfied by other operators in the config.
1 One or more dependencies are missing. See the output report for details.

Dependency types explained

olm.package.required

The most common dependency type. The operator explicitly declares that it needs another operator package to be installed. Found in the bundle's properties array:

{
  "type": "olm.package.required",
  "value": {
    "packageName": "devworkspace-operator",
    "versionRange": ">=0.6.0"
  }
}

olm.gvk.required

The operator requires a specific Kubernetes API (Custom Resource) to exist on the cluster. The script builds an index of which operators provide which GVKs (via olm.gvk properties) across the entire catalog and uses that to resolve GVK requirements back to operator package names.

{
  "type": "olm.gvk.required",
  "value": {
    "group": "workspace.devfile.io",
    "version": "v1alpha1",
    "kind": "DevWorkspace"
  }
}

olm.constraint

A general-purpose constraint mechanism that can contain arbitrarily nested structures. The script recursively walks the constraint tree to extract any packageName references. These often include human-readable failureMessage fields:

{
  "type": "olm.constraint",
  "value": {
    "all": {
      "constraints": [
        {
          "failureMessage": "Package amq7-interconnect-operator is needed for data transport with STF",
          "package": {
            "packageName": "amq7-interconnect-operator",
            "versionRange": ">=1.10.0"
          }
        }
      ]
    },
    "failureMessage": "Require data transport for Service Telemetry Framework"
  }
}

Tips

  • Run early: check your imageset-config before starting the mirror. A missing dependency discovered after hours of mirroring is costly.
  • Iterate with --local-configs: extract the configs once with --keep-configs, then re-run with --local-configs as you add operators to your imageset-config. This avoids re-pulling the index image each time.
  • Transitive dependencies: the script checks the operators you listed, not their dependencies' dependencies. If the script suggests adding devworkspace-operator and that operator also has dependencies, run the script again after adding it to catch the next level.
  • Informational GVK warnings: some olm.gvk.required entries refer to APIs provided by the OpenShift platform itself (not by an operator). These appear in the "UNRESOLVABLE GVK DEPENDENCIES" section and can usually be ignored.
#!/usr/bin/env bash
#
# check-operator-deps.sh
#
# Given an imageset-config.yaml for oc-mirror, extract the file-based catalog
# from the redhat-operator-index image and check whether every operator listed
# in the config has its dependencies satisfied by other operators in the same
# config. Reports missing dependencies or prints an all-clear.
#
# Usage:
# ./check-operator-deps.sh <imageset-config.yaml> [--keep-configs]
# ./check-operator-deps.sh <imageset-config.yaml> --local-configs /path/to/configs
#
# Requirements: jq, yq (python-yq or mikefarah/yq), podman (unless --local-configs)
#
set -euo pipefail
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
die() { echo "ERROR: $*" >&2; exit 1; }
info() { echo "INFO: $*"; }
warn() { echo "WARN: $*" >&2; }
usage() {
cat <<EOF
Usage: $(basename "$0") <imageset-config.yaml> [--keep-configs] [--local-configs <path>]
Options:
--keep-configs Do not delete the extracted configs directory on exit.
Useful for debugging or re-running against the same index.
--local-configs <path> Use an already-extracted configs directory instead of
pulling the operator-index image via podman. <path>
should contain operator subdirectories (e.g. web-terminal/).
The imageset-config.yaml should follow the oc-mirror format, e.g.:
kind: ImageSetConfiguration
apiVersion: mirror.openshift.io/v1alpha2
mirror:
operators:
- catalog: registry.redhat.io/redhat/redhat-operator-index:v4.21
packages:
- name: web-terminal
- name: odf-operator
EOF
exit 1
}
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
KEEP_CONFIGS=false
LOCAL_CONFIGS=""
IMAGESET_CONFIG=""
while [[ $# -gt 0 ]]; do
case "$1" in
--keep-configs) KEEP_CONFIGS=true; shift ;;
--local-configs) LOCAL_CONFIGS="$2"; shift 2 ;;
-h|--help) usage ;;
-*) die "Unknown option: $1" ;;
*)
if [[ -z "$IMAGESET_CONFIG" ]]; then
IMAGESET_CONFIG="$1"; shift
else
die "Unexpected argument: $1"
fi
;;
esac
done
[[ -n "$IMAGESET_CONFIG" ]] || usage
[[ -f "$IMAGESET_CONFIG" ]] || die "File not found: $IMAGESET_CONFIG"
for cmd in jq yq; do
command -v "$cmd" >/dev/null 2>&1 || die "'$cmd' is required but not found in PATH."
done
if [[ -z "$LOCAL_CONFIGS" ]]; then
command -v podman >/dev/null 2>&1 || die "'podman' is required (unless --local-configs is used)."
fi
# ---------------------------------------------------------------------------
# Parse the imageset-config.yaml
#
# Supports multiple operator catalog entries. For each catalog image we
# collect the list of requested operator package names.
# ---------------------------------------------------------------------------
CATALOG_COUNT=$(yq -r '.mirror.operators | length' "$IMAGESET_CONFIG")
[[ "$CATALOG_COUNT" -gt 0 ]] || die "No operators found in $IMAGESET_CONFIG under .mirror.operators[]"
# ---------------------------------------------------------------------------
# Process each catalog entry
# ---------------------------------------------------------------------------
OVERALL_RC=0
for (( i=0; i<CATALOG_COUNT; i++ )); do
CATALOG_IMAGE=$(yq -r ".mirror.operators[$i].catalog" "$IMAGESET_CONFIG")
[[ -n "$CATALOG_IMAGE" && "$CATALOG_IMAGE" != "null" ]] || die "Could not read catalog image for .mirror.operators[$i]"
# Collect requested package names into a bash array
mapfile -t REQUESTED_PKGS < <(
yq -r ".mirror.operators[$i].packages[].name" "$IMAGESET_CONFIG" 2>/dev/null
)
[[ ${#REQUESTED_PKGS[@]} -gt 0 ]] || { warn "No packages listed for catalog $CATALOG_IMAGE — skipping."; continue; }
info "============================================================"
info "Catalog : $CATALOG_IMAGE"
info "Packages: ${REQUESTED_PKGS[*]}"
info "============================================================"
# ------------------------------------------------------------------
# Extract /configs from the operator-index image (or use local copy)
# ------------------------------------------------------------------
if [[ -n "$LOCAL_CONFIGS" ]]; then
CONFIGS_ROOT="$LOCAL_CONFIGS"
[[ -d "$CONFIGS_ROOT" ]] || die "Local configs directory not found: $CONFIGS_ROOT"
info "Using local configs at $CONFIGS_ROOT"
else
CONFIGS_DIR=$(mktemp -d --suffix=.olm-configs)
if [[ "$KEEP_CONFIGS" == "true" ]]; then
info "Extracted configs will be kept at: $CONFIGS_DIR"
else
trap 'rm -rf "$CONFIGS_DIR"' EXIT
fi
info "Pulling and extracting /configs from $CATALOG_IMAGE ..."
CONTAINER_ID=$(podman create --quiet "$CATALOG_IMAGE" 2>/dev/null) \
|| die "Failed to pull / create container from $CATALOG_IMAGE"
podman cp "$CONTAINER_ID":/configs "$CONFIGS_DIR/" 2>/dev/null \
|| { podman rm -f "$CONTAINER_ID" >/dev/null 2>&1; die "Failed to copy /configs from $CATALOG_IMAGE"; }
podman rm -f "$CONTAINER_ID" >/dev/null 2>&1
CONFIGS_ROOT="$CONFIGS_DIR/configs"
[[ -d "$CONFIGS_ROOT" ]] || die "Expected directory $CONFIGS_ROOT not found after extraction."
info "Catalog extracted to $CONFIGS_ROOT"
fi
# ------------------------------------------------------------------
# Build a lookup set of requested package names for fast membership
# tests. We use an associative array.
# ------------------------------------------------------------------
declare -A REQUESTED_SET=()
for pkg in "${REQUESTED_PKGS[@]}"; do
REQUESTED_SET["$pkg"]=1
done
# ------------------------------------------------------------------
# For each requested operator, scan its catalog for dependencies.
#
# The FBC layout has two shapes:
# 1. Single-file: <operator>/catalog.json (concatenated JSON objects)
# 2. Split: <operator>/bundles/*.json (one JSON object per file)
# + channels/ and package.json
#
# Dependencies appear inside olm.bundle objects in the "properties"
# array as one of:
# - olm.package.required → .value.packageName
# - olm.gvk.required → .value.{group,version,kind}
# - olm.constraint → recursively nested .package.packageName
# ------------------------------------------------------------------
# Collect ALL operators that provide a given GVK so we can resolve
# olm.gvk.required dependencies. Build: GVK_KEY → space-separated
# list of operator package names.
declare -A GVK_PROVIDERS=()
info "Building GVK provider index (this may take a moment) ..."
for op_dir in "$CONFIGS_ROOT"/*/; do
op_name=$(basename "$op_dir")
# Extract olm.gvk entries (APIs this operator provides)
gvks=""
if [[ -f "$op_dir/catalog.json" ]]; then
gvks=$(jq -r '
select(.schema == "olm.bundle") |
.properties[]? |
select(.type == "olm.gvk") |
"\(.value.group)/\(.value.version)/\(.value.kind)"
' "$op_dir/catalog.json" 2>/dev/null || true)
elif [[ -d "$op_dir/bundles" ]]; then
gvks=$(find "$op_dir/bundles" -name '*.json' -exec \
jq -r '
.properties[]? |
select(.type == "olm.gvk") |
"\(.value.group)/\(.value.version)/\(.value.kind)"
' {} + 2>/dev/null || true)
fi
while IFS= read -r gvk_key; do
[[ -n "$gvk_key" ]] || continue
existing="${GVK_PROVIDERS["$gvk_key"]:-}"
# Avoid duplicate provider entries
if [[ " $existing " != *" $op_name "* ]]; then
GVK_PROVIDERS["$gvk_key"]="$existing $op_name"
fi
done <<< "$gvks"
done
# ------------------------------------------------------------------
# Scan each requested operator for dependencies
# ------------------------------------------------------------------
declare -A ALL_MISSING_PKGS=() # packageName → list of "needed by" operators
declare -A ALL_MISSING_GVKS=() # GVK key → list of "needed by" operators
declare -A ALL_RESOLVABLE_GVKS=() # GVK key → provider(s) not in requested set
for pkg in "${REQUESTED_PKGS[@]}"; do
OP_DIR="$CONFIGS_ROOT/$pkg"
if [[ ! -d "$OP_DIR" ]]; then
warn "Operator directory not found in index: $pkg — skipping."
continue
fi
# Gather all dependency properties from every bundle version
DEP_PROPS=""
if [[ -f "$OP_DIR/catalog.json" ]]; then
DEP_PROPS=$(jq -c '
select(.schema == "olm.bundle") |
.properties[]? |
select(.type == "olm.package.required"
or .type == "olm.gvk.required"
or .type == "olm.constraint")
' "$OP_DIR/catalog.json" 2>/dev/null || true)
elif [[ -d "$OP_DIR/bundles" ]]; then
DEP_PROPS=$(find "$OP_DIR/bundles" -name '*.json' -exec \
jq -c '
.properties[]? |
select(.type == "olm.package.required"
or .type == "olm.gvk.required"
or .type == "olm.constraint")
' {} + 2>/dev/null || true)
fi
[[ -n "$DEP_PROPS" ]] || continue
# --- olm.package.required -------------------------------------------
pkg_deps=$(echo "$DEP_PROPS" | jq -r '
select(.type == "olm.package.required") | .value.packageName
' 2>/dev/null | sort -u)
while IFS= read -r dep; do
[[ -n "$dep" ]] || continue
if [[ -z "${REQUESTED_SET[$dep]:-}" ]]; then
ALL_MISSING_PKGS["$dep"]="${ALL_MISSING_PKGS["$dep"]:-}${ALL_MISSING_PKGS["$dep"]:+, }$pkg"
fi
done <<< "$pkg_deps"
# --- olm.constraint (nested packageName) ----------------------------
constraint_deps=$(echo "$DEP_PROPS" | jq -r '
select(.type == "olm.constraint") |
.value | .. | .packageName? // empty
' 2>/dev/null | sort -u)
while IFS= read -r dep; do
[[ -n "$dep" ]] || continue
if [[ -z "${REQUESTED_SET[$dep]:-}" ]]; then
ALL_MISSING_PKGS["$dep"]="${ALL_MISSING_PKGS["$dep"]:-}${ALL_MISSING_PKGS["$dep"]:+, }$pkg"
fi
done <<< "$constraint_deps"
# --- olm.gvk.required -----------------------------------------------
gvk_deps=$(echo "$DEP_PROPS" | jq -r '
select(.type == "olm.gvk.required") |
"\(.value.group)/\(.value.version)/\(.value.kind)"
' 2>/dev/null | sort -u)
while IFS= read -r gvk_key; do
[[ -n "$gvk_key" ]] || continue
providers="${GVK_PROVIDERS["$gvk_key"]:-}"
if [[ -z "$providers" ]]; then
ALL_MISSING_GVKS["$gvk_key"]="${ALL_MISSING_GVKS["$gvk_key"]:-}${ALL_MISSING_GVKS["$gvk_key"]:+, }$pkg"
continue
fi
# Check if ANY provider is in the requested set
resolved=false
for prov in $providers; do
if [[ -n "${REQUESTED_SET[$prov]:-}" ]]; then
resolved=true
break
fi
done
if [[ "$resolved" == "false" ]]; then
trimmed=$(echo "$providers" | xargs)
ALL_RESOLVABLE_GVKS["$gvk_key"]="${ALL_RESOLVABLE_GVKS["$gvk_key"]:-}${ALL_RESOLVABLE_GVKS["$gvk_key"]:+, }$pkg (provided by: $trimmed)"
fi
done <<< "$gvk_deps"
done
# ------------------------------------------------------------------
# Report results
# ------------------------------------------------------------------
echo ""
HAS_ISSUES=false
if [[ ${#ALL_MISSING_PKGS[@]} -gt 0 ]]; then
HAS_ISSUES=true
echo "=========================================="
echo " MISSING OPERATOR PACKAGE DEPENDENCIES"
echo "=========================================="
for dep in $(echo "${!ALL_MISSING_PKGS[@]}" | tr ' ' '\n' | sort); do
needed_by="${ALL_MISSING_PKGS[$dep]}"
echo " - $dep"
echo " needed by: $needed_by"
done
echo ""
fi
if [[ ${#ALL_RESOLVABLE_GVKS[@]} -gt 0 ]]; then
HAS_ISSUES=true
echo "=========================================="
echo " MISSING GVK (API) DEPENDENCIES"
echo "=========================================="
echo " These GVKs are required but the providing"
echo " operator is not in your imageset-config."
echo "=========================================="
for gvk in $(echo "${!ALL_RESOLVABLE_GVKS[@]}" | tr ' ' '\n' | sort); do
info_str="${ALL_RESOLVABLE_GVKS[$gvk]}"
echo " - $gvk"
echo " $info_str"
done
echo ""
fi
if [[ ${#ALL_MISSING_GVKS[@]} -gt 0 ]]; then
HAS_ISSUES=true
echo "=========================================="
echo " UNRESOLVABLE GVK DEPENDENCIES"
echo "=========================================="
echo " These GVKs are required but no operator in"
echo " the index provides them. They may be provided"
echo " by CRDs installed outside OLM."
echo "=========================================="
for gvk in $(echo "${!ALL_MISSING_GVKS[@]}" | tr ' ' '\n' | sort); do
needed_by="${ALL_MISSING_GVKS[$gvk]}"
echo " - $gvk"
echo " needed by: $needed_by"
done
echo ""
fi
if [[ "$HAS_ISSUES" == "true" ]]; then
echo "=========================================="
echo " SUGGESTED ADDITIONS to imageset-config"
echo "=========================================="
echo " Add these packages under .mirror.operators[].packages:"
echo ""
for dep in $(echo "${!ALL_MISSING_PKGS[@]}" | tr ' ' '\n' | sort); do
echo " - name: $dep"
done
# Suggest GVK providers too
declare -A SUGGESTED_GVK_PROVIDERS=()
for gvk in "${!ALL_RESOLVABLE_GVKS[@]}"; do
info_str="${ALL_RESOLVABLE_GVKS[$gvk]}"
# Extract provider names from the "(provided by: x y)" suffix
provs=$(echo "$info_str" | grep -oP 'provided by: \K[^)]+' | tr ' ' '\n' | sort -u)
for p in $provs; do
[[ -z "${REQUESTED_SET[$p]:-}" ]] && SUGGESTED_GVK_PROVIDERS["$p"]=1
done
done
for dep in $(echo "${!SUGGESTED_GVK_PROVIDERS[@]}" | tr ' ' '\n' | sort); do
# Don't double-suggest if already in the package list
[[ -n "${ALL_MISSING_PKGS[$dep]:-}" ]] && continue
echo " - name: $dep # provides required GVK"
done
echo ""
OVERALL_RC=1
else
echo "ALL OK — all operator dependencies are satisfied."
echo ""
fi
# Clean up associative arrays for next catalog iteration
unset REQUESTED_SET ALL_MISSING_PKGS ALL_MISSING_GVKS ALL_RESOLVABLE_GVKS GVK_PROVIDERS
done
exit $OVERALL_RC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment