Skip to content

Instantly share code, notes, and snippets.

@dmc5179
Last active August 24, 2026 18:51
Show Gist options
  • Select an option

  • Save dmc5179/4430a81c713ba65e3a170ec094313881 to your computer and use it in GitHub Desktop.

Select an option

Save dmc5179/4430a81c713ba65e3a170ec094313881 to your computer and use it in GitHub Desktop.
Creating a custom OLM catalog from upstream portworx operator bundle

Portworx Custom Operator Catalog from docker.io

This procedure builds a custom OLM catalog for the Portworx operator using the bundle image directly from docker.io, completely bypassing registry.connect.redhat.com. The rendered FBC from the bundle contains registry.connect.redhat.com image references which are replaced with their docker.io equivalents before building the catalog.

Step 1 — Pull the bundle image

podman pull docker.io/portworx/portworx-certified-bundle:26.2.1

Step 2 — Render FBC from the docker.io bundle

opm render docker.io/portworx/portworx-certified-bundle:26.2.1 > portworx-fbc.json

This produces a single olm.bundle entry. It does not include the olm.package or olm.channel entries that OLM requires to discover and install the operator. Those are added in Step 4.

Step 3 — Replace registry.connect.redhat.com image references with docker.io

The rendered bundle references registry.connect.redhat.com/portworx/openstorage-operator in three places: the relatedImages list, the deployment container spec, and the containerImage annotation. These are the same operator binary that Portworx publishes on docker.io as docker.io/portworx/px-operator.

First, identify the image reference that needs replacing:

jq -r '.. | .image? // empty' portworx-fbc.json | grep registry.connect

Expected output:

registry.connect.redhat.com/portworx/openstorage-operator@sha256:ca1368a398964d35e2dad2add10027722b4af1336bc0c9e359f44da0bc1dec15

Set the old and new image references as environment variables:

export OLD_IMAGE=$(jq -r '.. | .image? // empty' portworx-fbc.json | grep registry.connect)
export NEW_IMAGE="docker.io/portworx/px-operator:26.2.1"

The image reference appears both in plain JSON fields and inside base64-encoded bundle objects. Use the following Python script to replace all occurrences:

python3 <<'PYEOF'
import json
import base64
import os

old_image = os.environ["OLD_IMAGE"]
new_image = os.environ["NEW_IMAGE"]

with open("portworx-fbc.json", "r") as f:
    content = f.read()

objects = []
decoder = json.JSONDecoder()
pos = 0
content_stripped = content.strip()
while pos < len(content_stripped):
    while pos < len(content_stripped) and content_stripped[pos] in ' \t\n\r':
        pos += 1
    if pos >= len(content_stripped):
        break
    obj, end = decoder.raw_decode(content_stripped, pos)
    objects.append(obj)
    pos = end

for obj in objects:
    if "image" in obj and isinstance(obj["image"], str):
        obj["image"] = obj["image"].replace(old_image, new_image)

    if "relatedImages" in obj:
        for ri in obj["relatedImages"]:
            if "image" in ri:
                ri["image"] = ri["image"].replace(old_image, new_image)

    if "properties" in obj:
        for prop in obj["properties"]:
            if prop.get("type") == "olm.bundle.object" and "value" in prop and "data" in prop["value"]:
                decoded = base64.b64decode(prop["value"]["data"]).decode("utf-8")
                if old_image in decoded:
                    decoded = decoded.replace(old_image, new_image)
                    prop["value"]["data"] = base64.b64encode(decoded.encode("utf-8")).decode("utf-8")

with open("portworx-fbc.json", "w") as f:
    for obj in objects:
        json.dump(obj, f, separators=(',', ':'))
        f.write("\n")

print(f"Replaced: {old_image}")
print(f"    with: {new_image}")
PYEOF

Verify no registry.connect references remain:

grep -c "registry.connect" portworx-fbc.json
jq -r '.. | .image? // empty' portworx-fbc.json | sort -u

Expected output:

0
docker.io/portworx/portworx-certified-bundle:26.2.1
docker.io/portworx/px-operator:26.2.1

Step 4 — Add the package and channel entries

4a — Find the CSV name from the rendered bundle

CSV_NAME=$(jq -r 'select(.schema == "olm.bundle") | .name' portworx-fbc.json)
echo "CSV_NAME=${CSV_NAME}"

For version 26.2.1 the value is portworx-operator.v26.2.1.

4b — Extract the package name and default channel from the certified-operator-index

Render the Portworx entries from the official certified-operator-index to a separate file:

opm render registry.redhat.io/redhat/certified-operator-index:v4.22 | \
  jq 'select(.package == "portworx-certified" or .name == "portworx-certified")' \
  > portworx-certified-fbc.json

Note: This command renders the entire certified-operator-index and filters for Portworx. It can take several minutes depending on network speed.

Extract the package name and default channel:

PACKAGE_NAME=$(jq -r 'select(.schema == "olm.package") | .name' portworx-certified-fbc.json)
DEFAULT_CHANNEL=$(jq -r 'select(.schema == "olm.package") | .defaultChannel' portworx-certified-fbc.json)
echo "PACKAGE_NAME=${PACKAGE_NAME}"
echo "DEFAULT_CHANNEL=${DEFAULT_CHANNEL}"

For Portworx the values are portworx-certified and stable.

4c — Append the package and channel entries to the FBC

cat >> portworx-fbc.json <<EOF
{
  "schema": "olm.package",
  "name": "${PACKAGE_NAME}",
  "defaultChannel": "${DEFAULT_CHANNEL}"
}
{
  "schema": "olm.channel",
  "package": "${PACKAGE_NAME}",
  "name": "${DEFAULT_CHANNEL}",
  "entries": [
    {
      "name": "${CSV_NAME}"
    }
  ]
}
EOF

4d — Verify the result

Confirm all three schema types are present and the names are consistent:

jq -r '[.schema, .name, .package // empty] | join("  ")' portworx-fbc.json

Expected output:

olm.bundle   portworx-operator.v26.2.1  portworx-certified
olm.package  portworx-certified
olm.channel  stable  portworx-certified

The package field on the olm.channel must match the olm.package name, and the entries[].name on the channel must match the olm.bundle name.

Step 5 — Validate the catalog

opm validate requires a directory, not a file. Copy the FBC into a directory structure and validate:

mkdir -p portworx-catalog
cp portworx-fbc.json portworx-catalog/catalog.json
opm validate portworx-catalog

No output means validation passed.

Step 6 — Build the custom catalog image

Create Containerfile.portworx-catalog:

FROM registry.redhat.io/openshift4/ose-operator-registry-rhel9:v4.22
COPY portworx-catalog /configs
RUN ["/bin/opm", "serve", "/configs", "--cache-dir=/tmp/cache", "--cache-only"]
EXPOSE 50051
ENTRYPOINT ["/bin/opm"]
CMD ["serve", "/configs", "--cache-dir=/tmp/cache"]

Build and push:

podman build -f Containerfile.portworx-catalog -t <mirror-registry>/portworx-custom-catalog:latest .
podman push <mirror-registry>/portworx-custom-catalog:latest

Step 7 — Mirror the operator images to your disconnected registry

List all images referenced in the FBC:

jq -r '.. | .image? // empty' portworx-fbc.json | sort -u

Expected output:

docker.io/portworx/portworx-certified-bundle:26.2.1
docker.io/portworx/px-operator:26.2.1

Mirror each image from docker.io to your mirror registry:

skopeo copy \
  docker://docker.io/portworx/portworx-certified-bundle:26.2.1 \
  docker://<mirror-registry>/portworx/portworx-certified-bundle:26.2.1

skopeo copy \
  docker://docker.io/portworx/px-operator:26.2.1 \
  docker://<mirror-registry>/portworx/px-operator:26.2.1

Step 8 — Create the CatalogSource

Apply catalogsource.yaml:

apiVersion: operators.coreos.com/v1alpha1
kind: CatalogSource
metadata:
  name: portworx-custom-catalog
  namespace: openshift-marketplace
spec:
  sourceType: grpc
  image: <mirror-registry>/portworx-custom-catalog:latest
  displayName: Portworx (Custom Catalog)
  publisher: Custom
  updateStrategy:
    registryPoll:
      interval: 30m
oc apply -f catalogsource.yaml

Step 9 — Create IDMS to redirect image pulls to your mirror

Apply idms-portworx.yaml:

apiVersion: config.openshift.io/v1
kind: ImageDigestMirrorSet
metadata:
  name: portworx-mirror
spec:
  imageDigestMirrors:
  - source: docker.io/portworx
    mirrors:
    - <mirror-registry>/portworx
oc apply -f idms-portworx.yaml

Step 10 — Install the operator

The Portworx operator should now appear in OperatorHub under the custom catalog. To install via CLI:

oc apply -f - <<EOF
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: portworx-certified
  namespace: openshift-operators
spec:
  channel: stable
  name: portworx-certified
  source: portworx-custom-catalog
  sourceNamespace: openshift-marketplace
EOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment