Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 6 additions & 0 deletions ACM-42568_go_migration/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright Contributors to the Open Cluster Management project

testdata/recorded/
*.exe
*.test
coverage.out
128 changes: 128 additions & 0 deletions ACM-42568_go_migration/QUIRKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Known Node backend quirks (ACM-42590)

Documented so Go can **replicate** them on purpose, or **fix later** with an explicit decision. Contract tests encode the current Node wire behavior.

## Replicate (bug-compatible / observable contract)

### DELETED SSE events skip RBAC

`eventFilter` in `backend/src/routes/events.ts` returns `true` for every `DELETED` event. ADDED/MODIFIED go through SelfSubjectAccessReview. Comment in source: namespace deletion makes a later access check fail.

**Decision:** replicate until a dedicated security follow-up tracks who previously received the object.

### SSAR cache TTL 60s

Per-token access cache: TTL 60s, cleanup interval 90s, max 1000 tokens (`ACCESS_CACHE_TTL` in `events.ts`).

**Decision:** replicate. Tests do not assert wall-clock TTL (flake); they assert authorized vs unauthorized snapshots.

### Kube proxy header allowlist

`proxy.ts` forwards only `accept`, `accept-encoding`, `content-encoding`, `content-length`, `content-type` to the API server, and only `cache-control`, `content-type`, `content-length`, `content-encoding`, `etag` back to the client. Cookies and `X-Forwarded-*` are not passed through.

**Decision:** replicate. Tests assert `content-type` is present and `set-cookie` is absent on `/api`.

### Session probe is GET /api, not TokenReview

`isAuthenticated` uses `GET {CLUSTER_API_URL}/api` with the user Bearer token. `/username` and `/userpreference` use TokenReview.

**Decision:** replicate. `/authenticated` returns that status with an empty body.

### OAuth routes only when NODE_ENV !== production

`/configure`, `/login`, `/login/callback`, `/logout` are registered only outside production.

**Decision:** replicate. Local `npm run plugins` is development, so these cases are required there.

### 401 / 404 / 500 bodies are empty

`unauthorized`, `notFound`, and `respondInternalServerError` write status with no JSON body.

**Decision:** replicate (`bodyEmpty: true` on negative cases).

### Search WebSocket injects Authorization in `connection_init`

The browser graphql-ws `connection_init` payload is rewritten to include `Authorization: Bearer <token>` before relay to search-api. The HTTP POST `/proxy/search` sends Bearer on the outbound request.

**Decision:** replicate. WS test sends `connection_init` without a token and expects `connection_ack` if Search is up.

### userpreference username sanitization

TokenReview username is lowercased and non `[a-z0-9-.]` characters become `-` for the CR name.

**Decision:** replicate. Tests only assert GET returns JSON.

### Metrics path rewrite

`/observability/*` and `/prometheus/*` replace the first path segment with `/api/v1` before proxying.

**Decision:** replicate.

### Authentication CR is not fanned out on SSE

`{ kind: 'Authentication', apiVersion: 'config.openshift.io/v1', forwardEventsToClients: false }`. Hub still reads it for `/hub`. The informer cache (ACM-42597) still stores it; snapshot compare **includes** Authentication and **excludes** Argo `isPolled` kinds.

**Decision:** replicate. Watched-kind set used by SSE tests omits Authentication.

### SSE framing, packets, compression, keepalive

- `id:` + `data:{json}` + blank line
- Snapshot order: START, SETTINGS, then ManagedCluster/HostedCluster/… packets ending with `EOP`, remainder, LOADED
- Gzip (not brotli; Firefox) unless `DISABLE_STREAM_COMPRESSION=true`
- Keepalive comment `:\n\n` every 10s
- `Set-Cookie: watch=<instanceID>; Secure; HttpOnly; Path=/`
- `Content-Type: text/event-stream`, `Cache-Control: no-store, no-transform`

**Decision:** replicate framing and lifecycle types. Tests do not require exact ADDED ordering inside a packet, only type presence and object shape.

### `/multicloud` prefix

HTTP `requestHandler` strips `/multicloud` before `find-my-way`. WebSocket `upgrade` only matches `/multicloud/proxy/search` and `/multicloud/managedclusterproxy` β€” a bare `/proxy/search` upgrade is ignored and the socket hangs.

**Decision:** replicate HTTP stripping. WS tests use the `/multicloud` path the plugin actually opens.

### Development `admin-token` cache

`getToken()` in `NODE_ENV=development` reads `admin-token` from `node-localstorage` under `./certs` when the request has no cookie and no `Authorization`. `getAuthenticatedToken` writes that key after a successful `GET /api`. Local `npm run plugins` therefore treats a no-token request as kubeadmin after the first login.

**Decision:** do not replicate in Go (dev-only). Negative catalog cases send `Authorization: Bearer acm-42590-invalid-token` (`auth: invalid`) so they 401 in both development and production. Missing-token 401 is production-only.

### CORS only in non-production

`cors.ts` reflects `Origin`, `Access-Control-Allow-Credentials: true`, and answers OPTIONS with 200.

**Decision:** replicate for standalone/dev. Production plugin traffic is same-origin through Console.

### find-my-way maxParamLength 500

Long Kubernetes names need this; unmatched routes 404.

**Decision:** replicate.

## Fix later (do not block migration)

- DELETED without access check (security).
- Empty error bodies (harder to debug; changing them would break clients that treat any body as JSON).
- Development `admin-token` file cache (do not port).
- Webpack devServer missing `/apiPaths`, `/cluster-version`, `/placement-debug` (plugin proxy is prefix-based so production plugins are fine).
- Dead webpack entries `/multicloud/common`, `/multicloud/console-links` (not backend routes).

## Plugin proxy `authorize: true` and hub alignment

OpenShift Console on :9000 proxies to `https://localhost:4000` with `authorize: true`. The user token is validated against `CLUSTER_API_URL`. If that URL does not match `oc whoami --show-server`, authenticated routes 401 and the UI redirects to `/dashboards`.

**Decision:** not a backend bug. `run-catalog.sh` preflight (and `scripts/check-hub-alignment.sh` when present) fail fast. Fix: `npm run setup:hub`.

## TLS on :4000

Plugin proxy and contract tests use `https://localhost:4000`. Missing `backend/certs/` makes Go serve HTTP; Console logs `tls: first record does not look like a TLS handshake`.

**Decision:** preflight requires certs when the backend URL is HTTPS. `npm run generate-certs` then restart `npm run plugins`.

## Not in Node (do not require against sidecar-only)

`GET /events/rbac` is Go-owned (never served by Node). Catalog case `events-rbac-sse` is **soft** and skips on 404 so a sidecar-only run stays green.

## SSE / WebSocket compare

`CONTRACT_COMPARE_URL` diffs **REST only**. SSE and WebSocket are skipped on purpose (`shouldCompareREST`). A Node-vs-Go SSE shadow-diff (including multi-role RBAC) is **ACM-42598**. The hook is already isolated so 42598 can enable it without changing REST compare.
144 changes: 144 additions & 0 deletions ACM-42568_go_migration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Contract test framework (ACM-42590)

Isolated Go module. Black-box HTTP client against the running console backend (Node today, Go later). Not wired into root `package.json` or CI.

## Quick start (npm run plugins already running)

With `npm run plugins` up and `oc login` done:

```bash
cd ACM-42568_go_migration
./run-catalog.sh
```

Or manually:

```bash
cd ACM-42568_go_migration
export CONTRACT_BACKEND_URL=https://localhost:4000
export CONTRACT_TOKEN=$(oc whoami -t)
go test . -count=1 -timeout 15m -v -run TestCatalogAgainstBackend
```

**You only need the backend on :4000** (started by `npm run plugins`). You do **not** need OpenShift Console on :9000.

Quick check: `curl -sk https://localhost:4000/ping` β†’ `200`.

`./run-catalog.sh` runs a **preflight** before tests: hub alignment (`oc whoami --show-server` vs `CLUSTER_API_URL` in `backend/.env`), TLS certs for HTTPS, and `GET /ping`. `CONTRACT_SKIP_PREFLIGHT=1` disables it.

At the end: colored summary β€” **Executed**, **OK** (green), **SOFT** (yellow), **FAIL** (red). **FAIL must be 0** for a green gate. **SOFT** skips are optional upstreams missing on your hub (normal on dev).

`./run-catalog.sh` prints per-case progress by default (`CONTRACT_VERBOSE=1`). A full run can take several minutes when hub proxies time out (30s per slow case). Set `CONTRACT_VERBOSE=0` for summary-only output. `NO_COLOR=1` disables colors.

## Prerequisites

- Hub: `oc login`
- Backend: `npm run plugins` from repo root (Go on `https://localhost:4000`, Node sidecar on `:4001`)
- Go 1.22+
- `backend/certs/` present (`npm run generate-certs`) when using HTTPS

## Troubleshooting

### Plugin UI redirects to `/dashboards`

`oc whoami --show-server` must match `CLUSTER_API_URL` in `backend/.env`. After `oc login` to a new hub, run `npm run setup:hub` and restart `npm run plugins`. The OpenShift Console plugin proxy on :9000 sends your token with `authorize: true`; a hub mismatch 401s authenticated routes and the frontend logs out.

### `./run-catalog.sh` appears stuck (no output for minutes)

The script runs **~118 HTTP checks** sequentially. Without per-case output it looks frozen. As of the latest `run-catalog.sh`, progress lines print by default (`CONTRACT_VERBOSE=1`). A full run can still take **10–20+ minutes** when hub proxies or aggregate search hit their per-case timeout (30–60s each).

```bash
# Subset while working on long-tail (ACM-42602):
CONTRACT_GROUP=long-tail ./run-catalog.sh

# Faster timeouts (more SOFT skips on a slow hub):
CONTRACT_HTTP_TIMEOUT=20 ./run-catalog.sh
```

If **every** case hits `context deadline exceeded`, restart `npm run plugins` so Go serves the migrated routes (long-tail no longer lives on the Node sidecar). Confirm `curl -sk https://localhost:4000/ping` β†’ `200` and `oc whoami -t` is non-empty.

### `tls: first record does not look like a TLS handshake`

`backend/certs/` is missing, or backends started before certs existed. Run `npm run generate-certs` and restart **both** Go and the Node sidecar (certs are read only at startup).

## What each layer validates

| Layer | Command | Gate for |
|-------|---------|----------|
| REST catalog | `./run-catalog.sh` | Phases 1–2 and later REST migrations. **FAIL: 0** |
| SSE `GET /events` | catalog `events-sse` | **ACM-42598** (still Node today) |
| SSE `GET /events/rbac` | catalog `events-rbac-sse` (soft) | Already Go; 404 skip if hitting sidecar-only |
| Watch spec parity | `go test -run TestWatchedResourcesMatchEventsTS` | YAML vs `events.ts` `definitions` |
| Cache snapshot | `./compare-informer-cache.sh` | **ACM-42597** informer cache |

## Snapshot harness (ACM-42597)

Compares normalized keys `{apiVersion,kind,namespace,name}`. Argo `polled` kinds are excluded. Authentication **is** included (cached, not fanned out on SSE).

```bash
cd ACM-42568_go_migration
./compare-informer-cache.sh
```

The test **skips** (not fail) when `GET /debug/informer-snapshot` is missing (Go cache not wired yet). After ACM-42597:

Go informers start **after** `:4000` is bound. `CONSOLE_INFORMER_CACHE=0` disables them. After sync, the Go process logs `informer cache memory` with `heapAlloc` β€” compare that to the sidecar `INFO:memory` `eventCache` size, not combined RSS. The Go store is uncompressed `unstructured` (managedFields stripped except Policy).

| Variable | Purpose |
|----------|---------|
| `CONTRACT_GO_SNAPSHOT_URL` | Default `{BACKEND}/debug/informer-snapshot` |
| `CONTRACT_NODE_SNAPSHOT_URL` | Optional Node dump URL |
| `CONTRACT_GO_SNAPSHOT_FILE` / `CONTRACT_NODE_SNAPSHOT_FILE` | JSON `{ "items": [...] }` |

Offline unit tests (`TestDiffSnapshots`, `TestExcludePolled`) always run.

## Alternative: through the OCP Console proxy

Only if you also run the full plugin stack with Console on :9000:

```bash
cd ACM-42568_go_migration
export CONTRACT_BACKEND_URL=http://localhost:9000
export CONTRACT_PATH_PREFIX=/api/proxy/plugin/mce/console/multicloud
export CONTRACT_TOKEN=$(oc whoami -t)
go test . -count=1 -timeout 15m
```

For day-to-day contract runs, use **:4000** (quick start above).

## Modes

| Env | Effect |
|-----|--------|
| `CONTRACT_MODE=assert` (default) | Check status, headers, JSON shape, SSE framing, WS upgrade |
| `CONTRACT_COMPARE_URL=https://...` | Replay **REST** cases against a second backend and diff. **Does not diff SSE or WebSocket** (ACM-42598 will add a shadow-diff). |
| `CONTRACT_RECORD=1` | Write captures under `testdata/recorded/` (gitignored) |

Other: `CONTRACT_SSE_TIMEOUT` (default 120s), `CONTRACT_HTTP_TIMEOUT` (default 60s), `CONTRACT_TLS_INSECURE` (default true), `CONTRACT_RECORD_DIR`.

## Catalog

YAML in `catalog/` β€” add a route = add a YAML case.

- `soft: true` β€” skip (not fail) when optional upstream is missing
- `alsoMulticloud: true` β€” also run with `/multicloud` prefix
- `kind: sse` / `kind: websocket` β€” streaming cases
- `auth: invalid` β€” fake Bearer for 401 cases

`catalog/watched-resources.yaml` is the watch-spec catalog: one entry per `events.ts` definition, including `labelSelector`, `fieldSelector`, `polled`, and `forwardEventsToClients`. `source: events-rbac` marks the Go-only ClusterRole informer.

See `QUIRKS.md` for Node behaviors Go should replicate.

## Layout

```text
ACM-42568_go_migration/
β”œβ”€β”€ catalog/
β”œβ”€β”€ run-catalog.sh # preflight + full catalog + colored summary
β”œβ”€β”€ compare-informer-cache.sh # ACM-42597 snapshot gate
β”œβ”€β”€ testdata/recorded/ # gitignored
β”œβ”€β”€ *.go
β”œβ”€β”€ go.mod
β”œβ”€β”€ README.md
└── QUIRKS.md
```
Loading