Repository navigation
feat(bff): migrate BFF handlers to OpenShell Go SDK [RHOAIENG-83116] - #43
Merged
Merged
Conversation
This was referenced Aug 17, 2026
- Add github.com/NVIDIA/OpenShell/sdk/go as dependency (upstream, post-#2702) - Create sdkclient/auth.go with ContextAuthProvider for per-request token relay - Add writeSDKError() to respond.go using SDK typed error helpers (IsNotFound, etc.) - Existing writeGrpcError() kept for hybrid mode during incremental migration Part of: RHOAIENG-83121
- Rewrite sandboxes_handler.go to use app.sdk.Sandboxes().* calls - Add sdk field to App struct (hybrid mode: gateway + SDK coexist) - Create models/sdk_converters.go with FromSDKSandbox, BuildSDKSandboxSpec, ParseSDKPolicy, and marshalSDKPolicy for SDK domain types - Update main.go to create SDK client with ContextAuthProvider - Annotations now correctly passed via CreateOptions (was silently dropped) - CPU/Memory now correctly mapped to Template.Resources Build passes. Tests need mock update for SDK client (next commit). Part of: RHOAIENG-83121
- Rewrite sandboxes_handler_test.go to use SDK mock instead of gateway mock - Add mock_sdk_test.go with minimal mockSDK implementing ClientInterface - All sandbox tests use SDK error types (StatusError) instead of grpc/status - Remove GPU/resource capture test (will be re-added in Phase 2 with SDK types) - Tests pass with GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn (hybrid mode) Part of: RHOAIENG-83121
- Only set SDK TLS config when URL scheme indicates TLS (prevents plaintext+CA mismatch in dev-env) - Add fallback gRPC status mapping in writeSDKError for FailedPrecondition, OutOfRange (400), and ResourceExhausted (429) codes not covered by SDK typed helpers Part of: RHOAIENG-83121
- Add TestWriteSDKError covering all SDK error types + fallback gRPC codes (FailedPrecondition->400, ResourceExhausted->429, unknown->500) - Add TestBuildSDKSandboxSpec* covering basic, GPU, CPU/Memory, invalid policy - Add TestFromSDKSandboxPhaseMapping verifying uppercase phase conversion Part of: RHOAIENG-83121
- Fix golangci-lint: rename Tls->TLS, reorder struct fields for alignment, apply De Morgan's law, suppress gocyclo on main() - Add GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn to CI test and e2e steps (required during hybrid mode while gen/ and SDK coexist) Part of: RHOAIENG-83121
Replace the 15 provider, profile, and refresh RPCs with SDK nested sub-clients while preserving the existing HTTP JSON contract. Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
…appers List/Attach/Detach now go through sdk.Sandboxes(), and the proto provider client is removed now that handlers no longer call it. Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Gateway reads omit Spec.Credentials; names live on CredentialHandles. Also nil-guard FromSDKSandbox, share the AWS STS strategy const, and ignore the local backend/server binary. Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Replace proto gateway RPCs for workspaces, policy/drafts, services, inference, settings, logs, health, terminal, and files with app.sdk. Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
…gaps Files().Upload/Download is a stub in this SDK build; pipe uploads through Exec Interactive dd and downloads through Exec Run cat. Reconstruct policy revision history with GetStatus(WithVersion) because Policy.List omits sandbox name. Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Return 502 when dd exits non-zero instead of HTTP 200 with success:false. Log skipped policy revisions and load older versions concurrently. Cover terminal relay, upload failures, and gapped policy history in unit tests. Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
golangci-lint failed on check-backend: mockGateway was dead after the SDK cut, UploadFile/TestTerminalRelay exceeded gocyclo, and the interactive session mock failed fieldalignment. Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
sync.Once is 12 bytes; putting closed before exit uses that padding and drops the struct from 96 to 88. Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
The SDK migration left the old protoc-generated stubs (backend/gen/*) and the hand-rolled gateway wrapper (internal/gateway/*) compiled into the binary alongside the OpenShell Go SDK. Both register the same proto descriptors (options.proto, etc.) in the global protobuf registry, so the server panicked at init with "proto: file already registered". CI masked this with GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn, but production containers (deploy/Dockerfile) set no such var and would crash-loop on boot. All handlers already call app.sdk exclusively; app.gateway was unused. - Delete backend/gen/ and backend/internal/gateway/ (dead) - Drop old proto-based converters/builders from internal/models, keeping only the JSON DTO structs that sdk_converters.go reuses - Remove old converter tests (models_test, models_ext_test, observability_test); sdk_converters_test covers the SDK converters - Remove the vestigial gateway field/param from App/NewApp and the gateway client construction from main.go - go mod tidy: SDK is now a direct dependency (was // indirect) - Remove the GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn CI workaround Verified: server boots and serves /healthz with no env-var workaround; go build/test (-race)/vet/golangci-lint all pass (api and models test packages, previously un-runnable due to the panic, now run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Gage Krumbach <gkrumbac@redhat.com>
Two HIGH-severity data-integrity regressions from the SDK-migration audit. 1) Policy fidelity (models): the hand-rolled policy<->JSON converter only serialized 6 endpoint fields, silently dropping L7 allow/deny rules, IP allowlists, multi-port, MCP options, credential binding, signing, GraphQL persisted queries, and network middleware on both read AND write — a sandbox created or a rule edited with those fields lost them before reaching the gateway, weakening egress policy with no error. Replace it with a proto round-trip via protojson (policyproto.go ports the SDK's internal domain<->proto converter, which is not importable), reproducing the exact camelCase, full-fidelity JSON contract the pre-SDK code emitted. Unknown-field rejection is preserved. Added a round-trip test over a fully-populated policy. 2) Binary upload (files): uploads were piped through Exec().Interactive, which forces a PTY — terminal line discipline (EOF/flow-control bytes, CR/LF translation, echo) silently corrupts binary payloads. The SDK exposes no non-TTY stdin exec (Run has no stdin; Interactive forces TTY) and its file transport is a stub, on latest too. Add a small dedicated RawExecClient that calls the gateway's ExecSandbox RPC with Stdin bytes and Tty=false, resolving name->UUID via the SDK first. Uses the same address/TLS/bearer-forwarding as the main client. Added a bufconn test proving raw stdin bytes and Tty=false reach the server. Verified live against a local gateway (v0.0.85, podman): creating a sandbox with L7 rules / allowedIps / multi-port / denyRules and GETting it back preserves every field; the upload path issues ExecSandbox (non-TTY), with zero ExecSandboxInteractive calls. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Gage Krumbach <gkrumbac@redhat.com>
PR ai-openshell#42 added --gateway-client-cert/--gateway-client-key (mTLS to the gateway) by passing them to the hand-rolled gateway.New(). The SDK migration removed that client, so those flags were dropped in the rebase. Re-wire them into the SDK client's TLSConfig (CertFile/KeyFile) and into the RawExecClient used for upload, so both honor gateway mTLS. Error out if only one of cert/key is supplied. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Gage Krumbach <gkrumbac@redhat.com>
Gkrumbach07
force-pushed
the
feat/sdk-migration
branch
from
August 27, 2026 17:51
94bfecd to
a09462a
Compare
Contributor
TestingRebased onto latest Automated
Manual, live against a local gateway (v0.0.85, podman, plaintext dev)
🤖 Generated with Claude Code |
Gkrumbach07
added a commit
that referenced
this pull request
Aug 27, 2026
- Fix false-positive success toasts: list-page Stop/Start now report success/failure from the mutation's actual outcome, not immediately on click. Detail-page Stop/Start gained the error feedback they were missing entirely. - Extract canStopSandbox/canStartSandbox so the list row and detail page page can't disagree on which phases allow which action. - Add STOPPED/STOPPING/STARTING colors to StatusDot (PhaseLabel already had them). - Drop the delete-confirmation copy claiming deletion is "the only way to stop a running sandbox" — no longer true now that Stop exists. - ApproveDraftChunk: accept an optional client-supplied reviewToken (the frontend already has it from GetDraftPolicy) and only fall back to the extra GetDraft round-trip when the client didn't send one. - Add handler tests for StopSandbox/StartSandbox (previously untested; the SDK mock's Stop/Start literally panicked) and for both ApproveDraftChunk token paths. - Correct .claude/rules/openshell-api.md and CLAUDE.md: backend/proto/ and backend/internal/gateway/ were removed by the SDK migration (#43) and are stale/unused — the vendored Go SDK is the source of truth now, not a local proto copy. Also documented ApproveDraftChunk's new review_token under the optimistic-concurrency hard fact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Gage Krumbach <gkrumbac@redhat.com>
Gkrumbach07
added a commit
that referenced
this pull request
Aug 27, 2026
- Fix false-positive success toasts: list-page Stop/Start now report success/failure from the mutation's actual outcome, not immediately on click. Detail-page Stop/Start gained the error feedback they were missing entirely. - Extract canStopSandbox/canStartSandbox so the list row and detail page page can't disagree on which phases allow which action. - Add STOPPED/STOPPING/STARTING colors to StatusDot (PhaseLabel already had them). - Drop the delete-confirmation copy claiming deletion is "the only way to stop a running sandbox" — no longer true now that Stop exists. - ApproveDraftChunk: accept an optional client-supplied reviewToken (the frontend already has it from GetDraftPolicy) and only fall back to the extra GetDraft round-trip when the client didn't send one. - Add handler tests for StopSandbox/StartSandbox (previously untested; the SDK mock's Stop/Start literally panicked) and for both ApproveDraftChunk token paths. - Correct .claude/rules/openshell-api.md and CLAUDE.md: backend/proto/ and backend/internal/gateway/ were removed by the SDK migration (#43) and are stale/unused — the vendored Go SDK is the source of truth now, not a local proto copy. Also documented ApproveDraftChunk's new review_token under the optimistic-concurrency hard fact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Gage Krumbach <gkrumbac@redhat.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://redhat.atlassian.net/browse/RHOAIENG-83116
Replaces #40 (RHOAIENG-83121) and #41 (RHOAIENG-83122). Phase 3 is RHOAIENG-83123. Phase 4 cleanup is a follow-up: RHOAIENG-83124.
Description
This PR moves the dashboard BFF off the hand-rolled gRPC wrappers (
backend/internal/gateway/+backend/gen/) and onto the official NVIDIA OpenShell Go SDK (github.com/NVIDIA/OpenShell/sdk/go). Every HTTP handler that talks to OpenShell now goes throughapp.sdksub-clients. The frontend JSON contract is unchanged.I originally split this into stacked PRs (sandbox → providers → the rest). After talking it through with Roland, mixed gRPC/SDK in the same binary is not worth the review cost: you still pay the proto registration conflict, you still ship both clients, and reviewers cannot tell if a bug is “SDK” or “the leftover wrapper”. So this is one PR covering what was Phase 1 + 2 + 3. Phase 4 (delete
backend/gen/, stop dialinggateway.New(), dropGOLANG_PROTOBUF_REGISTRATION_CONFLICT) stays separate so we can land the handler cut without also deleting the generated stubs in the same diff.The BFF was wrapping 57 RPCs through a private client and ~21K lines of generated stubs. The SDK (merged upstream in NVIDIA/OpenShell#2702) is the supported surface: typed sub-clients, domain types, and error helpers maintained by the OpenShell team. Roland’s old intermediate PR (
review/sdk-migration/github.com/rhuss/openshell-sdk-go) is the design reference, not the dependency.What changed
Foundation
backend/go.mod:github.com/NVIDIA/OpenShell/sdk/go v0.0.0-20260814001211-c4b500a7de64(upstream, post-#2702 — goroutine leak fixes inInteractiveSession).backend/internal/sdkclient/auth.go:ContextAuthProviderimplementsgrpc.PerRPCCredentialsby reading the bearer from request context on every RPC.RequireTLSfollows the gateway URL scheme.backend/cmd/server/main.go: builds the SDK client next to the old gateway client.grpcs:///https://→ TLS (optionalGATEWAY_CA_CERT). Plaintext →TLSConfig{Insecure: true}.scripts/dev-env.shnow emitsgrpcs://localhost:${GATEWAY_GRPC_PORT}.backend/internal/api/respond.go:writeSDKError()maps SDK typed errors (IsNotFound,IsAlreadyExists, …) onto the existing HTTP codes, with a fallback to raw gRPC status forFailedPrecondition/ResourceExhausted. Message comes fromStatusError.Messageso we do not leak the error-chain prefix.backend/internal/api/app.go:Appstill has bothgatewayandsdk(hybrid until Phase 4). Handlers only callsdk.Phase 1 — sandboxes
sandboxes_handler.go: Create / Get / List / Delete viaapp.sdk.Sandboxes(). Annotations go throughCreateOptions(they were silently dropped on the old path).Phase 2 — providers + attach
providers_handler.go: all 15 methods onProviders(),Providers().Profiles(),Providers().Refresh().logs_handler.go:ListSandboxProviders/ Attach / Detach viaSandboxes().ListProviders/AttachProvider/DetachProvider. Logs themselves also moved in Phase 3.gateway/providers.go(209 lines) and the provider wrappers ingateway/logs.go.Phase 3 — everything else
Exec().Interactive→ WebSocket relay)Converters + tests
models/sdk_converters.go: SDK domain types → existing HTTP DTOs. We map into the JSON the frontend already speaks (camelCase, UPPER_SNAKE categories), not the SDK’s TitleCase.mockSDK(newTestAppWithSDK).writeSDKErrorhas its own table tests.Design decisions
1. One PR, not four stacked ones
Stacked PRs looked cleaner on paper and matched the Jira tickets. In practice the hybrid binary is the painful part, and splitting it across #40 / #41 / a Phase 3 PR means three reviews of the same wiring. Roland’s guidance was to do this in one or two shots, agent-reviewable. This PR is that shot. The Jira tickets still map 1:1 onto the commits if anyone wants to follow the original phases.
2. Hybrid until Phase 4 — do not delete
backend/gen/heremain.gostill dialsgateway.New()and assignsApp.gateway. Nothing in the HTTP handlers reads it. I left it because several files underinternal/modelsandinternal/gatewaystill import the generated stubs, and deletinggen/in the same PR would mix “did the SDK mapping work?” with “did we miss a proto import?”. CI needsGOLANG_PROTOBUF_REGISTRATION_CONFLICT=warnuntil Phase 4 because bothbackend/gen/and the SDK register the same proto packages.3. Upstream SDK, not Roland’s fork
github.com/NVIDIA/OpenShell/sdk/go, notgithub.com/rhuss/openshell-sdk-go. Upstream has the InteractiveSession close/leak fixes. Roland’s branch was useful as a map of which handler goes to which sub-client (app.clientthere →app.sdkhere).4. Preserve the HTTP JSON contract
The frontend is not part of this change. Converters (
FromSDKSandbox,FromSDKProvider,FromSDKDraftPolicy, …) keep:INFERENCE,AGENT, …)credentialNamesas the union ofCredentialsandCredentialHandles(sorted). Handles-only providers were coming back with an empty list after the first SDK cut; that is fixed.We do not invent RPCs. If the OpenShell API does not expose it, the BFF does not fake it.
GetDraftSummaryis still the pre-existing empty stub (no cross-workspace draft RPC).5. Delete semantics follow the SDK, with two exceptions
Old wrappers returned
(bool, error)for several deletes. The SDK returnserroronly. On success we now always return{"deleted": true}/{"removed": true}; a missing resource is 404. That is a small, intentional contract change.DeleteProviderProfileandDeleteProviderRefreshstill forward the SDK’sdeletedbool, because those RPCs actually return it.6. File transfer — SDK
Files()is a stub in this builddefaultSSHTransport.available()is hardcodedfalsein the SDK we pin. Official docs: the standalone SDK does not currently ship an SSH file-transfer transport.Files().Upload/Downloadalways returnErrTransportNotAvailable.Upload therefore pipes bytes into
Exec().Interactivewithdd of=<path> bs=4096(same idea as the oldExecSandboxpath). Non-zeroddexit is HTTP 502upload_failed(not 200 +success: false). Download isExec().Run(["cat", path]); non-zero exit is 404. Path validation and a 64MiBMaxBytesReaderare unchanged. This is a workaround we should drop when the SDK ships a real transport.7. Policy history —
Policy.Listdrops the sandbox nameThe proto has
ListSandboxPoliciesRequest.Name. The SDKPolicy.Listmethod does not send it, so a list call is not scoped to the sandbox.GetSandboxPolicytherefore:GetStatusfor the latest revisionGetStatus(WithVersion(v))in parallelslog.Warn(not a 500)That is reconstructing history the SDK should have given us. Worth an upstream issue if it is still true on a newer SDK drop.
8. Error mapping lives in one place
Migrated handlers use
writeSDKError.writeGrpcErrorstays until Phase 4 because the leftover gateway client / tests still use it. Status mapping:9. Known JSON gaps vs the old proto converters (not regressions we paper over)
These are SDK type gaps, called out so reviewers do not think the converters “forgot” fields:
FromSDKSandboxLogsdoes not setsandboxId(SDKLogLinehas no id; JSONomitempty).CurrentUserhas noemail; we do not invent one.10. Token relay is unchanged (ADR 0014)
The BFF still does not run OIDC. It forwards the bearer (or none, when
AUTH_DISABLED=true). The SDK’sContextAuthProvideris the same relay, just on the SDK dial instead of the hand-rolled client.Dependencies
Out of scope (Phase 4)
backend/gen/andbackend/proto/gateway.New()/ removeApp.gatewaywriteGrpcErrorandmock_gateway_test.gogen/GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warnGo SDK e2e against NVIDIA/OpenShell (
e2e/go/) is jpetrlik’s track, not this repo.How Has This Been Tested?
1. Unit tests
Coverage by surface:
expectedResourceVersionGetStatus(WithVersion)skips missing revisions)dd of=… bs=4096, faileddd→ 502, Interactive error → 500, download 200 / 404120×40writeSDKErrortable for the status mapping abovecredentialNamesunion, profile categories, nil sandbox2. Live smoke (Phase 1 + 2, personal ROSA gateway)
Same setup as #40 / #41:
Sandboxes (UI, localhost:3000)
pruebaprueba-2prueba-2invalid_nameProviders (UI)
pruebaAUTH_DISABLED=truestill hits the real SDK dial, converters, and error mapping. The gateway validates the RPCs either way.3. Phase 3 live (files / terminal / drafts)
Unit-tested as above. I started a local
dev-env.shgateway to go further and hit two environment issues that are not BFF bugs:grpcs://localhost:17670lands on the compute-driver callback listener (127.0.0.1is covered by the0.0.0.0primary bind and scoped to sandbox-callback RPCs only). The BFF has to dial a non-loopback address that is still the primary listener (LAN IP + cert SAN), or the SDK correctly getsPermissionDenied.python:3.12-slimsandbox dies withOCI USER is required because run_as_user is omitted. Community imageghcr.io/nvidia/openshell-community/sandboxes/base:latestis the right target; I did not wait out a full pull + files/terminal pass for this PR.So: handler + mock coverage for files/terminal/drafts is in the diff. End-to-end against a live sandbox for those three is still a manual pass (Dan’s review / a gateway that is already up). Happy to run it on the ROSA forward if we want that before merge.
4. Why this testing approach
Full browser OIDC would only test the proxy in front of the BFF. The migration risk is SDK mapping, error codes, and the two SDK gaps (files transport, policy list). Unit tests pin those. Phase 1/2 live smoke already proved TLS + token relay + CRUD against a real gateway. Phase 4 should not merge until
gen/is gone and the dual-registration env var can disappear.