Skip to content

refactor(bff): migrate gateway client from raw gRPC to OpenShell Go SDK #35

Closed

Description

@gmenher

Problem Statement

The BFF gateway layer duplicates functionality that the official OpenShell Go SDK now provides. This creates unnecessary maintenance overhead: 57 hand-written RPC wrappers, local proto stubs, manual error wrapping, and a custom auth mechanism — all of which the SDK handles upstream. This issue proposes an incremental migration to eliminate that duplication.

Why

The BFF maintains a hand-written gRPC client wrapper (backend/internal/gateway/client.go + 9 domain files) that directly imports proto-generated stubs and manually wraps 57 RPC methods with fmt.Errorf context. This is a maintenance burden:

  1. Dual maintenance. Every proto change upstream requires regenerating stubs in this repo and updating the wrapper layer. The official Go SDK (github.com/NVIDIA/OpenShell/sdk/go, merged in NVIDIA/OpenShell#2271) already provides typed sub-clients, domain types, and proto converters — maintained upstream.

  2. No test fakes. The current interface is mocked by a manually-written mockGateway with injectable function fields. The SDK ships a fake package with in-memory stores, watch broadcasting, and deep-copy isolation — free testing infrastructure.

  3. Fragile error extraction. The BFF wraps every gRPC error with fmt.Errorf("verb resource: %w", err), then must unwrap it in writeGrpcError via errors.As + GRPCStatus(). The SDK surfaces errors as StatusError with typed helpers (IsNotFound(), IsAlreadyExists()), making the unwrap/re-wrap cycle unnecessary.

  4. Auth duplication. The BFF implements its own PerRPCCredentials for token forwarding. The SDK provides AuthProvider implementations (including refreshable tokens) that handle this at the transport layer.

What This Means Today

The SDK foundation (#2271) is merged. The completion PR (NVIDIA/OpenShell#2702) adds all 14 domain clients including Workspace, Inference, Provider, Exec, Policy, and Service — every RPC the BFF currently wraps. It is open, mergeable, and under review.

Settled Design Decisions

1. Adapter pattern behind gateway.Interface

The existing gateway.Interface (57 methods) remains the contract between API handlers and the gateway layer. The migration replaces the implementation behind it, not the interface itself. This means:

  • API handlers do not change.
  • The mock in mock_gateway_test.go continues to work.
  • Migration can be incremental (one sub-client at a time).

2. Per-request auth via custom AuthProvider

The BFF forwards a different bearer token per HTTP request (from the auth proxy). The SDK's AuthProvider is set at client construction time. Resolution: implement a context-aware AuthProvider that reads the token from the request context on each RPC:

type contextAuth struct{}

func (a *contextAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
    token := auth.TokenFromContext(ctx)
    if token == "" {
        return nil, nil
    }
    return map[string]string{"authorization": "Bearer " + token}, nil
}

func (a *contextAuth) RequireTransportSecurity() bool { return false }

This preserves the BFF's relay-only architecture (ADR 0014) while using the SDK's transport layer.

3. No proto stubs in this repo after migration

Once fully migrated, backend/gen/ (generated proto stubs) can be deleted. The SDK owns the proto-to-domain conversion internally. The BFF consumes only SDK domain types.

Proposed Design

Migration phases

Phase 1: Sandbox client (immediate)

  • Replace gateway/sandboxes.go (5 methods) with SDK Sandboxes() calls.
  • Verify: auth forwarding works, error types map to existing HTTP codes, streaming ExecSandbox response shape unchanged.
  • After merge: BFF uses SDK for sandbox CRUD; raw gRPC for everything else.

Phase 2: Providers + Profiles + Refresh (after SDK #2702 merges)

  • Replace gateway/providers.go (15 methods).
  • Largest surface area; validates that the converter types align.

Phase 3: Workspaces + Policy + Services + Inference

  • Replace remaining domain files.
  • After merge: backend/gen/ is dead code.

Phase 4: Cleanup

  • Delete backend/gen/, remove proto generation tooling from the repo.
  • Remove go.sum entries for direct proto dependencies.
  • Update go.mod to depend only on github.com/NVIDIA/OpenShell/sdk/go.

Cross-cutting constraints

  • No API contract change. HTTP response shapes must be identical before and after. Validated by existing respond_ext_test.go and e2e tests.
  • Error code preservation. The SDK's StatusError codes must map to the same HTTP status codes the BFF currently returns. Verified via TestWriteGrpcErrorWrapped.
  • Streaming passthrough. ExecSandboxInteractive must remain a bidirectional stream bridge (WebSocket to gRPC). The SDK's Exec().Interactive() returns an InteractiveSession with Read/Write/Resize/Close — verified compatible (see below).

Compatibility Verification

The following were verified against NVIDIA/OpenShell main and openshell-dashboard at 8bf7a76:

Check Result Detail
Workspace parameter pattern Compatible SDK uses (ctx, workspace, name string, ...) — same as BFF
ExecResult type Compatible SDK: {ExitCode int, Stdout []byte, Stderr []byte} — identical to BFF aggregator
InteractiveSession vs raw stream Compatible SDK Read/Write/Resize maps to BFF Send(Stdin)/Send(Resize)/Recv(Stdout+Stderr). Stdout and stderr are mixed in Read() which is correct for terminal (xterm.js renders both to same output)
grpc-go version Compatible SDK v1.80, BFF v1.82. Go modules resolves to v1.82 (semver minor, backward compatible)
Inference client Blocked on #2702 inference.proto exists in OpenShell but SDK client only comes with PR #2702. Keep raw gRPC for 3 Inference methods until then

Alternatives Considered

Keep the raw gRPC client. Zero effort now, ongoing tax every time the proto evolves. Every new RPC requires: regenerate stubs, write wrapper method, add to interface, update mock. With the SDK: go get -u and call the new method.

Wait for SDK to be declared stable. The SDK is alpha and may have breaking changes. However, OpenShell itself is alpha (badge on README), the BFF already pins a specific proto commit, and the SDK follows semver once tagged. Pinning the SDK version is no riskier than pinning the proto version.

Use the SDK only for new features. Creates a split where some handlers use the SDK and others use raw gRPC. Acceptable as an intermediate state (Phase 1-3) but not as a permanent architecture.

Agent Investigation

Verified against openshell-dashboard at 8bf7a76 and NVIDIA/OpenShell main (Aug 11 2026):

  • backend/internal/gateway/interface.go: 57 methods on Interface.
  • backend/internal/gateway/client.go: grpc.NewClient with PerRPCCredentials, URL prefix parsing (grpcs://, http://).
  • backend/gen/openshellv1/: regenerated stubs from OpenShell proto. Module path github.com/Gkrumbach07/openshell-dashboard/backend/gen/openshellv1.
  • SDK at sdk/go/openshell/v1/: NewClient(Config{Address, Auth, TLS}), Sandboxes() implemented (11 methods), 9 stub sub-clients in stub_clients.go.
  • SDK PR #2702: adds remaining 14 domain clients, OIDC flows, fakes, gateway discovery. 100 files, 1265 tests.
  • SDK infrastructure is ~80% complete (13 interfaces, 20 domain types, converters, auth, error model). Only method body wiring is stubbed (195 lines).

Checklist

  • Reviewed existing BFF gateway code (57 methods, 53 RPCs)
  • Reviewed SDK API surface and implementation status
  • Verified proto compatibility between BFF stubs and SDK
  • Confirmed SDK #2702 covers all BFF RPC needs
  • Identified auth model challenge and proposed solution
  • Verified InteractiveSession compatibility for terminal WebSocket bridge
  • Verified grpc-go version compatibility (v1.80 + v1.82)
  • Confirmed Inference is only blocker (3 methods, pending #2702)
  • This is a design proposal tracking incremental migration

Activity

  1. rhuss commented on Aug 12, 2026

    @rhuss
    Contributor

    If you find any dragons in the Golang SDK, please let me know (best, open an issue upstream and ping me and @ChristianZaccaria, that would be awesome). Thank you!

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions