This document describes the rules that plcc2fbc uses to validate data from the Red Hat Product Life Cycle Checker (PLCC) API before generating FBC (File-Based Catalog) lifecycle blobs.
After PLCC data is fetched, each product passes through four stages:
- PLCC filtering (
pkg/plcc/plcc.go): basic structural filtering before any validation runs. Drops products with nopackagename (FilterPackages). When-pis specified, keeps only the requested packages (FilterByPackageNames) and returns an error for any names not found. - PLCC-level validation (
pkg/plcc/validation.go): validators that check raw PLCC data quality (tier, release cadence, phase completeness, dates, duplicates). Organized into three groups:syntax,semantic, andcatalog. By default these filter out failing packages; with--permissivethey produce warnings only. Use--validatorsto select which validators to run (by label or group:all,syntax,semantic,catalog). - FBC Converter Pipeline (
pkg/fbc/conversion.go): type-checked field translation fromplcc.Versiontofbc.Version. Each converter validates one field and populates the corresponding output. Organized inconverterRegistry. Always run — cannot be disabled. - FBC Filter Pipeline (
pkg/fbc/filter.go): an ordered sequence ofFiltercallbacks that clean the translated FBC output (e.g., drop incomplete phases). Organized infilterRegistry.
| Condition | Stage | Effect |
|---|---|---|
| Product has no package name | PLCC filtering | Silently skipped |
Requested -p package not found |
PLCC filtering | Error (exit 3); with --permissive warning only |
| Package maps to multiple products | PLCC-level validation | All copies removed; with --permissive warning only |
| Invalid version name, timestamp, or OCP format | FBC converter pipeline | Entire package rejected |
| Phase with nil start or end date | FBC filter pipeline | Phase silently removed |
Before the FBC pipelines run, main.go calls PLCC-level validators on each raw plcc.Product. These validators live in pkg/plcc/validation.go alongside the data types they check. By default, failing packages are filtered out and logged as structured JSON to stderr. Use --permissive to keep failing packages in the output (warnings only). Use --validators to select which validators to run (by label or group), and --list-validators to see available options.
Validators are split into three groups: SyntaxValidators() (data format/structure), SemanticValidators() (business/lifecycle rules), and catalog (cross-product checks). DefaultValidators() composes syntax + semantic; DefaultCatalogValidators() returns catalog-level checks. All three groups are included in --validators all (the default). Catalog-level checks run via catalog.Validate().
The three-tier lifecycle model (Platform Aligned / Platform Agnostic / Rolling Stream) was introduced with OCP 4.14, which GA'd on 2023-10-31 (TierModelCutoffDate). Versions whose earliest parseable phase start date predates this cutoff — or that have no phases with parseable dates — are considered pre-tier-model and are exempt from tier-specific validators. This prevents false positives on legacy versions that were published before the tier model existed.
Tier-specific validators that apply the skip: ValidateTierSelected (REQ-TIER-ALL-02), ValidatePlatformAlignedPhases (REQ-TIER-PA-01), ValidatePlatformAlignedOCP (REQ-TIER-PA-02), ValidatePlatformAgnosticPhases (REQ-TIER-AG-01), ValidatePlatformAgnosticEUSPhases (REQ-TIER-AG-03), ValidatePlatformAgnosticEUSOCP (REQ-TIER-AG-04), ValidateRollingStreamPhases (REQ-TIER-RS-01), ValidateRollingStreamForbiddenPhases (REQ-TIER-RS-02), ValidateOCPFormat (REQ-FIELD-02), ValidateOCPFormatAll (CUSTOM-04).
Universal invariants that always apply regardless of version age: ValidateDatesStatic (REQ-DATE-02), ValidateDatesClean (REQ-DATE-03), ValidateDatesContiguity (REQ-DATE-04), ValidateVersionNames (REQ-VER-01), ValidatePhaseEndAfterStart (CUSTOM-03), ValidateReleaseCadence (REQ-TIER-ALL-01), ValidateIsOperator (CUSTOM-01), ValidateHasVersions (CUSTOM-02).
| # | Function | Label | Skip | Purpose |
|---|---|---|---|---|
| 1 | ValidateIsOperator |
CUSTOM-01 | Product must have package name and be flagged as operator | |
| 2 | ValidateHasVersions |
CUSTOM-02 | Product must have ≥1 version | |
| 3 | ValidateDatesStatic |
REQ-DATE-02 | Dates must be static values (checks _format field) |
|
| 4 | ValidateDatesClean |
REQ-DATE-03 | Non-empty, non-N/A dates must cleanly parse | |
| 5 | ValidateDatesContiguity |
REQ-DATE-04 | Consecutive phases must start 1 day after previous ends | |
| 6 | ValidatePhaseEndAfterStart |
CUSTOM-03 | Phase end date must be after start date | |
| 7 | ValidateVersionNames |
REQ-VER-01 | Version names must match MAJOR.MINOR |
|
| 8 | ValidateOCPFormat |
REQ-FIELD-02 | pre-tier | OCP compatibility on aligned versions must match MAJOR.MINOR |
| 9 | ValidateOCPFormatAll |
CUSTOM-04 | pre-tier | OCP compatibility format on non-aligned versions |
| # | Function | Label | Skip | Purpose |
|---|---|---|---|---|
| 1 | ValidateReleaseCadence |
REQ-TIER-ALL-01 | Operators must have release cadence specified | |
| 2 | ValidateTierSelected |
REQ-TIER-ALL-02 | pre-tier | Operator versions must have lifecycle tier selected |
| 3 | ValidatePlatformAlignedPhases |
REQ-TIER-PA-01 | pre-tier | Aligned: required phases with parseable dates (OCP cross-referenced) |
| 4 | ValidatePlatformAlignedOCP |
REQ-TIER-PA-02 | pre-tier | Aligned: OCP compatibility must be specified |
| 5 | ValidatePlatformAgnosticPhases |
REQ-TIER-AG-01 | pre-tier | Agnostic: Full Support and Maintenance with parseable dates |
| 6 | ValidatePlatformAgnosticEUSPhases |
REQ-TIER-AG-03 | pre-tier | EUS-aligned agnostic: all 3 EUS terms with parseable dates, or none |
| 7 | ValidatePlatformAgnosticEUSOCP |
REQ-TIER-AG-04 | pre-tier | EUS-aligned agnostic: OCP compatibility must be specified |
| 8 | ValidateRollingStreamPhases |
REQ-TIER-RS-01 | pre-tier | Rolling: Full Support with parseable dates |
| 9 | ValidateRollingStreamForbiddenPhases |
REQ-TIER-RS-02 | pre-tier | Rolling: must not include Maintenance or EUS phases |
ValidatePlatformAlignedPhases checks that platform-aligned operator versions have the required lifecycle phases with parseable dates. Rather than hardcoding all five phases (Full Support, Maintenance, EUS Terms 1/2/3), the validator cross-references the OCP (OpenShift Container Platform) product from the PLCC catalog to determine which phases are actually required.
Why: As of this writing, OCP only has EUS phases on even-numbered minor versions (4.14, 4.16, 4.18, ...). Odd-numbered versions (4.15, 4.17, ...) have EUS phase slots in PLCC with "N/A" dates. This pattern could change in future OCP releases. Rather than hardcoding even/odd logic, the validator reads the OCP product's actual PLCC data to determine which phases have real dates — so it will automatically adapt if the EUS policy changes. Without this cross-referencing, operators on odd OCP versions always fail because they lack EUS phases that OCP itself doesn't have.
Resolution logic (resolveRequiredPhases):
- Always required: Full Support and Maintenance.
- Conditionally required: Each EUS phase (Terms 1, 2, 3) is required only if ANY OCP version referenced in the operator version's
openshift_compatibilityfield has that phase with parseable (non-N/A, non-empty) start and end dates. - Fallback (conservative): If OCP product data is unavailable, the OCP version is not found, or the operator has no
openshift_compatibilityvalue, all five phases are required. This prevents false negatives.
How OCP context is injected:
LookupValidators(deps, ...)callsinitPlatformAlignedPhases(deps)for the REQ-TIER-PA-01 entry, which creates an OCP-aware closure viaValidatePlatformAlignedPhases(ocpProduct).- The OCP product is extracted from the full PLCC catalog before package-name filtering (since OCP itself is not an operator package and would be filtered out).
Example:
An operator version with openshift_compatibility: "4.17":
- OCP 4.17 has Full Support and Maintenance with dates, but EUS Terms 1/2/3 have
"N/A"dates. - Only Full Support and Maintenance are required for this operator version.
An operator version with openshift_compatibility: "4.16, 4.18":
- OCP 4.16 has all five phases with dates → all five phases required (union semantics: if ANY referenced OCP version has EUS, the operator must too).
Catalog validators are cross-product checks selectable via --validators catalog or by label (e.g. --validators REQ-VAL-01). They are included in --validators all (the default). By default, all products with a duplicated package name are removed (all copies, since we cannot determine which is authoritative). With --permissive, duplicates produce warnings only.
| # | Function | Label | Skip | Purpose |
|---|---|---|---|---|
| 1 | ValidateNoDuplicates |
REQ-VAL-01 | No package name appears in multiple products |
The converter pipeline is defined by converterRegistry in pkg/fbc/conversion.go. Each converter has the signature:
type Converter func(src plcc.Version, dst *Version) []errorConverters validate and translate one field of a PLCC version into the corresponding FBC version field. They always run during translation — they cannot be disabled. If any converter returns errors, the entire package is rejected.
| # | Function | Label | Purpose |
|---|---|---|---|
| 1 | ConvertVersionName |
FBC-VER-01 | Parse version name as MAJOR.MINOR |
| 2 | ConvertPhases |
FBC-PHASE-01 | Translate phase timestamps to FBC dates |
| 3 | ConvertOCPCompatibility |
FBC-OCP-01 | Parse OCP compatibility versions as MAJOR.MINOR |
DefaultConverters() returns all converters from the registry in order. translateVersion() iterates them to build each fbc.Version.
The pipeline is defined by filterRegistry in pkg/fbc/filter.go. Each filter has the signature:
type Filter func(*Package) []stringFilters mutate packages and enforce structural invariants on translated FBC output. Mutation filters clean up data (e.g., drop incomplete phases); invariant validators reject packages that violate FBC schema requirements. Returning a non-empty []string rejects the package; returning nil means the package passes.
Filters run in order, and the pipeline short-circuits: the first filter that returns reasons stops execution.
The default pipeline:
| # | Function | Label | Kind | Purpose |
|---|---|---|---|---|
| 1 | FilterIncompletePhases |
FBC-MUTATE-01 | mutate | Drop phases where either date is nil |
| 2 | ValidatePackageHasVersions |
FBC-VAL-01 | invariant | Package must have ≥1 version |
| 3 | ValidateVersionsHavePhases |
FBC-VAL-02 | invariant | Every version must have ≥1 phase |
| 4 | ValidatePhaseDates |
FBC-VAL-03 | invariant | Every phase must have non-nil start and end dates |
| 5 | ValidateDateOrdering |
FBC-VAL-04 | invariant | Phase start date must not be after end date |
| 6 | ValidatePhaseContiguity |
FBC-VAL-05 | invariant | Consecutive phases must be contiguous (end + 1 day = next start) |
Removes phases where either startDate or endDate is nil. This includes N/A phases (both nil) and point-in-time phases (one set, one nil).
This filter always returns nil — it mutates the package but never rejects it.
Filters live in pkg/fbc/filter.go and handle output cleanup (mutations) and FBC structural invariants (validation). PLCC-level data quality checks belong in the PLCC validators (pkg/plcc/validation.go).
-
Write a new function with signature
func(p *Package) []string.- Mutate the package
pas needed (e.g., drop or rewrite data). - Return
nilto accept or a list of reason strings to reject.
- Mutate the package
-
Add an entry to
filterRegistrywith a label (e.g.FBC-MUTATE-02for mutations,FBC-VAL-06for invariants) and group. Embed the label in all reason strings. -
Add a test in
pkg/fbc/filter_test.go.
Note: DefaultFilters() reads from filterRegistry and returns a fresh slice each time, so callers can safely append or reorder filters for custom pipelines without affecting the default.