Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 40 additions & 20 deletions cluster-update/product-lifecycle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,29 +41,52 @@ Be specific with product names to avoid overly broad results.

```bash
python3 product-lifecycle/scripts/plc_lookup.py olm-check --ocp 4.21 \
--operators '[{"package":"cluster-logging"},{"package":"web-terminal"}]'
--operators '[{"package":"cluster-logging","version":"6.5.1"},{"package":"web-terminal"}]'
```

Looks up each operator by its OLM `package` name. Reports
`lifecycle_unavailable` for operators not tracked by package name.
When `olm-check` reports `lifecycle_unavailable`, retry those operators
with `products` using the operator's product name (e.g. "compliance operator")
before concluding that lifecycle data is unavailable.
Looks up each operator by its OLM `package` name. Each operator in the
JSON array accepts `package` (required) and `version` (optional).

Returns **one result per operator**. The shape depends on whether a
`version` was provided:

- **With version (matched):** `package`, `requested_version`, `product`,
`status`, `ocp_compatible`, `phases`. No `error` field.
- **Without version (package exists):** `package`, `product`,
`available_versions`. No `error` field.
- **Error:** `error` field set — explains what went wrong (package not
found, version not tracked). Includes `available_versions` when the
package exists but the version doesn't.

When a version is provided, the tool normalizes it to major.minor
(e.g. `1.9.0` → `1.9`) and matches against API version names.

Reports `lifecycle_unavailable` listing every operator whose result has `error` set.


## Output Format

All commands output JSON. Each product version entry includes:
All commands output JSON.

### `products` output

Each product version entry includes `product`, `former_names`, `package`,
`version`, `status`, `ocp_versions`, `ocp_compatible`, and `phases`.

### `olm-check` output

One entry per operator:

| Field | Description |
|---|---|
| `product` | Current product name |
| `former_names` | Previous product names (useful for search fallback) |
| `package` | OLM package name (maps to Subscription `spec.name`) |
| `version` | Version number |
| `status` | Raw API support status (e.g. `"Full Support"`, `"End of life"`) |
| `ocp_versions` | List of compatible OCP versions (empty for non-layered products) |
| `ocp_compatible` | `true`/`false`/`null` — only present when `--ocp` is used |
| `phases` | Array of lifecycle phases with `name`, `start_date`, `end_date` |
| `package` | OLM package name queried |
| `requested_version` | Normalized version checked (when provided) |
| `error` | Why data is unavailable — absent on success, set on failure |
| `product` | Product name (on success) |
| `status` | Raw API support status, e.g. `"Full Support"`, `"End of life"` (on success) |
| `ocp_compatible` | `true`/`false`/`null` — whether the version is compatible with the target OCP (on success) |
| `phases` | Lifecycle phases with `name`, `start_date`, `end_date` (on success) |
| `available_versions` | Versions the API does track (when package exists but version doesn't) |

## When to Use

Expand All @@ -77,10 +100,7 @@ All commands output JSON. Each product version entry includes:
## Important

- `ocp_versions` is only present on **layered product** versions, not on OCP itself.
- Not all operators have lifecycle entries — report "lifecycle data unavailable"
rather than treating missing data as an error.
- An operator has lifecycle data only if the API returns an entry matching its
**installed version**. If the API tracks the package but not the specific
version installed, treat it as "lifecycle data unavailable" for that version.
- Not all operators have lifecycle entries — `olm-check` sets `error` on the
result for operators without data.
- The `package` field in API responses maps to the OLM Subscription's
`spec.name` — use this for exact matching, not product name.
86 changes: 63 additions & 23 deletions cluster-update/product-lifecycle/scripts/plc_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
API_BASE = "https://access.redhat.com/product-life-cycles/api/v2/products"


def api_search(name):
url = f"{API_BASE}?{urllib.parse.urlencode({'name': name})}"
def _api_get(url):
"""Fetch a PLC API URL and return the 'data' list."""
req = urllib.request.Request(url, headers={"User-Agent": "plc-lookup/1.0"})
try:
with urllib.request.urlopen(req, timeout=30) as resp:
Expand All @@ -27,6 +27,20 @@ def api_search(name):
return body["data"]


def api_search(url=API_BASE, name=None):
"""Fetch products, optionally filtering by name."""
if name:
url = f"{url}?{urllib.parse.urlencode({'name': name})}"
return _api_get(url)


def _normalize_version(version):
"""Normalize to major.minor, stripping leading 'v' (e.g. 'v1.9.0' → '1.9')."""
version = version.lstrip("v")
parts = version.split(".")
return ".".join(parts[:2]) if len(parts) >= 2 else version
Comment thread
jrangelramos marked this conversation as resolved.


def parse_ocp_versions(compat_string):
if not compat_string:
return []
Expand Down Expand Up @@ -60,7 +74,7 @@ def format_product_version(product, version, target_ocp=None):


def cmd_products(args, output=sys.stdout):
products = api_search(args.name)
products = api_search(name=args.name)
if not products:
json.dump({"error": "no products found", "query": args.name}, output, indent=2)
output.write("\n")
Expand All @@ -84,51 +98,76 @@ def cmd_olm_check(args, output=sys.stdout):
operators = json.loads(args.operators)
target = args.ocp

batch = api_search("OpenShift")
all_products = api_search()
by_package = collections.defaultdict(list)
for p in batch:
for p in all_products:
pkg = p.get("package")
if pkg:
by_package[pkg].append(p)

results = []
missed_packages = []

for op in operators:
pkg = op.get("package", "")
requested_version = op.get("version", "")

if not pkg:
results.append({
"package": pkg,
"status": "lifecycle_unavailable",
"reason": "empty package name",
})
missed_packages.append(pkg)
results.append({"package": pkg, "error": "empty package name"})
continue

products = by_package.get(pkg)

if not products:
extra = api_search(pkg.replace("-", " "))
products = [p for p in extra if p.get("package") == pkg]
entry = {"package": pkg, "error": "package not found in PLC API"}
if requested_version:
entry["requested_version"] = _normalize_version(requested_version)
results.append(entry)
continue

if not products:
all_versions = {v.get("name") for p in products for v in p["versions"]} - {None}

if not requested_version:
results.append({
"package": pkg,
"product": products[0]["name"],
"available_versions": sorted(all_versions),
})
continue

norm = _normalize_version(requested_version)
matched = None
for p in products:
for v in p["versions"]:
if v.get("name") in (norm, f"{norm}.x"):
matched = (p, v)
break
if matched:
break

if not matched:
results.append({
"package": pkg,
"status": "lifecycle_unavailable",
"reason": "no lifecycle data found for this package",
"requested_version": norm,
"error": f"version {norm} not tracked",
"available_versions": sorted(all_versions),
})
missed_packages.append(pkg)
continue
Comment thread
jrangelramos marked this conversation as resolved.

for product in products:
for v in product["versions"]:
results.append(format_product_version(product, v, target_ocp=target))
product, version = matched
formatted = format_product_version(product, version, target_ocp=target)
results.append({
"package": pkg,
"requested_version": norm,
"product": formatted["product"],
"status": formatted["status"],
"ocp_compatible": formatted.get("ocp_compatible"),
"phases": formatted["phases"],
})

json.dump({
"ocp_target": target,
"operators_checked": len(operators),
"lifecycle_unavailable": missed_packages,
"lifecycle_unavailable": [r["package"] for r in results if "error" in r],
"results": results,
}, output, indent=2)
output.write("\n")
Expand All @@ -141,7 +180,8 @@ def main(args=None, output=sys.stdout):
epilog="Examples:\n"
' %(prog)s products "logging for Red Hat OpenShift"\n'
' %(prog)s products "logging for Red Hat OpenShift" --ocp 4.21\n'
' %(prog)s olm-check --ocp 4.21 --operators \'[{"package":"cluster-logging"}]\'\n',
' %(prog)s olm-check --ocp 4.21 --operators \'[{"package":"cluster-logging"}]\'\n'
' %(prog)s olm-check --ocp 4.21 --operators \'[{"package":"cluster-logging","version":"6.5.1"},{"package":"openshift-pipelines-operator-rh","version":"1.22.0"}]\'\n',
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", required=True)
Expand Down
Loading