Skip to content

[HYPERSHELL-259] feat: reconcile gateway version for CLI installation - #210

Open
jsell-rh wants to merge 14 commits into
mainfrom
feature/gateway-version-install
Open

[HYPERSHELL-259] feat: reconcile gateway version for CLI installation#210
jsell-rh wants to merge 14 commits into
mainfrom
feature/gateway-version-install

Conversation

@jsell-rh

@jsell-rh jsell-rh commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Jira: HYPERSHELL-259

Summary

  • Read the runtime version from each ready gateway through its internal health endpoint.
  • Reconcile a dedicated ClusterIP Service and a controller-only NetworkPolicy for health access.
  • Store the complete reported value in the read-only gateway_version API field through an atomic field-owned write.
  • Use an initial list, periodic resync, four bounded workers, and one serial pass for each gateway.
  • Add bounded timeouts, reject redirects, and repair drift in the owned health-access resources.
  • Build the OpenShell installation command without an installed OpenShell CLI or jq.
  • Remove the first - and all following text from the installer version. For example, use v0.0.109 when the gateway reports v0.0.109-rh9a8f8.
  • Keep the full reported version in the API and keep the installer URL on the NVIDIA OpenShell main branch.
  • Poll for a missing reconciled version for a bounded period and remove duplicate installation text.

Validation

  • make check
  • go test ./... in components/control-plane
  • go test -race ./internal/gateway ./internal/reconciler in components/control-plane
  • go test ./... in components/sdk-go
  • pnpm --filter @openshift-online/hypershell-gateway-management-ui check
  • pnpm --filter @openshift-online/hypershell-sdk check
  • Full web console checks, production build, and Storybook build
  • API server and control-plane lint checks
  • Base and OpenShift Kustomize render checks

The API integration harness currently stops before the tests because its shared migration setup reports pq: got 2 parameters but the statement requires 1. The focused API compile check and an isolated PostgreSQL test for the guarded version update pass.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Team

Run ID: b2907115-a099-45a7-b263-74c9b3200709

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@jsell-rh jsell-rh changed the title feat(web-console): add gateway-matched CLI installation [HYPERSHELL-259] feat(web-console): add gateway-matched CLI installation Aug 26, 2026
@jsell-rh
jsell-rh added this pull request to the merge queue Aug 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 27, 2026
@jsell-rh

jsell-rh commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Amber review

Status: Complete

Verdict

This is a clean, well-tested web-console change that adds a gateway-version-matched OpenShell CLI install command, reorders the Connection tab so installation precedes the combined registration + provider block, and removes the service-account shortcut - all reflected in the spec and covered by additive tests. I recommend it for merge after two Minor cleanups; the main action item is cross-PR coordination with #208, which restructures the same Connection tab and still consumes the pre-rename EditableCommand/buildSetupScript API this PR replaces.

Amber Analysis

The refactor of EditableCommand -> CommandBlock (making marker/label/value props optional so a static command can reuse the same highlighted, copyable block) is a good consolidation, and the user-supplied endpoint is correctly passed through shellArgument() before it lands in the generated command (with a test that injects a space). Test diff scrutiny came back clean: the modified assertions track real behavior changes (prereq copy, step ordering, service-account link removal) and are all mirrored in the spec and PR description, not silent contract removals - and the endpoint-undefined readiness case that moved out of gateway-connection-steps.test.tsx is still covered by the unit test in gateway-connections.test.ts.

Findings

[Minor] Translator description contradicts the rendered order - Content / i18n
connectionInstallPrereq's description says the note is "shown after gateway registration and before provider setup" (messages.ts:102, en.json:156), but the component renders the prerequisite alert (and its install command) before the combined registration+provider block, and the test asserts exactly that (installationIndex < registrationIndex). The description gives translators the wrong placement context; align it with the actual "before the one-time setup commands" order the spec mandates.

[Minor] install.sh is fetched from main while the CLI version is pinned - Supply chain (Confidence: Low)
buildOpenShellInstallCommand pins OPENSHELL_VERSION to the gateway's reported version but pulls the installer itself from .../NVIDIA/OpenShell/main/install.sh (gateway-connections.ts:80-81,104). A future breaking change to install.sh on main could desync from the pinned version. This matches the command verbatim added to architecture.spec.md in this same PR, so it is intentional - flagging only so maintainers consciously accept pulling the installer from an unpinned ref.

Cross-PR coordination

I reviewed the other open PRs in openshift-online/hypershell (#216, #214, #212, #211, #209, #208, #207, #206, #201, #200, #194, #189, #188, #185, #182, #179, #151, #150, #148, #135, #109, #75, #73). One material conflict:

No material conflict with the other open PRs. The dashboard PRs (#209/#214) and the deps/i18n PRs touch locales/en.json/messages.ts only additively (no competing design), #206 (hsctl login) is a separate Go CLI and does not compete with this UI's "register the gateway" copy change, and #216 (OpenShift Route console) changes how the control plane publishes the gateway endpoint but keeps the same endpoint field this UI consumes.

Findings Summary (ordered by severity, highest first)

  1. [Minor] Translator description for connectionInstallPrereq says "after gateway registration" but the alert renders before registration - Content / i18n (messages.ts L102, en.json L156)
  2. [Minor] Installer pulled from NVIDIA/OpenShell/main/install.sh (unpinned) while CLI version is pinned - Supply chain (gateway-connections.ts L80-81, L104)

Convention Checklist

Convention Result
Input validated / shell arguments quoted Pass
No secrets in logs or responses Pass
Reuse shared/PatternFly UI components (no duplicate component) Pass
Config separate from code (i18n messages, URLs) Pass
Test Diff Scrutiny (modified assertions justified, no hidden contract removal) Pass
Spec updated to match behavior Pass
Conventional commit messages Pass

@jsell-rh jsell-rh left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verdict

This is a clean, well-tested web-console change that adds a gateway-version-matched OpenShell CLI install command, reorders the Connection tab so installation precedes the combined registration + provider block, and removes the service-account shortcut - all reflected in the spec and covered by additive tests. I recommend it for merge after two Minor cleanups; the main action item is cross-PR coordination with #208, which restructures the same Connection tab and still consumes the pre-rename EditableCommand/buildSetupScript API this PR replaces.

Amber Analysis

The refactor of EditableCommand -> CommandBlock (making marker/label/value props optional so a static command can reuse the same highlighted, copyable block) is a good consolidation, and the user-supplied endpoint is correctly passed through shellArgument() before it lands in the generated command (with a test that injects a space). Test diff scrutiny came back clean: the modified assertions track real behavior changes (prereq copy, step ordering, service-account link removal) and are all mirrored in the spec and PR description, not silent contract removals - and the endpoint-undefined readiness case that moved out of gateway-connection-steps.test.tsx is still covered by the unit test in gateway-connections.test.ts.

Findings

[Minor] Translator description contradicts the rendered order - Content / i18n
connectionInstallPrereq's description says the note is "shown after gateway registration and before provider setup" (messages.ts:102, en.json:156), but the component renders the prerequisite alert (and its install command) before the combined registration+provider block, and the test asserts exactly that (installationIndex < registrationIndex). The description gives translators the wrong placement context; align it with the actual "before the one-time setup commands" order the spec mandates.

[Minor] install.sh is fetched from main while the CLI version is pinned - Supply chain (Confidence: Low)
buildOpenShellInstallCommand pins OPENSHELL_VERSION to the gateway's reported version but pulls the installer itself from .../NVIDIA/OpenShell/main/install.sh (gateway-connections.ts:80-81,104). A future breaking change to install.sh on main could desync from the pinned version. This matches the command verbatim added to architecture.spec.md in this same PR, so it is intentional - flagging only so maintainers consciously accept pulling the installer from an unpinned ref.

Cross-PR coordination

I reviewed the other open PRs in openshift-online/hypershell (#216, #214, #212, #211, #209, #208, #207, #206, #201, #200, #194, #189, #188, #185, #182, #179, #151, #150, #148, #135, #109, #75, #73). One material conflict:

  • #208 "[HYPERSHELL-129] feat(web-console): Adding section to gateway UI with instructions for sandbox connecting" - This is a genuine design/interface conflict, not just a file-merge overlap. Both PRs restructure the same Connection tab and edit the same shared files (editable-command.tsx, gateway-connection-steps.tsx, gateway-connections.ts, gateway-connection-steps.test.tsx, gateway-connections.test.ts, messages.ts, locales/en.json). Critically, #210 renames the shared exports EditableCommand -> CommandBlock and buildSetupScript -> buildOneTimeSetupScript, while #208 still imports and uses the old EditableCommand and buildSetupScript to add its "Connect to a sandbox" step. Whichever merges first, the other must rebase onto the renamed component API and the reordered step layout - and both are inserting/reordering children inside the same GatewayConnectionSteps step list. Maintainers should decide a merge order and have the later PR adopt this PR's CommandBlock/buildOneTimeSetupScript API rather than reintroducing the old names.

No material conflict with the other open PRs. The dashboard PRs (#209/#214) and the deps/i18n PRs touch locales/en.json/messages.ts only additively (no competing design), #206 (hsctl login) is a separate Go CLI and does not compete with this UI's "register the gateway" copy change, and #216 (OpenShift Route console) changes how the control plane publishes the gateway endpoint but keeps the same endpoint field this UI consumes.

Findings Summary (ordered by severity, highest first)

  1. [Minor] Translator description for connectionInstallPrereq says "after gateway registration" but the alert renders before registration - Content / i18n (messages.ts L102, en.json L156)
  2. [Minor] Installer pulled from NVIDIA/OpenShell/main/install.sh (unpinned) while CLI version is pinned - Supply chain (gateway-connections.ts L80-81, L104)

Convention Checklist

Convention Result
Input validated / shell arguments quoted Pass
No secrets in logs or responses Pass
Reuse shared/PatternFly UI components (no duplicate component) Pass
Config separate from code (i18n messages, URLs) Pass
Test Diff Scrutiny (modified assertions justified, no hidden contract removal) Pass
Spec updated to match behavior Pass
Conventional commit messages Pass

Comment thread packages/gateway-management-ui/src/messages.ts Outdated
Comment thread packages/gateway-management-ui/src/gateways/gateway-connections.ts
@jsell-rh

jsell-rh commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Amber review: comment

Amber review

Status: Complete

Verdict

This is a clean, well-scoped web-console change: the version-matched OpenShell install command is shell-escaped, the spec and i18n catalog are updated in lockstep, and the tests exercise ordering, copy behaviour, and the not-ready gating. My only substantive questions are about the install command's own bootstrapping assumptions and one cross-PR coordination point on the shared connection-tab components.

Highlights

  • buildOpenShellInstallCommand reuses shellArgument() so the endpoint is single-quoted when it contains anything outside the safe charset, and the command substitution re-tokenizes correctly. Injection surface is covered by the "quotes the gateway endpoint" test.
  • Readiness gating is consistent: both the install command and the setup script return undefined unless phase === "Running" and an endpoint exists, and the UI renders nothing rather than a partial command.
  • Test-diff scrutiny: the renamed "hides the installation prerequisite until the gateway is ready" test now keeps endpoint populated while phase is Provisioning. This is a stronger assertion (matches the spec's "give a Provisioning gateway an endpoint" case), and the endpoint-absent path is still covered in gateway-connections.test.ts, so no guarantee was dropped.

Findings

[Minor] Install command depends on openshell (and jq) already being present. The command computes OPENSHELL_GATEWAY_VERSION by invoking openshell ... status | jq -r '.version' — i.e. it requires the CLI it is meant to install, plus an undocumented jq dependency. The alert is titled "Prerequisite" and the copy says "Install the OpenShell CLI version for this gateway," which reads as first-install guidance to a user who has no CLI yet. If the intent is "re-install / pin to the gateway's version for someone who already has openshell," a one-line clarification in the prerequisite text (and/or noting the jq requirement) would prevent a confusing failure. Please confirm the assumed starting state. (gateway-connections.ts) Confidence: Medium.

[Minor] Duplicated instruction text. connectionInstallPrereq and connectionSetupDescription both now open with "Install the OpenShell CLI version for this gateway…". The repetition is harmless but slightly noisy in the rendered step; consider letting the setup description focus on the register/provider/model sequence. (messages.ts) Confidence: High.

Cross-PR coordination

Another open pull request adds a new "Connect to a sandbox" step to the same Connection tab and builds directly on the current shared exports — it renders its new command through EditableCommand and imports buildSetupScript from gateway-connections.ts. This PR renames those exact symbols (EditableCommandCommandBlock, buildSetupScriptbuildOneTimeSetupScript) and reworks the same gateway-connection-steps.tsx, its test, and the .shiki-count assertions. This is a competing interface change plus a shared-composition change, not just file overlap: whichever merges first forces the other to be rewritten against the new names and to reconcile the expected number of command blocks on the tab. Maintainers should decide a merge order and have the second PR rebase onto the renamed API (and agree on the final ordering of install / setup / create-sandbox / connect-sandbox steps). PR #208.

Convention Checklist

Convention Result
Input validated / shell arguments escaped Pass
No secrets in logs or responses Pass
Config separate from code (URLs as constants) Pass
i18n catalog matches message definitions Pass
Spec updated to match behavior Pass
Test Diff Scrutiny (no silently removed guarantees) Pass

Findings Summary (ordered by severity, highest first):

  1. [Minor] Install command assumes openshell/jq already installed while framed as a first-install prerequisite - UX / Logic (gateway-connections.ts)
  2. [Minor] Duplicated "Install the OpenShell CLI version…" text across two adjacent messages - Content (messages.ts)

@jsell-rh jsell-rh left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verdict

This is a clean, well-scoped web-console change: the version-matched OpenShell install command is shell-escaped, the spec and i18n catalog are updated in lockstep, and the tests exercise ordering, copy behaviour, and the not-ready gating. My only substantive questions are about the install command's own bootstrapping assumptions and one cross-PR coordination point on the shared connection-tab components.

Highlights

  • buildOpenShellInstallCommand reuses shellArgument() so the endpoint is single-quoted when it contains anything outside the safe charset, and the command substitution re-tokenizes correctly. Injection surface is covered by the "quotes the gateway endpoint" test.
  • Readiness gating is consistent: both the install command and the setup script return undefined unless phase === "Running" and an endpoint exists, and the UI renders nothing rather than a partial command.
  • Test-diff scrutiny: the renamed "hides the installation prerequisite until the gateway is ready" test now keeps endpoint populated while phase is Provisioning. This is a stronger assertion (matches the spec's "give a Provisioning gateway an endpoint" case), and the endpoint-absent path is still covered in gateway-connections.test.ts, so no guarantee was dropped.

Findings

[Minor] Install command depends on openshell (and jq) already being present. The command computes OPENSHELL_GATEWAY_VERSION by invoking openshell ... status | jq -r '.version' — i.e. it requires the CLI it is meant to install, plus an undocumented jq dependency. The alert is titled "Prerequisite" and the copy says "Install the OpenShell CLI version for this gateway," which reads as first-install guidance to a user who has no CLI yet. If the intent is "re-install / pin to the gateway's version for someone who already has openshell," a one-line clarification in the prerequisite text (and/or noting the jq requirement) would prevent a confusing failure. Please confirm the assumed starting state. (gateway-connections.ts) Confidence: Medium.

[Minor] Duplicated instruction text. connectionInstallPrereq and connectionSetupDescription both now open with "Install the OpenShell CLI version for this gateway…". The repetition is harmless but slightly noisy in the rendered step; consider letting the setup description focus on the register/provider/model sequence. (messages.ts) Confidence: High.

Cross-PR coordination

Another open pull request adds a new "Connect to a sandbox" step to the same Connection tab and builds directly on the current shared exports — it renders its new command through EditableCommand and imports buildSetupScript from gateway-connections.ts. This PR renames those exact symbols (EditableCommandCommandBlock, buildSetupScriptbuildOneTimeSetupScript) and reworks the same gateway-connection-steps.tsx, its test, and the .shiki-count assertions. This is a competing interface change plus a shared-composition change, not just file overlap: whichever merges first forces the other to be rewritten against the new names and to reconcile the expected number of command blocks on the tab. Maintainers should decide a merge order and have the second PR rebase onto the renamed API (and agree on the final ordering of install / setup / create-sandbox / connect-sandbox steps). PR #208.

Convention Checklist

Convention Result
Input validated / shell arguments escaped Pass
No secrets in logs or responses Pass
Config separate from code (URLs as constants) Pass
i18n catalog matches message definitions Pass
Spec updated to match behavior Pass
Test Diff Scrutiny (no silently removed guarantees) Pass

Findings Summary (ordered by severity, highest first):

  1. [Minor] Install command assumes openshell/jq already installed while framed as a first-install prerequisite - UX / Logic (gateway-connections.ts)
  2. [Minor] Duplicated "Install the OpenShell CLI version…" text across two adjacent messages - Content (messages.ts)

Comment thread packages/gateway-management-ui/src/gateways/gateway-connections.ts Outdated
Comment thread packages/gateway-management-ui/src/messages.ts Outdated
@jsell-rh jsell-rh changed the title [HYPERSHELL-259] feat(web-console): add gateway-matched CLI installation [HYPERSHELL-259] feat: reconcile gateway version for CLI installation Aug 27, 2026
@jsell-rh

jsell-rh commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Amber review: changes requested

Amber review

Status: Complete

Verdict

REQUEST_CHANGES

This is a well-structured, well-tested feature: the new gateway_version field is written through a dedicated atomic API path that a whole-row replace cannot clobber, health/version work is ordered and bounded, and the health Service/NetworkPolicy are reconciled with update-or-create semantics. The one blocking concern is that moving the health loop from serial to concurrent workers now shares a single Keycloak client whose token read is not fully lock-protected, which is a data race in production reconciler code.

@jsell-rh jsell-rh left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verdict

REQUEST_CHANGES

This is a well-structured, well-tested feature: the new gateway_version field is written through a dedicated atomic API path that a whole-row replace cannot clobber, health/version work is ordered and bounded, and the health Service/NetworkPolicy are reconciled with update-or-create semantics. The one blocking concern is that moving the health loop from serial to concurrent workers now shares a single Keycloak client whose token read is not fully lock-protected, which is a data race in production reconciler code.

Findings

Major

Concurrent workers share a Keycloak client with an unsynchronized token readcomponents/control-plane/internal/reconciler/health.go

reconcileGateways (health.go:200) now fans work out across up to four concurrent workers (runGatewayWorkers), and each worker's residual-absence path can call the single shared consoleClientChecker (ConsoleClientExists). The reconciler previously ran serially, and the removed comment even said so ("The health loop is serial, so a single shared client needs no additional synchronization"). The replacement comment (health.go:72) now claims "The client protects its shared token state for concurrent use" — but that is not fully true. keycloak.Client.ensureToken writes c.token under c.mu, while doRequestRaw reads c.token to build the Authorization header outside the lock. With two workers hitting a token-refresh boundary, one worker's unlocked read of c.token races the other worker's locked write — a data race (undefined behavior) that can produce a torn/partial admin token.

The PR's -race validation did not catch this because the reconciler tests use a fake ConsoleClientChecker, so the real client's concurrency path is never exercised under the race detector.

Fix options: snapshot the token inside ensureToken and return it (so the caller never reads c.token unlocked), or guard the token read in doRequestRaw with c.mu. Either way, please correct the health.go comment so it does not assert a guarantee the client does not currently provide.

Confidence: High.

Minor

Observed version is logged without sanitizing embedded control characterscomponents/control-plane/internal/reconciler/gateway_version.go

observedVersion comes from the gateway's own /health response (a semi-trusted, potentially attacker-influenced source if a gateway is compromised) and is only strings.TrimSpaced — that trims the ends but leaves embedded \n/\r intact. It is then logged verbatim (gateway_version.go:62, plus the WARN lines). security.spec.md calls for log-injection sanitization on values that flow into logs. Consider rejecting versions containing control characters, or stripping \r/\n before logging. The length bound (128) and shellArgument quoting in the console already limit blast radius, hence Minor.

Confidence: Medium.

Cross-PR coordination

Two open pull requests have material overlap that maintainers must resolve.

  • #194 shifts gateway deployment away from static YAML manifests to the upstream OpenShell Helm chart and records an explicit "no NetworkPolicies" decision, replacing the same files and code paths this PR builds on (manifests/gateway/service.yaml, manifests/gateway/networkpolicy.yaml, internal/gateway/manifests.go, internal/gateway/reconciler.go, internal/reconciler/health.go). This PR depends on those static manifests to ship the new openshell-gateway-health Service and the controller-only openshell-gateway-allow-controller-health NetworkPolicy, and adds a CONTROL_PLANE_NAMESPACE_PLACEHOLDER substitution in that manifest path. Maintainers must decide which deployment mechanism owns the health Service, whether the controller-health NetworkPolicy survives the no-NetworkPolicies decision, how the controller reaches the gateway health port under the Helm model, and the merge order of the two efforts.

  • #208 adds a new step to the same shared GatewayConnectionSteps component and new command builders in packages/gateway-management-ui/src/gateways/gateway-connections.ts, and imports/uses the exported buildSetupScript. This PR renames that export to buildOneTimeSetupScript and restructures the same connection-steps workflow. This is a competing interface/ownership change, not just a text merge: whoever merges second must adapt to the renamed export and reconcile the competing step layout. The owners should agree on the shared component structure and a merge order.

Findings Summary (ordered by severity, highest first)

  1. [Major] Concurrent health workers share a Keycloak client whose token read is unlocked, creating a data race (and a comment that overstates the safety guarantee) - Concurrency / Control Plane (health.go:72, health.go:200)
  2. [Minor] Observed gateway version logged without control-character sanitization - Security (log injection) (gateway_version.go:62)

Convention Checklist

Convention Result
No panic() in production code Pass
Errors wrapped with fmt.Errorf context Pass
errors.IsNotFound handled for 404 scenarios Pass
No secrets in logs or responses Pass
Input validated Pass
Log injection prevented Fail
SecurityContext on new pod specs N/A (no new pod specs)
Reconcile (update-or-create) pattern used Pass
Status/owned-field updates on error paths Pass
Proper context propagation (bounded timeouts, stream ctx) Pass
Concurrency safety of shared state Fail
Image references consistent across manifests Pass
OpenAPI/proto client generated, not hand-edited Pass
DB migration registered + nullable/back-compatible Pass
Test Diff Scrutiny (no silently flipped assertions) Pass


// consoleClientChecker is one long-lived Keycloak client for residual-absence
// checks. It keeps its token cache between passes. It is nil when Keycloak is
// not configured. The client protects its shared token state for concurrent

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Major] Concurrency: shared Keycloak client token read is not lock-protected.

This comment now asserts the console client "protects its shared token state for concurrent use," but with the switch to bounded concurrent workers (reconcileGateways/runGatewayWorkers) that guarantee does not hold. keycloak.Client.ensureToken writes c.token under c.mu, while doRequestRaw reads c.token to build the Authorization header outside the lock. Two workers hitting a token-refresh boundary race read-vs-write on c.token (undefined behavior).

The reconciler was previously serial (the prior comment said as much). Please either snapshot the token inside ensureToken and return it, or guard the token read in doRequestRaw with c.mu, and correct this comment. Note the -race run did not exercise this because the tests use a fake ConsoleClientChecker.

log.Printf("WARN gateway version: store runtime version for %s: %v", gatewayID, err)
return
}
log.Printf("INFO gateway version: %s runtime version set to %s", gatewayID, observedVersion)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Minor] Log injection: sanitize the observed version before logging.

observedVersion comes from the gateway /health response and is only strings.TrimSpaced, so embedded \r/\n survive and are logged verbatim here (and in the WARN lines above). security.spec.md calls for log-injection sanitization on externally-sourced values. Consider rejecting versions containing control characters, or stripping \r/\n before logging.

@JuanmaBM

Copy link
Copy Markdown
Collaborator

Follow-up stacked on this PR: #219 makes the e2e test install the openshell CLI via the console-recommended command, deriving the version from the gateway_version field added here (instead of a hardcoded pin). It's based on this branch and should merge after this one lands.

@jsell-rh

jsell-rh commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

Amber review

Status: Stopped

The pull request head changed before Amber posted the review. A later job can review the new head.

@jsell-rh

jsell-rh commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

Amber review: comment

Amber review

Status: Complete

View the submitted review.

@jsell-rh jsell-rh left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verdict

COMMENT — This is a careful, well-structured feature: the runtime gateway_version is written through a dedicated field-owned atomic gRPC path, the newly concurrent health loop is correctly guarded (map access under mu, Keycloak token reads now return a locked snapshot), and version observation is bounded, redirect-rejecting, and size/control-char validated. I found no blocking convention or security issues; the notes below are maintainability-level, and the Cross-PR section flags coordination that needs a maintainer decision.

What I verified

  • Field ownership is airtight. Replace now omits both ActiveSandboxCount and GatewayVersion, and SetGatewayVersion uses UPDATE ... WHERE gateway_version IS DISTINCT FROM ? RETURNING, emitting the outbox event only on an actual change. The gRPC integration test proves a whole-row update cannot clobber the reconciled version. Good, consistent with the sandbox-count precedent.
  • Concurrency is safe. The new bounded worker pool (gatewayHealthWorkerCount = 4) keeps per-gateway work serial; all routeNotReadySince / routeTornDown / routeVerifiedAt access goes through h.mu; and the Keycloak ensureToken change returns the token under lock instead of reading c.token after unlock — that closes the data race the new concurrency would otherwise open.
  • Version observation is defensive. Bounded timeout, CheckRedirectErrUseLastResponse (3xx then rejected by the status check), LimitReader + explicit oversize check, empty/control-char/length validation. The observed value is validated before it reaches a log line, so no log-injection vector.
  • Migration + generated artifacts are consistent. migrationAddGatewayVersion is registered in plugin.go; proto (field 23 + SetGatewayVersion RPC), OpenAPI (read_only), model, presenters, SDKs, and the data-model/health specs all line up with the source-of-truth definitions.
  • Test diff scrutiny. All modified test hunks are additive (new GatewayVersion fields/assertions, new worker tests). No pre-existing assertion was flipped from accept→reject or optional→required, and the new field is nullable with no backfill requirement, so no removed-guarantee concern.

Findings

[Minor] Dual ownership of the health Service/NetworkPolicy — The health Service and NetworkPolicy are created both by the provisioning manifest path (service.yaml / networkpolicy.yaml via ApplyManifestToNamespace) and by ReconcileGatewayHealthAccess. The specs match today, so they converge, but two independent writers of the same object will silently fight on every interval if either definition drifts. Consider a single source of truth (e.g. have the reconciler own it and drop the manifest copy, or derive both from one spec) to prevent future divergence. Confidence: High.

[Minor] Gateway namespace is interpolated into the health URL without an explicit validity checkhttpGatewayVersionObserver.Observe builds http://openshell-gateway-health.<namespace>.svc.cluster.local:... from the gateway's stored namespace. The value is control-plane-derived (low risk), but a defensive DNS-label check before URL construction would harden this against a malformed namespace ever reaching the request. Confidence: Medium.

Cross-PR coordination

A stacked test PR must merge after this one and re-implements this PR's install-command version derivation (trim → drop the first '-' and everything after → ensure a leading 'v') as a shell helper that mirrors buildOpenShellInstallCommand. The two derivations must be kept in sync, and the merge order must be honored; maintainers should confirm both before merging.

Another open PR adds a new "Connect to a sandbox" section to the same gateway Connection tab and edits the same shared files this PR restructures (gateway-connection-steps.tsx, the command builders in gateway-connections.ts, and the messages.ts / en.json catalogs, including this PR's buildSetupScriptbuildOneTimeSetupScript rename). These are overlapping designs for the same UI surface, not a plain merge conflict; the owners need to agree on the tab layout, the message keys, and a merge order so one does not silently drop the other's section.

Another open PR proposes replacing the static gateway manifests with the upstream OpenShell Helm chart and explicitly decides against provisioning NetworkPolicies at all. This PR's health-access design depends on a static Service + NetworkPolicy manifest (plus the CONTROL_PLANE_NAMESPACE_PLACEHOLDER substitution) and on controller→gateway:8081 ingress being allowed. Maintainers must decide how the gateway health endpoint is exposed and access-controlled under that direction so the two approaches do not collide.

Findings Summary (ordered by severity, highest first)

  1. [Minor] Health Service/NetworkPolicy have two independent writers (provisioning manifests + reconciler) that can fight if definitions drift — Reconciliation / Maintainability
  2. [Minor] Gateway namespace interpolated into the health URL without an explicit DNS-label check — Input Validation

Convention Checklist

Convention Result
No panic() in production code Pass
Errors wrapped with fmt.Errorf context Pass
errors.IsNotFound / not-found handled Pass
No secrets in logs or responses Pass
Input validated (version value, response size) Pass
Reconcile (update-or-create) pattern used Pass
Status/field updates on error paths preserve last value Pass
Concurrency guarded (mutex, token snapshot) Pass
Image/manifest references consistent across stack Pass
OpenAPI/proto generated, not hand-edited Pass
Migration registered Pass
Conventional commit messages Pass
Test diff scrutiny (no flipped assertions) Pass

// ReconcileGatewayHealthAccess makes the internal gateway health endpoint
// available to the control plane. It changes only the dedicated Service and
// NetworkPolicy that this reconciler owns.
func ReconcileGatewayHealthAccess(ctx context.Context, clientset kubernetes.Interface, namespace, controlPlaneNamespace string, skipNetworkPolicies bool) error {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Minor] Dual writers for the health Service/NetworkPolicy. These objects are also created by the provisioning manifest path (service.yaml / networkpolicy.yaml applied through ApplyManifestToNamespace). ReconcileGatewayHealthAccess reconciles the same names on every interval. The specs match today so they converge, but if either definition drifts the two writers will overwrite each other every pass. Consider a single source of truth (reconciler-owned, or both derived from one spec).

},
},
timeout: defaultGatewayVersionTimeout,
endpoint: func(namespace string) string {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Minor] Namespace goes straight into the health URL. http://openshell-gateway-health.<namespace>.svc.cluster.local:... is built from the gateway's stored namespace. It is control-plane-derived so risk is low, but a defensive K8s DNS-label validation before constructing the request would harden this against a malformed namespace ever reaching the URL.

@jsell-rh

jsell-rh commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

Amber review: comment

Amber review

Status: Complete

View the submitted review.

@jsell-rh jsell-rh left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verdict

This is a well-structured feature: the runtime version is observed from the gateway /health endpoint, stored through a dedicated field-owned gRPC write that a whole-row Replace cannot clobber, and the new concurrent health loop is correctly guarded (route-timer maps behind mu, Keycloak token returned as a locked snapshot). I found no blockers; the notes below are minor, plus one deploy change that is out of scope for this feature and overlaps a dedicated PR.

Strengths

  • SetGatewayVersion is atomic (UPDATE ... WHERE gateway_version IS DISTINCT FROM ?) and emits the update event only on a real change; Replace now omits GatewayVersion alongside ActiveSandboxCount, and grpc_integration_test.go proves a whole-row update cannot overwrite the reconciled version. Good use of the transactional-outbox pattern.
  • The move to 4 bounded workers is race-safe: the shared route-state maps are all accessed under h.mu, and keycloak.Client.ensureToken now returns a token snapshot instead of letting callers read c.token unlocked.
  • Version observation is defensively bounded: 3s timeout, redirects rejected (http.ErrUseLastResponse), response body size-limited, control characters and over-long values rejected, and observation failures are logged without perturbing phase/status.
  • Spec, data-model, OpenAPI, proto, SDKs, and web console are all updated consistently, and the read-only field is documented as control-plane-owned.

Minor findings

  1. Per-tick, per-gateway health-access reconcile (components/control-plane/internal/reconciler/gateway_version.go:43) - ReconcileGatewayHealthAccess runs for every ready gateway on every tick, issuing a Service+NetworkPolicy Get (and possible Update) each pass. This is bounded and correct, but the owned resources rarely drift; consider reconciling them less frequently (e.g. only on create/first-observe or on a longer cadence) to reduce steady-state API traffic across a large fleet. Confidence: Medium.

  2. Unrelated deploy/auth change bundled into this feature PR (deploy/openshift/kustomization.yaml:80-81) - adding API_ENV=development_oidc and restructuring the env patch to append is an OpenShift JWT-auth fix, not part of gateway-version reconciliation. It also duplicates a dedicated PR (see Cross-PR coordination). Consider dropping it here so the feature PR stays focused. Confidence: High.

  3. Sandbox NetworkPolicy no longer allows port 8081 (components/control-plane/internal/gateway/reconciler.go, openshell-gateway-allow-sandbox-v2) - health access on 8081 is now restricted to the controller-only policy. The spec was updated to match (sandboxes need only gRPC 8080), so this looks intentional; please confirm no sandbox workload relies on reaching the gateway health port. Confidence: Medium.

Cross-PR coordination

  • The dedicated OpenShift JWT-auth fix (API_ENV=development_oidc) that this PR adds in its final commit to deploy/openshift/kustomization.yaml is the entire subject of another open pull request that changes the same file for the same purpose. This is a duplicate/competing solution: maintainers must decide which PR owns the deploy-env change and coordinate merge order, because the second to merge will conflict and carry a redundant fix.
  • Another open pull request adds a new "Connect to a sandbox" section to the same gateway Connection-tab components this PR restructures. This PR renames the shared EditableCommand component to CommandBlock (making its props optional) and renames buildSetupScript to buildOneTimeSetupScript in gateway-connections.ts. That is a competing interface change to shared UI code the other PR builds on; whichever merges second must be updated to the renamed component/API, so the owners need to agree on the final component surface and a merge order.
  • Another open pull request is stacked on this branch and asserts, in an e2e test, the exact console-recommended install command that this PR's buildOpenShellInstallCommand produces (including the --suffix stripping) and consumes the new read-only gateway_version field. It must merge after this PR, and any change reviewers request to the install-command format here has to be mirrored there; coordinate the command contract and merge order.

Findings Summary (ordered by severity, highest first)

  1. [Minor] Unrelated OpenShift JWT-auth deploy change bundled into a version-reconcile feature PR - Scope / Cross-PR (deploy/openshift/kustomization.yaml:80)
  2. [Minor] Per-tick, per-gateway reconcile of owned health Service/NetworkPolicy adds steady-state API traffic - Performance (gateway_version.go:43)
  3. [Minor] Confirm no sandbox relies on gateway port 8081 after tightening the sandbox NetworkPolicy - Spec Consistency (reconciler.go)

Convention Checklist

Convention Result
No panic() in production code Pass
Errors wrapped with fmt.Errorf context Pass
errors.IsNotFound handled for 404 scenarios Pass
No secrets in logs or responses Pass
Input validated (version length/control chars, required ID) Pass
Reconcile pattern used (update-or-create) Pass
Status/field updated only on change; error paths logged Pass
Concurrency safe (shared maps + Keycloak token guarded) Pass
OpenAPI client not manually edited (make generate) Pass
Image references consistent across manifests Pass
Conventional commit messages Pass
Test diff scrutiny (assertions additive, no flipped guarantees) Pass

return
}

if err := gateway.ReconcileGatewayHealthAccess(ctx, h.clientset, namespace, h.controlPlaneNamespace, h.skipNetworkPolicies); err != nil {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Minor] Performance: ReconcileGatewayHealthAccess runs for every ready gateway on every health tick, issuing a Service + NetworkPolicy Get (and a possible Update) each pass. It is bounded and correct, but these owned resources rarely drift. Consider reconciling them on first observation or on a longer cadence to reduce steady-state K8s API traffic across a large fleet. Confidence: Medium.

- name: RBAC_SERVICE_ACCOUNTS
value: "service-account-hypershell-control-plane"
name: API_ENV
value: "development_oidc"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Minor] Scope / Cross-PR: Adding API_ENV=development_oidc and switching the env patch to append-mode is an OpenShift JWT-auth fix unrelated to gateway-version reconciliation, and it duplicates a separate open PR that changes this same file for the same purpose. Recommend dropping it from this feature PR and coordinating with the dedicated deploy-env PR on ownership and merge order. Confidence: High.

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.

2 participants