Skip to content

OTA-1974: Teach Cincinnati to include product information when serving /graph-data - #1073

Open
racheljpg wants to merge 1 commit into
openshift:masterfrom
racheljpg:teachcincinnati
Open

OTA-1974: Teach Cincinnati to include product information when serving /graph-data#1073
racheljpg wants to merge 1 commit into
openshift:masterfrom
racheljpg:teachcincinnati

Conversation

@racheljpg

@racheljpg racheljpg commented Jul 10, 2026

Copy link
Copy Markdown

Hello! This is a PR to for OTA-1974. Thanks!

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added optional product lifecycle API polling (API URL, polling interval, request timeout) configurable via CLI and config.
    • Introduced background fetching/persistence and new Prometheus metrics for fetch attempts, errors, 304 “not modified”, and last successful update.
    • Product lifecycle data is now included in generated graph artifacts when available.
  • Bug Fixes

    • Graph generation continues even if lifecycle data cannot be copied into the artifact (warning logged).
  • Bug Fixes / Validation

    • When enabled, zero polling interval or request timeout is now rejected.

Assisted-by: Claude Code

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jul 10, 2026
@openshift-ci-robot

openshift-ci-robot commented Jul 10, 2026

Copy link
Copy Markdown

@racheljpg: This pull request references OTA-1974 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Hello! This is a draft PR to get some feedback on this PR for OTA-1974. Thanks!

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@racheljpg
racheljpg marked this pull request as draft July 10, 2026 12:46
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds configurable product lifecycle polling, shared product-data state, graph-builder propagation, and inclusion of products.json in generated graph archives.

Changes

Product lifecycle data integration

Layer / File(s) Summary
Product lifecycle configuration
graph-builder/src/config/*
Adds CLI/TOML options, defaults, merging, and validation for lifecycle enablement, API URL, polling interval, and request timeout.
Polling and persistence
graph-builder/src/product_lifecycle.rs, graph-builder/src/lib.rs
Fetches product data with ETag support, validates JSON, persists it, updates shared state, and records Prometheus metrics.
Graph state and startup propagation
graph-builder/src/graph.rs, graph-builder/src/main.rs
Adds lifecycle state, starts the polling task, registers metrics, and passes the product path through plugin parameters.
Graph archive product packaging
commons/src/lib.rs, cincinnati/src/plugins/internal/graph_builder/*/plugin.rs
Defines the product-data parameter key and copies the referenced file into graph data as products.json when available.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProductFetcher
  participant ProductAPI
  participant GraphState
  participant GraphRunner
  participant MetadataScraper
  ProductFetcher->>ProductAPI: Fetch product lifecycle JSON with ETag
  ProductAPI-->>ProductFetcher: Return JSON or 304 response
  ProductFetcher->>GraphState: Persist data and saved path
  GraphRunner->>GraphState: Read product data path
  GraphRunner->>MetadataScraper: Pass PRODUCT_DATA_PARAM_KEY
  MetadataScraper->>MetadataScraper: Copy file as products.json
  MetadataScraper-->>GraphRunner: Continue graph archive creation
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
No-Sensitive-Data-In-Logs ❌ Error main logs full AppSettings and product_lifecycle logs api_url at info level, exposing configured endpoints/internal hostnames. Remove or sanitize those logs; redact product_api_url and other sensitive settings, and keep only non-sensitive status in info logs.
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
No-Weak-Crypto ❓ Inconclusive temp temp
✅ Passed checks (12 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed No Ginkgo-style titles were added; the touched files only contain static Rust #[test] function names, not dynamic It/Describe/Context/When strings.
Test Structure And Quality ✅ Passed No Ginkgo/Go test files were changed; the diff only touches Rust source files, so this test-quality check is not applicable.
Microshift Test Compatibility ✅ Passed No Ginkgo/e2e tests were added; the commit only changes Rust graph-builder/plugin code, so MicroShift API compatibility isn’t implicated.
Single Node Openshift (Sno) Test Compatibility ✅ Passed No new Ginkgo/e2e tests were added; the touched files are implementation-only and contain no multi-node or SNO-unsafe test assumptions.
Topology-Aware Scheduling Compatibility ✅ Passed No deployment manifests, affinities, node selectors, replicas, or PDB changes were introduced; the PR only adds product-lifecycle fetch/copy logic and shared state.
Ote Binary Stdout Contract ✅ Passed No process-level stdout writes were introduced; main() uses env_logger defaults and the only println! found is inside a test.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No new Ginkgo e2e tests were added in the changed files; the diff only touches Rust production code, so IPv4/disconnected-network test concerns don't apply.
Container-Privileges ✅ Passed The patch only touches Rust source files; no container/K8s manifests were changed and searches found no privileged/host* or allowPrivilegeEscalation settings.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding product information to /graph-data responses.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
graph-builder/src/config/settings.rs (1)

86-101: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Defaulting product_enabled to true makes external network fetching mandatory by default.

Prior to this change, graph-builder had no dependency on an external Red Hat API. With product_enabled defaulting to true, every existing deployment (including disconnected/airgapped OpenShift clusters, a common Cincinnati deployment scenario per the project's architecture) will now attempt to poll access.redhat.com unless explicitly disabled via config. Worth confirming whether fetch failures degrade gracefully (background thread) rather than blocking startup or /graph-data availability — the fetcher implementation isn't in this review batch.

Consider defaulting product_enabled to false (opt-in) to avoid surprising existing deployments with a new mandatory external dependency, or clearly document this behavior change in release/migration notes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graph-builder/src/config/settings.rs` around lines 86 - 101, Default product
lifecycle fetching to opt-in so existing and disconnected deployments do not
contact the external Red Hat API unexpectedly. Update the default value for
product_enabled in the settings configuration from true to false, while
preserving the existing API URL, polling interval, and timeout defaults.
graph-builder/src/graph.rs (2)

155-160: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Product-lifecycle fields are pub, unlike the rest of State.

json, mandatory_params, live, ready, secondary_metadata are all private with accessor methods (is_live, is_ready), but the new product_data/product_etag/product_data_path are pub, directly exposing mutable internal state outside the module. Since the only external writer is product_lifecycle.rs within the same crate, pub(crate) preserves current usage while restoring encapsulation.

♻️ Proposed fix
     /// Product lifecycle JSON data
-    pub product_data: Arc<RwLock<String>>,
+    pub(crate) product_data: Arc<RwLock<String>>,
     /// Last ETag from product lifecycle API
-    pub product_etag: Arc<RwLock<Option<String>>>,
+    pub(crate) product_etag: Arc<RwLock<Option<String>>>,
     /// Path to temporary products.json file
-    pub product_data_path: Arc<RwLock<Option<std::path::PathBuf>>>,
+    pub(crate) product_data_path: Arc<RwLock<Option<std::path::PathBuf>>>,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graph-builder/src/graph.rs` around lines 155 - 160, Change the visibility of
State fields product_data, product_etag, and product_data_path from pub to
pub(crate), preserving access for product_lifecycle.rs within the crate while
preventing external direct mutation and matching the encapsulation of the other
State fields.

241-257: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silent drop when product data path isn't valid UTF-8.

If path.to_str() returns None, the parameter is silently omitted with no log, making it hard to diagnose why products.json never appears in the archive. Worth a warning log for visibility.

♻️ Proposed fix
         if let Some(ref path) = *state.product_data_path.read() {
             if let Some(path_str) = path.to_str() {
                 parameters.insert(
                     commons::PRODUCT_DATA_PARAM_KEY.to_string(),
                     path_str.to_string(),
                 );
+            } else {
+                warn!("product data path is not valid UTF-8, skipping: {:?}", path);
             }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graph-builder/src/graph.rs` around lines 241 - 257, Add a warning log in the
product-data parameter preparation block before or when `path.to_str()` returns
`None`, identifying the invalid UTF-8 path and explaining that
`commons::PRODUCT_DATA_PARAM_KEY` was omitted. Keep the existing insertion
behavior for valid paths and use the component’s existing logging facility.
cincinnati/src/plugins/internal/graph_builder/dkrv2_openshift_secondary_metadata_scraper/plugin.rs (1)

365-381: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated product-copy logic across both scraper plugins.

This exact block (lookup PRODUCT_DATA_PARAM_KEY, check existence, copy to products.json, log success/failure) is duplicated verbatim in github_openshift_secondary_metadata_scraper/plugin.rs (lines 526-542). Since both scrapers already share commons::create_tar, consider extracting this into a shared commons helper to avoid the two copies diverging over time.

♻️ Proposed shared helper
// commons/src/lib.rs
pub async fn copy_product_data(parameters: &std::collections::HashMap<String, String>, dest_dir: &std::path::Path) {
    if let Some(product_path) = parameters.get(PRODUCT_DATA_PARAM_KEY) {
        let source_path = std::path::Path::new(product_path);
        if source_path.exists() {
            let dest_path = dest_dir.join("products.json");
            match tokio::fs::copy(source_path, &dest_path).await {
                Ok(_) => log::info!("Copied product lifecycle data to graph-data tarball"),
                Err(e) => log::warn!("Failed to copy product lifecycle data: {}", e),
            }
        }
    }
}
-        // Copy product lifecycle data if available
-        if let Some(product_path) = io.parameters.get(PRODUCT_DATA_PARAM_KEY) {
-            let source_path = std::path::Path::new(product_path);
-            if source_path.exists() {
-                let dest_path = graph_data_path.join("products.json");
-                match tokio::fs::copy(source_path, &dest_path).await {
-                    Ok(_) => {
-                        log::info!("Copied product lifecycle data to graph-data tarball");
-                    }
-                    Err(e) => {
-                        log::warn!("Failed to copy product lifecycle data: {}", e);
-                        // Don't fail - continue without product data
-                    }
-                }
-            }
-        }
+        commons::copy_product_data(&io.parameters, &graph_data_path).await;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@cincinnati/src/plugins/internal/graph_builder/dkrv2_openshift_secondary_metadata_scraper/plugin.rs`
around lines 365 - 381, Product lifecycle data copying is duplicated across both
scraper plugins. Extract the shared lookup, existence check, copy, and logging
logic into a reusable async commons helper such as copy_product_data, then
replace the duplicate blocks in both scraper plugins with calls to it, passing
their parameters and graph-data destination directory.
graph-builder/src/product_lifecycle.rs (1)

140-198: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No backoff on repeated fetch failures.

On error the loop simply continues to the next poll_interval sleep with no backoff, so a persistently failing upstream API is retried at the same fixed cadence indefinitely. Consider a simple backoff (capped) on consecutive failures to reduce load on a struggling upstream.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graph-builder/src/product_lifecycle.rs` around lines 140 - 198, Implement
capped exponential backoff for consecutive failures in the product lifecycle
polling loop. Track consecutive fetch or save/validation failures around the
loop using the existing fetch logic, increase the delay after each failure up to
a maximum, and reset the failure count/backoff after a successful fetch and
save; use the backoff delay for the next retry while preserving the normal poll
interval on success.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@graph-builder/src/product_lifecycle.rs`:
- Around line 97-106: Update save_to_file to avoid the predictable shared path
and non-atomic File::create write: create a securely randomized tempfile using
tempfile::NamedTempFile, write and sync the JSON, then atomically persist/rename
it to the expected cincinnati-products.json path, handling cleanup and errors
appropriately.

---

Nitpick comments:
In
`@cincinnati/src/plugins/internal/graph_builder/dkrv2_openshift_secondary_metadata_scraper/plugin.rs`:
- Around line 365-381: Product lifecycle data copying is duplicated across both
scraper plugins. Extract the shared lookup, existence check, copy, and logging
logic into a reusable async commons helper such as copy_product_data, then
replace the duplicate blocks in both scraper plugins with calls to it, passing
their parameters and graph-data destination directory.

In `@graph-builder/src/config/settings.rs`:
- Around line 86-101: Default product lifecycle fetching to opt-in so existing
and disconnected deployments do not contact the external Red Hat API
unexpectedly. Update the default value for product_enabled in the settings
configuration from true to false, while preserving the existing API URL, polling
interval, and timeout defaults.

In `@graph-builder/src/graph.rs`:
- Around line 155-160: Change the visibility of State fields product_data,
product_etag, and product_data_path from pub to pub(crate), preserving access
for product_lifecycle.rs within the crate while preventing external direct
mutation and matching the encapsulation of the other State fields.
- Around line 241-257: Add a warning log in the product-data parameter
preparation block before or when `path.to_str()` returns `None`, identifying the
invalid UTF-8 path and explaining that `commons::PRODUCT_DATA_PARAM_KEY` was
omitted. Keep the existing insertion behavior for valid paths and use the
component’s existing logging facility.

In `@graph-builder/src/product_lifecycle.rs`:
- Around line 140-198: Implement capped exponential backoff for consecutive
failures in the product lifecycle polling loop. Track consecutive fetch or
save/validation failures around the loop using the existing fetch logic,
increase the delay after each failure up to a maximum, and reset the failure
count/backoff after a successful fetch and save; use the backoff delay for the
next retry while preserving the normal poll interval on success.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f5e2a557-dfda-456e-b123-f9efe1318dfa

📥 Commits

Reviewing files that changed from the base of the PR and between 6121cea and adcf7e8.

📒 Files selected for processing (11)
  • cincinnati/src/plugins/internal/graph_builder/dkrv2_openshift_secondary_metadata_scraper/plugin.rs
  • cincinnati/src/plugins/internal/graph_builder/github_openshift_secondary_metadata_scraper/plugin.rs
  • commons/src/lib.rs
  • graph-builder/src/config/cli.rs
  • graph-builder/src/config/file.rs
  • graph-builder/src/config/options.rs
  • graph-builder/src/config/settings.rs
  • graph-builder/src/graph.rs
  • graph-builder/src/lib.rs
  • graph-builder/src/main.rs
  • graph-builder/src/product_lifecycle.rs

Comment thread graph-builder/src/product_lifecycle.rs

@ankitathomas ankitathomas left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks solid, I'd say it should be okay to merge once the tests and the coderabbit comments are addressed for the tempfile.

Great work!

@racheljpg
racheljpg marked this pull request as ready for review July 15, 2026 14:07
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@cincinnati/src/plugins/internal/graph_builder/github_openshift_secondary_metadata_scraper/plugin.rs`:
- Around line 526-542: The archive refresh decision must also detect changes to
the file configured by PRODUCT_DATA_PARAM_KEY, rather than relying only on
secondary-metadata commit changes. Update the plugin’s refresh/build logic to
compare product-data freshness and regenerate products.json and the tarball when
it changes, while preserving existing behavior when neither source changes. Add
a regression test covering a product-data-only update.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d8a7864e-3ffe-4a49-ba8f-48f21fb21549

📥 Commits

Reviewing files that changed from the base of the PR and between adcf7e8 and 5a6afd6.

📒 Files selected for processing (11)
  • cincinnati/src/plugins/internal/graph_builder/dkrv2_openshift_secondary_metadata_scraper/plugin.rs
  • cincinnati/src/plugins/internal/graph_builder/github_openshift_secondary_metadata_scraper/plugin.rs
  • commons/src/lib.rs
  • graph-builder/src/config/cli.rs
  • graph-builder/src/config/file.rs
  • graph-builder/src/config/options.rs
  • graph-builder/src/config/settings.rs
  • graph-builder/src/graph.rs
  • graph-builder/src/lib.rs
  • graph-builder/src/main.rs
  • graph-builder/src/product_lifecycle.rs
🚧 Files skipped from review as they are similar to previous changes (9)
  • cincinnati/src/plugins/internal/graph_builder/dkrv2_openshift_secondary_metadata_scraper/plugin.rs
  • graph-builder/src/config/file.rs
  • graph-builder/src/product_lifecycle.rs
  • commons/src/lib.rs
  • graph-builder/src/config/options.rs
  • graph-builder/src/config/cli.rs
  • graph-builder/src/config/settings.rs
  • graph-builder/src/graph.rs
  • graph-builder/src/main.rs

@racheljpg
racheljpg force-pushed the teachcincinnati branch 2 times, most recently from 65e0673 to a415e48 Compare July 15, 2026 14:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
graph-builder/src/main.rs (2)

79-81: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Update all State::new callers.

State::new now requires product_data, product_etag, and product_data_path, but mock_state at Lines 241-249 still passes the old seven-argument form. The test target will not compile until those shared fields are initialized there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graph-builder/src/main.rs` around lines 79 - 81, Update the mock_state
State::new call to use the current signature by supplying initialized
product_data, product_etag, and product_data_path values alongside the existing
arguments. Ensure all State::new callers, especially mock_state, compile with
the shared product fields populated.

95-99: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Avoid using settings after moving it into the graph thread.

The move closure at Lines 87-90 captures settings by value, but this new block accesses settings.product_enabled, settings.product_api_url, and related fields afterward. Rust will reject this with a use-after-move error. Extract these values before spawning the graph thread or clone settings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graph-builder/src/main.rs` around lines 95 - 99, Move the product
configuration extraction before the graph thread’s move closure captures
settings, then use the extracted product_enabled, product_api_url,
product_poll_interval_secs, and product_timeout_secs values afterward. Do not
access settings after it has been moved into the graph thread.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@graph-builder/src/product_lifecycle.rs`:
- Line 187: Update the HTTP polling flow’s Ok((None, new_etag)) branch to set
PRODUCT_LIFECYCLE_LAST_FETCH to the current timestamp, matching the successful
JSON-response path while preserving the existing ETag handling.

---

Outside diff comments:
In `@graph-builder/src/main.rs`:
- Around line 79-81: Update the mock_state State::new call to use the current
signature by supplying initialized product_data, product_etag, and
product_data_path values alongside the existing arguments. Ensure all State::new
callers, especially mock_state, compile with the shared product fields
populated.
- Around line 95-99: Move the product configuration extraction before the graph
thread’s move closure captures settings, then use the extracted product_enabled,
product_api_url, product_poll_interval_secs, and product_timeout_secs values
afterward. Do not access settings after it has been moved into the graph thread.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: e16b81dd-d07f-4fae-af6e-ed3e9fbed5d9

📥 Commits

Reviewing files that changed from the base of the PR and between 5ec8753 and a415e48.

📒 Files selected for processing (4)
  • cincinnati/src/plugins/internal/graph_builder/dkrv2_openshift_secondary_metadata_scraper/plugin.rs
  • cincinnati/src/plugins/internal/graph_builder/github_openshift_secondary_metadata_scraper/plugin.rs
  • graph-builder/src/main.rs
  • graph-builder/src/product_lifecycle.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • cincinnati/src/plugins/internal/graph_builder/github_openshift_secondary_metadata_scraper/plugin.rs
  • cincinnati/src/plugins/internal/graph_builder/dkrv2_openshift_secondary_metadata_scraper/plugin.rs

Comment thread graph-builder/src/product_lifecycle.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
graph-builder/src/product_lifecycle.rs (2)

96-115: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use a private temp directory for product data.
graph-builder/src/product_lifecycle.rs:96-115 writes every instance to <TMPDIR>/cincinnati-products.json, so concurrent graph-builder processes can overwrite each other’s file and graph.rs may package the wrong product data. Persist products.json inside a per-task TempDir instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graph-builder/src/product_lifecycle.rs` around lines 96 - 115, Update
save_to_file to create a per-task tempfile::TempDir and place products.json
inside it, rather than using the shared system temporary directory and fixed
cincinnati-products.json path. Persist the temporary file within that private
directory and return the resulting path while keeping the existing write and
sync error handling.

72-91: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the decoded response body. response.text().await buffers the full gzip-decoded payload, so a large or malicious product API response can exhaust memory; stream it with a configurable maximum decoded size and reject overflow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@graph-builder/src/product_lifecycle.rs` around lines 72 - 91, Update the
response-body handling in the product lifecycle request flow to stream decoded
bytes incrementally instead of calling response.text().await. Enforce a
configurable maximum decoded body size, reject responses exceeding that limit,
and preserve the existing parsing and error behavior for bodies within the
limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@graph-builder/src/product_lifecycle.rs`:
- Around line 96-115: Update save_to_file to create a per-task tempfile::TempDir
and place products.json inside it, rather than using the shared system temporary
directory and fixed cincinnati-products.json path. Persist the temporary file
within that private directory and return the resulting path while keeping the
existing write and sync error handling.
- Around line 72-91: Update the response-body handling in the product lifecycle
request flow to stream decoded bytes incrementally instead of calling
response.text().await. Enforce a configurable maximum decoded body size, reject
responses exceeding that limit, and preserve the existing parsing and error
behavior for bodies within the limit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d82b0e91-2509-47d5-8f29-6c1389f3f3c9

📥 Commits

Reviewing files that changed from the base of the PR and between a415e48 and 01f1378.

📒 Files selected for processing (4)
  • cincinnati/src/plugins/internal/graph_builder/dkrv2_openshift_secondary_metadata_scraper/plugin.rs
  • cincinnati/src/plugins/internal/graph_builder/github_openshift_secondary_metadata_scraper/plugin.rs
  • graph-builder/src/main.rs
  • graph-builder/src/product_lifecycle.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • cincinnati/src/plugins/internal/graph_builder/github_openshift_secondary_metadata_scraper/plugin.rs
  • graph-builder/src/main.rs
  • cincinnati/src/plugins/internal/graph_builder/dkrv2_openshift_secondary_metadata_scraper/plugin.rs

@racheljpg

Copy link
Copy Markdown
Author

Hello! Now that all tests are passing I did some testing to make sure this PR is doing what we want.
I started the graph-builder locally in a podman container and queried it:

Results from the logs:
  [INFO  graph_builder::product_lifecycle] Starting product lifecycle fetcher: api_url=https://access.redhat.com/product-life-cycles/api/v2/products, poll_interval=3600s, timeout=30s
  [INFO  graph_builder::product_lifecycle] Fetched product lifecycle data (1573540 bytes), etag=Some("W/\"1802a4-VHx4UFXEBhcY4N3kGsbdvLWPwUU\"")
  [INFO  graph_builder::product_lifecycle] Saved product lifecycle data to "/tmp/cincinnati-products.json"

This looks good!

Also queried the metrics endpoint to make sure the new metrics were being populated, and they are, under the cincinnato_gb_ prefix:

Results:
  cincinnati_gb_product_lifecycle_fetches_total 1
  cincinnati_gb_product_lifecycle_errors_total 0
  cincinnati_gb_product_lifecycle_not_modified_total 0
  cincinnati_gb_product_lifecycle_last_successful_fetch_timestamp 1784205912

If anyone with some more experience than me with Cincinnati has any other testing suggestions, I am happy to hear them :)
But with the above testing and the fact that all tests are passing now on the PR, I think this is in a good place.
Thanks!

@racheljpg

Copy link
Copy Markdown
Author

Thanks for your previous review @ankitathomas - would appreciate if you had the chance to look again now that the tests are passing and I've done some testing too :) thanks!

@racheljpg

Copy link
Copy Markdown
Author

Had to rebase to merge two commits together so the tests will have to run again

@racheljpg

Copy link
Copy Markdown
Author

/retest

@ankitathomas ankitathomas left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label Jul 17, 2026
@openshift-ci

openshift-ci Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: ankitathomas, racheljpg
Once this PR has been reviewed and has the lgtm label, please assign pratikmahajan for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@jhadvig jhadvig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@racheljpg hey, nice work on this! Left a few suggestions below, mostly cleanup stuff. Happy to discuss any of them.

1. product_data and product_etag don't need to live on State

Both product_data (the full JSON body) and product_etag are only read/written inside product_lifecycle::run() - nothing else in the codebase touches them. Only product_data_path is actually shared (read in graph.rs to pass the path to plugins).

These two can be local variables inside the run() function instead of Arc<RwLock<>> fields on the shared State struct. That drops State::new from 10 params to 8 and removes two unnecessary locks.

// product_lifecycle.rs – run()
let mut last_etag: Option<String> = None;

// ... in the polling loop:
// instead of: let last_etag = state.product_etag.read().clone();
// just use the local `last_etag` directly

// on success:
last_etag = new_etag;
*state.product_data_path.write() = Some(path.clone());

// on 304:
if let Some(etag) = new_etag {
    last_etag = Some(etag);
}

2. Duplicated product copy logic in both scrapers

The 15-line copy block in dkrv2_openshift_secondary_metadata_scraper/plugin.rs and github_openshift_secondary_metadata_scraper/plugin.rs is essentially identical. Worth extracting to a shared helper ... something like:

// commons/src/lib.rs (or graph_builder commons)
pub async fn copy_product_data_if_available(
    io: &cincinnati::plugins::InternalIO,
    dest_dir: &std::path::Path,
) {
    if let Some(product_path) = io.parameters.get(PRODUCT_DATA_PARAM_KEY) {
        let source_path = std::path::Path::new(product_path);
        if source_path.exists() {
            let dest_path = dest_dir.join("products.json");
            match tokio::fs::copy(source_path, &dest_path).await {
                Ok(_) => log::info!("Copied product lifecycle data to graph-data tarball"),
                Err(e) => log::warn!("Failed to copy product lifecycle data: {}", e),
            }
        }
    }
}

Then both scrapers become a one-liner before create_tar.

3. Don't spawn the polling task when disabled

When product_enabled is false, the run() function sits in loop { sleep(86400) } forever.... occupying a tokio task slot doing nothing. Simpler to just not spawn it:

// main.rs
if product_enabled {
    let product_state = state.clone();
    tokio::spawn(async move {
        graph_builder::product_lifecycle::run(
            product_api_url,
            product_poll_interval_secs,
            product_timeout_secs,
            product_state,
        )
        .await
    });
} else {
    info!("Product lifecycle fetching is disabled");
}

Then run() can drop the enabled parameter and the disabled-branch entirely, and the return type doesn't need to be -> !.

4. Default product_enabled to false

Currently defaults to true in settings.rs, which means existing deployments will start hitting access.redhat.com on upgrade with no opt-in. Suggest defaulting to false so it's explicitly enabled in deployments that want it:

#[default(false)]
pub product_enabled: bool,

5. chrono usage

chrono::Utc::now().timestamp() is used once here and once in graph.rs. Both can use stdlib instead, which would let you eventually drop the chrono direct dependency from graph-builder/Cargo.toml. Not urgent, but since we're touching the area:

use std::time::{SystemTime, UNIX_EPOCH};

PRODUCT_LIFECYCLE_LAST_FETCH.set(
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs() as f64,
);

(Same pattern could be applied to graph.rs:309 in a follow-up.)

@openshift-ci openshift-ci Bot removed the lgtm Indicates that a PR is ready to be merged. label Jul 20, 2026
@openshift-ci

openshift-ci Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

New changes are detected. LGTM label has been removed.

@racheljpg
racheljpg force-pushed the teachcincinnati branch 2 times, most recently from e02604e to f367cef Compare July 20, 2026 16:16
@racheljpg

Copy link
Copy Markdown
Author

/retest

@racheljpg

Copy link
Copy Markdown
Author

Thanks Jakub, I implemented all your feedback and all the tests are passing again :)


GRAPH_LAST_SUCCESSFUL_REFRESH.set(chrono::Utc::now().timestamp() as i64);
GRAPH_LAST_SUCCESSFUL_REFRESH.set(
std::time::SystemTime::now()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's going on with this change away from chrono? We've been using chrono since 43f39e8 (#136), and while I like being able to use the stdlib and have one fewer dep to keep track of, I'd like to see some notes in the commit message explaining what this change is about. Was the use of a non-stdlib dep unneccessary from the start? Or has the stdlib grown functionality that obsoleted a previously-valid need for a dep? Or is this actually changing user-visible behavior? Or...?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! It doesn't change anything for the user, both these outputs produce the same Unix timestamp value.
I don't have any strong opinions on this at all. Do you think it's best that it's kept as it is rather than removing it?

Comment thread graph-builder/src/main.rs
// Product lifecycle fetcher
if product_enabled {
let product_state = Arc::new(state.clone());
tokio::spawn(async move {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparing with a few lines up where we're using thread::spawn, is there a reason to use tokio::spawn here? Is the expectation that graph::run is heavy (on memory?) and needs its own OS-level thread, while graph_builder::product_lifecycle::run is light, and can use... whatever Tokio is configured to use?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding here is that since this is an async function, tokio::spawn is used instead of graph::run, because graph::run is synchronous and would block the runtime. I'm new to this codebase though, so if that's incorrect/not an inefficient way to do things, let me know

// 304 Not Modified - data hasn't changed
PRODUCT_LIFECYCLE_NOT_MODIFIED.inc();
info!("Product lifecycle data not modified (304)");
return Ok((None, new_etag));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got nervous about "why would we have a changed etag here in a 304 response?", and found:

If a stored response is selected for update, the cache MUST:
...

  • use other header fields provided in the 304 (Not Modified) response to replace all instances of the corresponding header fields in the stored response.

So returning the new etag here is required, even if I don't understand why a 304 response would have a different etag.

Comment thread graph-builder/src/product_lifecycle.rs Outdated
/// Save product JSON to a temporary file
fn save_to_file(json: &str) -> Fallible<PathBuf> {
let temp_dir = std::env::temp_dir();
let final_path = temp_dir.join("cincinnati-products.json");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can diverge from copy_product_data_if_available's products.json target here, but personally, I don't see the need for using a new name. Can we just use products.json here too?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented!

Comment thread graph-builder/src/product_lifecycle.rs Outdated
api_url: &str,
last_etag: Option<&str>,
) -> Fallible<(Option<String>, Option<String>)> {
let mut request = client.get(api_url);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

products.json in copy_product_data_if_available assumes the response is JSON. Can we make that an explicit part of the request by setting Accept: application/json in our request?

@racheljpg racheljpg Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed now - thanks

.context("Failed to sync temporary file")?;

temp_file
.persist(&final_path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing to a temporary file and then atomically renaming it to a final path is a good pattern when the final path is consumed by other parallel readers. But in this save_to_file function, final_path is inside a temporary directory that's unique to this function, and the consumer blocks until save_to_file completes. So I don't see the upside to doing the write/sync/rename dance in this situation. Can't we just write (and maybe sync?).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But in this save_to_file function, final_path is inside a temporary directory that's unique to this function...

Ah, I'd misread. std::env::temp_dir(); is just giving us the standard temporary directory (like /tmp) and not a new temporary directory unique to this function.

... and the consumer blocks until save_to_file completes.

But this is still true, right? So the rename dance would be protecting us against... a later poll starting to write the file while an existing copy_product_data_if_available reader was copying it out into a graph-data directory? In that case, I'm on board with the write/sync/rename dance, but think we probably want to explain this situation in the commit message to make it more accessable to future developers.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct, it is to save us from reads during a write.
Do you think that leaving some relevant comments in the code is sufficient, or do you think it would be best to break the PR into smaller commits with commit messages? Personally I thought it was tidy to keep this all in one commit, but what are your thoughts?

@openshift-ci

openshift-ci Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@racheljpg: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants