Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fd2497d
HYPERFLEET-538 - feat: CEL-based condition mapping engine
ldornele Jul 27, 2026
2075280
HYPERFLEET-538 - feat: CEL-based condition mapping engine
ldornele Jul 27, 2026
52f40dd
HYPERFLEET-538 - feat: CEL-based condition mapping engine
ldornele Jul 27, 2026
da56371
HYPERFLEET-538 - feat: CEL-based condition mapping engine
ldornele Jul 27, 2026
b2f4f69
HYPERFLEET-538 - feat: CEL-based condition mapping engine
ldornele Jul 27, 2026
60d56ed
HYPERFLEET-538 - feat: CEL-based condition mapping engine
ldornele Jul 27, 2026
f3d343e
HYPERFLEET-538 - feat: CEL-based condition mapping engine
ldornele Jul 27, 2026
5031fac
HYPERFLEET-538 - feat: CEL-based condition mapping engine
ldornele Jul 28, 2026
6d1f345
HYPERFLEET-538 - feat: CEL-based condition mapping engine
ldornele Jul 28, 2026
2445c88
HYPERFLEET-538 - refactor: address code review feedback
ldornele Jul 30, 2026
a869327
HYPERFLEET-538 - fix: enforce CEL error rollback per design doc
ldornele Jul 30, 2026
cc1ece4
HYPERFLEET-538 - fix: add missing db.MarkForRollback on CEL error
ldornele Jul 30, 2026
c7ad824
HYPERFLEET-538 - fix: move Gomega assertion out of goroutine
ldornele Jul 30, 2026
49bea48
HYPERFLEET-538 - fix: propagate field validation error instead of sil…
ldornele Jul 30, 2026
d120775
HYPERFLEET-538 - fix: address code review feedback
ldornele Jul 30, 2026
b2437ac
HYPERFLEET-538 - perf: avoid map allocation when hasUnknown=true
ldornele Jul 30, 2026
9d80146
HYPERFLEET-538 - test: add coverage for reason/message expression errors
ldornele Jul 30, 2026
a202acc
HYPERFLEET-538 - feat: remove redundant tags, fix review comments
tirthct Aug 6, 2026
7a2a728
HYPERFLEET-538 - feat: remaining small fix
tirthct Aug 6, 2026
1222d5a
HYPERFLEET-538 - feat: fix coderabbit suggestions
tirthct Aug 6, 2026
35ecab5
HYPERFLEET-538 - feat: fix coderabbit and Dmitrii's suggestions
tirthct Aug 7, 2026
b97cc39
HYPERFLEET-538 - feat: fix linter
tirthct Aug 7, 2026
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
6 changes: 5 additions & 1 deletion cmd/hyperfleet-api/container/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import (

func (c *Container) ResourceService() services.ResourceService {
if c.resourceService == nil {
c.resourceService = services.NewResourceService(
svc, err := services.NewResourceService(
c.ResourceDao(),
c.ResourceLabelDao(),
c.AdapterStatusDao(),
c.ResourceConditionDao(),
c.GenericService(),
)
if err != nil {
panic("failed to create resource service: " + err.Error())
}
c.resourceService = svc
Comment on lines +9 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 '\bResourceService\s*\(\)' --glob '*.go' .
rg -n -C 8 '\b(New|Initialize|Run|Start|Serve|ListenAndServe)[A-Za-z0-9_]*\s*\(' --glob '*.go' cmd

Repository: openshift-hyperfleet/hyperfleet-api

Length of output: 50392


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- services.go ---'
cat -n cmd/hyperfleet-api/container/services.go
printf '%s\n' '--- container definition and constructors ---'
rg -n -C 6 'type Container struct|func NewContainer|resourceService|NewResourceService' cmd/hyperfleet-api/container pkg
printf '%s\n' '--- serve startup sequence ---'
sed -n '40,190p' cmd/hyperfleet-api/servecmd/cmd.go
printf '%s\n' '--- relevant tests ---'
sed -n '1,100p' cmd/hyperfleet-api/container/container_test.go
printf '%s\n' '--- all ResourceService call sites, compact ---'
rg -n '\b[A-Za-z0-9_]*ResourceService\s*\(' --glob '*.go' .

Repository: openshift-hyperfleet/hyperfleet-api

Length of output: 33261


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ResourceService constructor ---'
sed -n '1,115p' pkg/services/resource.go
printf '%s\n' '--- registry and CEL-related construction ---'
rg -n -C 8 'CEL|cel|condition|MustGet|NewResourceService' pkg cmd/hyperfleet-api --glob '*.go'
printf '%s\n' '--- startup call-order verifier ---'
python3 - <<'PY'
from pathlib import Path

source = Path("cmd/hyperfleet-api/servecmd/cmd.go").read_text()
resource = source.index("ctr.ResourceService()")
server = source.index("go apiServer.Start()")
print({
    "resource_service_called_before_api_server_start": resource < server,
    "resource_service_line": source[:resource].count("\n") + 1,
    "api_server_start_line": source[:server].count("\n") + 1,
})

container = Path("cmd/hyperfleet-api/container/services.go").read_text()
print({
    "constructor_error_panics": 'panic("failed to create resource service: " + err.Error())' in container,
    "resource_service_assignment_is_unsynchronized": (
        "if c.resourceService == nil" in container
        and "c.resourceService = svc" in container
        and "sync.Once" not in container
        and "sync.Mutex" not in container
    ),
})
PY

Repository: openshift-hyperfleet/hyperfleet-api

Length of output: 50392


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

source = Path("cmd/hyperfleet-api/servecmd/cmd.go").read_text()
resource_pos = source.index("ctr.ResourceService()")
server_pos = source.index("go apiServer.Start()")

container = Path("cmd/hyperfleet-api/container/services.go").read_text()

print("resource_service_before_api_server_start:", resource_pos < server_pos)
print("resource_service_line:", source[:resource_pos].count("\n") + 1)
print("api_server_start_line:", source[:server_pos].count("\n") + 1)
print("constructor_error_panics:",
      'panic("failed to create resource service: " + err.Error())' in container)
print("synchronization_primitive_present:",
      any(token in container for token in ("sync.Once", "sync.Mutex", "sync.RWMutex")))
PY

printf '%s\n' '--- non-test ResourceService accessor call sites ---'
rg -n '\b[A-Za-z0-9_]*ResourceService\s*\(' --glob '*.go' --glob '!**/*_test.go' \
  cmd pkg test

Repository: openshift-hyperfleet/hyperfleet-api

Length of output: 922


Propagate NewResourceService errors instead of panicking.

runServe already calls ResourceService() before starting the API server, so invalid CEL mappings fail during startup. Change the accessor to return the constructor error and handle it in runServe; do not convert configuration errors into panic (CWE-703).

🤖 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 `@cmd/hyperfleet-api/container/services.go` around lines 9 - 19, Change
ResourceService to return both the service and the NewResourceService error
instead of panicking when construction fails. Update runServe to receive and
handle that error before starting the API server, preserving the startup failure
flow without converting configuration errors into a panic.

Source: Path instructions

}
return c.resourceService
}
Expand Down
42 changes: 41 additions & 1 deletion configs/config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ health:
# Entity Registration
# Generic resource types registered at startup. Each entry auto-generates
# REST endpoints, spec validation, and delete policies.
# See docs/config.md for condition mapping documentation.
entities:
- kind: Cluster
plural: clusters
Expand All @@ -115,6 +116,26 @@ entities:
name_max_len: 53
require_spec_schema: true

# CEL-based condition mapping rules
# conditions:
# Example: Expose Landing Zone namespace readiness
# - type: LandingZoneReady
# when:
# expression: 'statuses.exists(s, s.adapter == "landing-zone-adapter" && s.conditions.exists(c, c.type == "NamespaceReady"))'
Comment thread
tirthct marked this conversation as resolved.
# output:
# status:
# expression: |
# statuses.filter(s, s.adapter == "landing-zone-adapter")[0]
# .conditions.filter(c, c.type == "NamespaceReady")[0].status
# reason:
# expression: |
# statuses.filter(s, s.adapter == "landing-zone-adapter")[0]
# .conditions.filter(c, c.type == "NamespaceReady")[0].reason
# message:
# expression: |
# "Landing zone: " + statuses.filter(s, s.adapter == "landing-zone-adapter")[0]
# .conditions.filter(c, c.type == "NamespaceReady")[0].message
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- kind: NodePool
plural: nodepools
parent_kind: Cluster
Expand All @@ -126,6 +147,26 @@ entities:
name_max_len: 15
require_spec_schema: true

# CEL-based condition mapping rules
# conditions:
# Example: Expose Validation quota check status
# - type: QuotaValid
# when:
# expression: 'statuses.exists(s, s.adapter == "validation-adapter" && s.conditions.exists(c, c.type == "QuotaSufficient"))'
# output:
# status:
# expression: |
# statuses.filter(s, s.adapter == "validation-adapter")[0]
# .conditions.filter(c, c.type == "QuotaSufficient")[0].status
# reason:
# expression: |
# statuses.filter(s, s.adapter == "validation-adapter")[0]
# .conditions.filter(c, c.type == "QuotaSufficient")[0].reason
# message:
# expression: |
# statuses.filter(s, s.adapter == "validation-adapter")[0]
# .conditions.filter(c, c.type == "QuotaSufficient")[0].message

- kind: Channel
plural: channels
spec_schema_name: ChannelSpec
Expand All @@ -142,7 +183,6 @@ entities:
plural: wifconfigs
spec_schema_name: WifConfigSpec


# ----------------------------------------------------------------------------
# Configuration Priority (highest to lowest):
# 1. Command-line flags (e.g., --server-host=0.0.0.0 --server-port=8000)
Expand Down
73 changes: 73 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,79 @@ health:

</details>

<details>
<summary><b>Condition Mapping (CEL)</b> (click to expand)</summary>

Each entity can define CEL-based condition mapping rules that expose
provider-specific adapter conditions in the public `status.conditions` array.

**Lifecycle:**

- Rules are compiled at startup (fail-fast). Invalid CEL expressions prevent API startup.
- Evaluation happens during status aggregation. Adapter entries with any `Unknown` condition are excluded entirely.

**Reserved condition types** (cannot be overridden by mapping):

- `Reconciled`
- `LastKnownReconciled`
- Per-adapter synthesized types (auto-generated from `required_adapters`).
Example: `validation` adapter produces `ValidationSuccessful` condition type.

**CEL Context Variables:**

| Variable | Type | Description |
|----------|------|-------------|
| `statuses` | `list(dyn)` | Array of adapter statuses. Each entry: `adapter` (string), `observed_generation` (number), `conditions` (array), `data` (map) |
| `resource` | `dyn` | Full cluster/nodepool object as map (sensitive fields masked) |

**Custom CEL Functions:**

| Function | Description |
|----------|-------------|
| `toJson(value)` | Marshal any value to a JSON string |
| `dig(target, "dot.path")` | Safe nested navigation returning `null` on missing keys |

**Security:** Adapter data fields matching sensitive patterns (`password`, `secret`, `token`,
`auth`, `private`, `connection`, `cert`, `credential`, etc.) are automatically masked with
`***REDACTED***` before CEL evaluation. This prevents credential leakage in public
condition messages/reasons. See `pkg/util/mask_sensitive.go` for the full pattern list.

**Field Length Constraints:**

| Field | Limit | Behavior |
|-------|-------|----------|
| `type` | 128 bytes | Validation error (prevents startup) |
| `reason` | 256 bytes | Truncated if exceeded |
| `message` | 2048 bytes | Truncated if exceeded |

**Example:**

```yaml
entities:
- kind: Cluster
conditions:
- type: LandingZoneReady
when:
expression: |
statuses.exists(s, s.adapter == "landing-zone-adapter"
&& s.conditions.exists(c, c.type == "NamespaceReady"))
output:
status:
expression: |
statuses.filter(s, s.adapter == "landing-zone-adapter")[0]
.conditions.filter(c, c.type == "NamespaceReady")[0].status
reason:
expression: |
statuses.filter(s, s.adapter == "landing-zone-adapter")[0]
.conditions.filter(c, c.type == "NamespaceReady")[0].reason
message:
expression: |
"Landing zone: " + statuses.filter(s, s.adapter == "landing-zone-adapter")[0]
.conditions.filter(c, c.type == "NamespaceReady")[0].message
```

</details>

---

## Complete Reference
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/go-gormigrate/gormigrate/v2 v2.1.6
github.com/go-playground/validator/v10 v10.30.3
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/cel-go v0.29.0
github.com/google/uuid v1.6.0
github.com/jinzhu/inflection v1.0.0
github.com/lib/pq v1.12.3
Expand Down Expand Up @@ -42,6 +43,8 @@ require (
)

require (
cel.dev/expr v0.25.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
Expand All @@ -51,6 +54,7 @@ require (
go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.44.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
golang.org/x/time v0.15.0 // indirect
)

Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
Expand All @@ -15,6 +17,8 @@ github.com/MicahParks/keyfunc/v3 v3.8.1/go.mod h1:LcorJ0sz2tZGvgZqIfaeyLkJmM+kxI
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
Expand Down Expand Up @@ -100,6 +104,8 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/cel-go v0.29.0 h1:fEG+Ja3YRwNOqnQxTyJwoByAUAvTuxUGiro/jhrm4F4=
github.com/google/cel-go v0.29.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
Expand Down Expand Up @@ -291,6 +297,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
Expand Down
Loading