Skip to content

HYPERFLEET-536 - feat: add condition subfield queries for selective Sentinel polling#71

Open
rafabene wants to merge 8 commits intoopenshift-hyperfleet:mainfrom
rafabene:HYPERFLEET-536
Open

HYPERFLEET-536 - feat: add condition subfield queries for selective Sentinel polling#71
rafabene wants to merge 8 commits intoopenshift-hyperfleet:mainfrom
rafabene:HYPERFLEET-536

Conversation

@rafabene
Copy link
Contributor

@rafabene rafabene commented Mar 6, 2026

Summary

  • Add support for querying condition subfields (last_updated_time, last_transition_time, observed_generation) with comparison operators (=, !=, <, <=, >, >=)
  • Enable Sentinel to selectively fetch only not-ready and stale-ready resources instead of fetching ALL resources every poll cycle
  • Work around TSL parser 3-part identifier limitation by preprocessing 4-part paths (status.conditions.Ready.last_updated_time) into encoded 3-part paths (status.conditions.Ready__last_updated_time)

New API query examples

# Not-ready resources
GET /api/hyperfleet/v1/clusters?search=status.conditions.Ready='False'

# Stale ready resources (ready but not updated in 30min)
GET /api/hyperfleet/v1/clusters?search=status.conditions.Ready='True' AND status.conditions.Ready.last_updated_time < '2026-03-06T14:00:00Z'

Related

  • Architecture docs PR: will be linked after creation
  • JIRA: HYPERFLEET-536

Test plan

  • Unit tests: TestConditionsNodeConverterSubfields (12 cases), TestPreprocessConditionSubfields (6 cases), TestHasConditionWithSubfields (3 cases), TestExtractConditionQueriesWithSubfields (3 cases)
  • Integration tests: TestSearchConditionSubfieldLastUpdatedTime, TestSearchConditionSubfieldCombinedWithStatus, TestSearchConditionSubfieldInvalidSubfield
  • All 462 unit tests pass
  • All 56 integration tests pass (1 pre-existing skip)
  • 0 lint issues

Summary by CodeRabbit

  • New Features

    • Query condition subfields (last_updated_time, last_transition_time, observed_generation) with full comparison operators; enforced 4096-char search limit.
    • Ready condition now flips to Not Ready when a spec changes to trigger reconcilation filtering.
  • Documentation

    • Added syntax, operators, examples, and NOT-query guidance for condition subfield queries.
  • Migrations

    • Added expression indexes to speed condition subfield queries.
  • Tests

    • Expanded unit and integration tests covering subfield queries, preprocessing, and spec-change behavior.

@openshift-ci
Copy link

openshift-ci bot commented Mar 6, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign rh-amarin 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

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8b634254-66ad-42a0-a71d-2f409aabc389

📥 Commits

Reviewing files that changed from the base of the PR and between 5d6436c and 8346325.

📒 Files selected for processing (13)
  • docs/search.md
  • pkg/dao/cluster.go
  • pkg/dao/conditions.go
  • pkg/dao/conditions_test.go
  • pkg/dao/node_pool.go
  • pkg/db/migrations/202603100001_add_conditions_subfield_indexes.go
  • pkg/db/migrations/migration_structs.go
  • pkg/db/sql_helpers.go
  • pkg/db/sql_helpers_test.go
  • pkg/services/generic.go
  • test/factories/clusters.go
  • test/factories/node_pools.go
  • test/integration/search_field_mapping_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • pkg/dao/conditions.go
  • pkg/db/migrations/migration_structs.go
  • pkg/dao/cluster.go
  • pkg/services/generic.go
  • pkg/dao/conditions_test.go

Walkthrough

Adds support for querying condition subfields (status.conditions..) by preprocessing 4-part paths into a 3-part form, mapping TSL operators to SQL operators, and converting subfield queries to SQL with appropriate casting for timestamptz and integers. Introduces detection helpers to reject NOT on condition queries, adds expression indexes and an immutable timestamptz wrapper via a migration, and flips Ready->False on spec changes by updating status_conditions. Tests, factories, and integration tests were extended to cover subfield queries and time-based behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Service as pkg/services/generic.go
    participant Preprocessor as pkg/db/sql_helpers.go (PreprocessConditionSubfields)
    participant Parser as TSL Parser
    participant Converter as pkg/db/sql_helpers.go (converters)
    participant DB as Database

    Client->>Service: Search request with query
    Service->>Service: Validate length (<=4096)
    Service->>Preprocessor: PreprocessConditionSubfields(query)
    Preprocessor-->>Service: Preprocessed query
    Service->>Parser: ParseTSL(preprocessed query)
    Parser-->>Service: AST
    Service->>Converter: Convert condition nodes
    alt condition subfield (4-part)
        Converter->>Converter: Validate operator & subfield
        Converter->>DB: SQL expression with casting (timestamptz/int)
    else condition status (3-part)
        Converter->>DB: JSON path SQL expression
    end
    DB-->>Service: Results
    Service-->>Client: Response
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Suggested Labels

lgtm, approved

Suggested Reviewers

  • yingzhanredhat
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature addition: condition subfield queries for selective Sentinel polling, which aligns with the core changes across query preprocessing, database converters, and integration tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/api-resources.md`:
- Around line 453-456: The documentation currently lists operators as
universally supported but the field status.conditions.<Type> only supports
equality in code (see conditionStatusConverter in pkg/db/sql_helpers.go); update
the docs to enumerate operator support by field category (e.g., equality-only
for status.conditions.<Type>, full comparison for numeric/time fields, in-list
for array/list fields, etc.), explicitly call out status.conditions.<Type> as
equality-only and map each operator to the corresponding field types so the API
contract matches the implementation.

In `@pkg/db/sql_helpers_test.go`:
- Around line 320-325: Tighten the negative-case assertions in
pkg/db/sql_helpers_test.go so that when tt.expectError is true you not only
assert err != nil but also verify the error message contains tt.errorContains;
update the block in the test (the branch checking tt.expectError and using the
err variable) to assert the error string contains tt.errorContains (e.g., via
strings.Contains(err.Error(), tt.errorContains) or your test helper like
require.ErrorContains) before returning so mismatched errors fail the test.

In `@pkg/db/sql_helpers.go`:
- Around line 172-173: PreprocessConditionSubfields currently applies
conditionSubfieldPattern.ReplaceAllString to the whole query and thus mutates
text inside quoted string literals; change it to only run the replacement on
non-quoted identifier tokens by scanning the input and skipping content inside
single and double quotes (handling escaped quotes), e.g. split the string into
segments or use a simple state machine that toggles in-quote/out-of-quote and
apply conditionSubfieldPattern.ReplaceAllString only when out-of-quote; update
the PreprocessConditionSubfields function and keep reference to
conditionSubfieldPattern so only unquoted segments are transformed.
- Around line 323-331: The code currently takes r.Left as a string (rightStr)
and builds a ::timestamptz predicate that defers timestamp parsing to the DB;
instead validate rightStr before building the query: attempt to parse rightStr
with the expected timezone-aware layouts (e.g. RFC3339 / acceptable formats)
using time.Parse/ParseInLocation, and if parsing fails return
errors.BadRequest("invalid timestamp for condition subfield '%s': %v", subfield,
parseErr); only when parsing succeeds proceed to construct the query string
(using sqlOp, jsonPath, subfield) and return sq.Expr(..., rightStr), nil so
invalid literals are rejected with a 400.
- Around line 336-345: The code currently casts r.Left (rightVal float64)
directly to int before building the SQL expression (sq.Expr), which silently
truncates fractions and can overflow PostgreSQL INTEGER; update the validation
around r.Left (rightVal) to reject non-integer values (e.g., require rightVal ==
math.Trunc(rightVal) or use math.Modf) and reject values outside the 32-bit
signed int range (<= -2147483648 or >= 2147483648), returning errors.BadRequest
with a clear message mentioning subfield (e.g., "expected integer within 32-bit
range for condition subfield '%s'"); only after those checks convert to an int
(or int32) and pass that to sq.Expr(query, jsonPath, subfield, intSafe) so
observed_generation and similar fields are validated and not truncated or
overflowed.

In `@test/factories/clusters.go`:
- Around line 165-166: The DB session is created using context.Background()
(dbFactory.New) which bypasses request-scoped context; update the helper to
accept a context parameter and use that when creating the session (pass the
existing ctx into the helper instead of context.Background()), and replace the
dbFactory.New(...) call with the context-aware session creation (use
db.NewContext(ctx) or dbFactory.NewContext/dbFactory.New(ctx) per your DB
factory API) so the call that sets err = dbSession.Model(cluster).Update(...)
uses a request-scoped context; locate the dbFactory.New invocation in
clusters.go and the helper signature to add a ctx parameter and thread it
through.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f24f421-2616-4edd-8e19-5b68594f5372

📥 Commits

Reviewing files that changed from the base of the PR and between 97284d9 and 22b4b97.

📒 Files selected for processing (6)
  • docs/api-resources.md
  • pkg/db/sql_helpers.go
  • pkg/db/sql_helpers_test.go
  • pkg/services/generic.go
  • test/factories/clusters.go
  • test/integration/search_field_mapping_test.go

@rafabene
Copy link
Contributor Author

rafabene commented Mar 6, 2026

Related architecture docs PR: openshift-hyperfleet/architecture#99

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/db/sql_helpers.go (1)

182-202: Escaped quotes inside string literals are not handled.

The quote-aware state machine doesn't account for escaped quotes (e.g., 'it\'s' or "say \"hello\""). If a user provides a query with escaped quotes, the state machine will prematurely exit the quoted segment, potentially transforming text that should remain untouched.

This is a minor edge case since condition paths are unlikely to appear in user-provided string values with escaped quotes, but worth noting for completeness.

💡 Suggested fix to handle escaped quotes
 	for i := 0; i < len(search); i++ {
 		ch := search[i]
 		if inQuote {
+			// Skip escaped characters
+			if ch == '\\' && i+1 < len(search) {
+				i++ // Skip the next character
+				continue
+			}
 			if ch == quoteChar {
 				// Flush quoted segment as-is (no replacement)
 				result.WriteString(search[segStart : i+1])
 				segStart = i + 1
 				inQuote = false
 			}
 			continue
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/db/sql_helpers.go` around lines 182 - 202, The loop that scans the SQL
fragment in pkg/db/sql_helpers.go (using variables search, result, segStart,
inQuote, quoteChar and conditionSubfieldPattern) doesn't treat escaped quotes as
part of a quoted literal; update the state machine so that when encountering a
quote character (ch == quoteChar) you first check whether it is escaped
(preceded by an odd number of backslashes) and only close the quoted segment if
it is not escaped, otherwise treat it as part of the quote; ensure flushing
logic for quoted and unquoted segments (the result.WriteString and
pattern.ReplaceAllString calls around segStart and i) remains correct when
skipping escaped quotes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/db/sql_helpers.go`:
- Around line 182-202: The loop that scans the SQL fragment in
pkg/db/sql_helpers.go (using variables search, result, segStart, inQuote,
quoteChar and conditionSubfieldPattern) doesn't treat escaped quotes as part of
a quoted literal; update the state machine so that when encountering a quote
character (ch == quoteChar) you first check whether it is escaped (preceded by
an odd number of backslashes) and only close the quoted segment if it is not
escaped, otherwise treat it as part of the quote; ensure flushing logic for
quoted and unquoted segments (the result.WriteString and
pattern.ReplaceAllString calls around segStart and i) remains correct when
skipping escaped quotes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d567d38e-d25f-4a40-bf2b-7ee79e522713

📥 Commits

Reviewing files that changed from the base of the PR and between 22b4b97 and 1dcda49.

📒 Files selected for processing (3)
  • docs/api-resources.md
  • pkg/db/sql_helpers.go
  • pkg/db/sql_helpers_test.go

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
pkg/dao/conditions_test.go (1)

11-113: Use the repo's Gomega test style in these new DAO tests.

These cases introduce raw t.Fatalf / t.Errorf assertions instead of the usual RegisterTestingT(t) + Gomega matchers, so they don't follow the test pattern used elsewhere in the repo.

As per coding guidelines: **/*_test.go: Test patterns: use Gomega assertions with RegisterTestingT(t).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/dao/conditions_test.go` around lines 11 - 113, The tests
TestResetReadyConditionOnSpecChange,
TestResetReadyConditionOnSpecChange_EmptyConditions and
TestResetReadyConditionOnSpecChange_InvalidJSON use raw t.Fatalf/t.Errorf;
update them to the repo's Gomega style by calling gomega.RegisterTestingT(t) at
the start of each subtest or test and replace t.Fatalf/t.Errorf checks with
appropriate gomega.Expect(...) matchers (e.g. Expect(err).ToNot(HaveOccurred()),
Expect(result).To(BeNil()) or Expect(string(result)).To(Equal(string(input))),
Expect(cond.Status).To(Equal(api.ConditionFalse)) /
To(Equal(api.ConditionTrue)), Expect(cond.Reason).ToNot(BeNil()) and
Expect(*cond.Reason).To(Equal("SpecChanged")), and
Expect(cond.LastTransitionTime.Equal(now)).To(BeTrue()) when asserting behavior
of resetReadyConditionOnSpecChange).
pkg/db/sql_helpers_test.go (1)

10-146: Use the repo’s Gomega test pattern in these new cases.

These additions keep extending the legacy t.Errorf/t.Fatalf style instead of the repository-standard assertion flow. Please switch the new tests to Gomega and call RegisterTestingT(t) in each test entry point so this file stays aligned with the rest of the suite.

As per coding guidelines, "Test patterns: use Gomega assertions with RegisterTestingT(t)".

Also applies to: 148-577

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/db/sql_helpers_test.go` around lines 10 - 146, Replace the legacy
t.Errorf/t.Fatalf style in TestConditionsNodeConverterStatus with the
repo-standard Gomega pattern: call RegisterTestingT(t) at the start of each
t.Run subtest, import and use Gomega matchers
(Expect/To/ContainSubstring/BeNil/Equal/HaveLen etc.) for all assertions (e.g.,
SQL, args, errors) instead of t.Errorf checks; update assertions around the call
to conditionsNodeConverter and the sqlizer.ToSql() results to use Expect, and
apply the same Gomega conversion to the other tests in this file (lines 148-577)
that exercise conditionsNodeConverter and related tsl.Node-based cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/search.md`:
- Around line 131-151: The docs fail to warn that using NOT with condition
subfield expressions (e.g., NOT status.conditions.<Type>.last_updated_time <
'...') is rejected; update the "Supported Operators" /
`status.conditions.<Type>` note to explicitly state that the NOT operator is not
supported for condition subfield queries and will result in a 400 error, and
suggest using positive expressions combined with other boolean operators
(AND/OR) or restructuring the query instead; mention the specific symbol forms
`status.conditions.<Type>` and examples like
`status.conditions.Ready.last_updated_time` to make the limitation discoverable.

In `@pkg/db/migrations/202603100001_add_conditions_subfield_indexes.go`:
- Around line 32-52: Expression indexes idx_clusters_ready_last_updated_time and
idx_node_pools_ready_last_updated_time embed literal JSONPath/field names that
won't match query builder bind-parameter predicates; instead add a
STORED/generated column on clusters and node_pools (e.g.,
ready_last_updated_time) computed with
immutable_timestamptz(jsonb_path_query_first(status_conditions, '$[*] ? (@.type
== "Ready")') ->> 'last_updated_time') cast to timestamptz, create a BTREE index
on that generated column, and update any queries to filter/order by the new
ready_last_updated_time column so the planner can use the index; ensure you
reference the generated column name and the original
status_conditions/jsonb_path_query_first expression in the migration change.

In `@pkg/db/sql_helpers.go`:
- Around line 436-444: The NOT-operator guard only inspects the direct child
node and misses conditions deeper in the NOT subtree; update the check in the
block where you test n.Func == tsl.NotOp to recursively scan the entire NOT
subtree using hasCondition instead of only testing the immediate child (i.e.,
call hasCondition on the full left subtree node rather than relying on a
single-level type assertion). Keep the error return unchanged; refer to symbols
n.Func, tsl.NotOp, n.Left and hasCondition to locate and modify the logic so any
condition anywhere under a NOT triggers the BadRequest.

---

Nitpick comments:
In `@pkg/dao/conditions_test.go`:
- Around line 11-113: The tests TestResetReadyConditionOnSpecChange,
TestResetReadyConditionOnSpecChange_EmptyConditions and
TestResetReadyConditionOnSpecChange_InvalidJSON use raw t.Fatalf/t.Errorf;
update them to the repo's Gomega style by calling gomega.RegisterTestingT(t) at
the start of each subtest or test and replace t.Fatalf/t.Errorf checks with
appropriate gomega.Expect(...) matchers (e.g. Expect(err).ToNot(HaveOccurred()),
Expect(result).To(BeNil()) or Expect(string(result)).To(Equal(string(input))),
Expect(cond.Status).To(Equal(api.ConditionFalse)) /
To(Equal(api.ConditionTrue)), Expect(cond.Reason).ToNot(BeNil()) and
Expect(*cond.Reason).To(Equal("SpecChanged")), and
Expect(cond.LastTransitionTime.Equal(now)).To(BeTrue()) when asserting behavior
of resetReadyConditionOnSpecChange).

In `@pkg/db/sql_helpers_test.go`:
- Around line 10-146: Replace the legacy t.Errorf/t.Fatalf style in
TestConditionsNodeConverterStatus with the repo-standard Gomega pattern: call
RegisterTestingT(t) at the start of each t.Run subtest, import and use Gomega
matchers (Expect/To/ContainSubstring/BeNil/Equal/HaveLen etc.) for all
assertions (e.g., SQL, args, errors) instead of t.Errorf checks; update
assertions around the call to conditionsNodeConverter and the sqlizer.ToSql()
results to use Expect, and apply the same Gomega conversion to the other tests
in this file (lines 148-577) that exercise conditionsNodeConverter and related
tsl.Node-based cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2c7fdb8b-7e01-42d7-9000-155854d94403

📥 Commits

Reviewing files that changed from the base of the PR and between 1dcda49 and 5d6436c.

📒 Files selected for processing (13)
  • docs/search.md
  • pkg/dao/cluster.go
  • pkg/dao/conditions.go
  • pkg/dao/conditions_test.go
  • pkg/dao/node_pool.go
  • pkg/db/migrations/202603100001_add_conditions_subfield_indexes.go
  • pkg/db/migrations/migration_structs.go
  • pkg/db/sql_helpers.go
  • pkg/db/sql_helpers_test.go
  • pkg/services/generic.go
  • test/factories/clusters.go
  • test/factories/node_pools.go
  • test/integration/search_field_mapping_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/integration/search_field_mapping_test.go

@rafabene
Copy link
Contributor Author

This PR, the ticket and Architecture PR are totally aligned.

…entinel polling

Support querying condition subfields (last_updated_time, last_transition_time,
observed_generation) with comparison operators, enabling Sentinel to selectively
fetch only not-ready and stale-ready resources instead of all resources.
- Fix operator docs to show per-field-type support table
- Fix weak error assertions in condition converter tests
- Make PreprocessConditionSubfields quote-aware to avoid
  mutating string literals inside quoted segments
- Add RFC3339 timestamp validation for time subfield queries
- Add integer truncation and int32 overflow validation
- Add test cases for new validations and quote-aware preprocessing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants