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.
podman pull docker.io/portworx/portworx-certified-bundle:26.2.1opm render docker.io/portworx/portworx-certified-bundle:26.2.1 > portworx-fbc.jsonThis 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.
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.connectExpected 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}")
PYEOFVerify no registry.connect references remain:
grep -c "registry.connect" portworx-fbc.json
jq -r '.. | .image? // empty' portworx-fbc.json | sort -uExpected output:
0
docker.io/portworx/portworx-certified-bundle:26.2.1
docker.io/portworx/px-operator:26.2.1
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.
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.jsonNote: 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.
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}"
}
]
}
EOFConfirm all three schema types are present and the names are consistent:
jq -r '[.schema, .name, .package // empty] | join(" ")' portworx-fbc.jsonExpected 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.
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-catalogNo output means validation passed.
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:latestList all images referenced in the FBC:
jq -r '.. | .image? // empty' portworx-fbc.json | sort -uExpected 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.1Apply 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: 30moc apply -f catalogsource.yamlApply idms-portworx.yaml:
apiVersion: config.openshift.io/v1
kind: ImageDigestMirrorSet
metadata:
name: portworx-mirror
spec:
imageDigestMirrors:
- source: docker.io/portworx
mirrors:
- <mirror-registry>/portworxoc apply -f idms-portworx.yamlThe 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