Skip to content

test(conformance): port the credential gating e2e tests - #5318

Merged
elezar merged 19 commits into
mainfrom
test/5296-credential-gating-pilot/matthewgrossman
Nov 10, 2026
Merged

elezar merged 19 commits into
mainfrom
test/5296-credential-gating-pilot/matthewgrossman

Conversation

@matthewgrossman

@matthewgrossman matthewgrossman commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Port the six credential gating e2e tests from #5264 into the conformance family, and remove the old e2e/rust test. This is the pilot port for #5296. The pieces the port needed that other tests will need too are shared: policy helpers and provider install in testkit, and a new tests/support/host-server crate for servers on the test host. The existing host-gateway feature test now uses them as well.

Related Issue

Closes #5317

Part of #5281. Pilot for #5296, which stays open. Ports the tests from #5264.

What the tests check, and what they needed

The #5264 tests check what the sandbox supervisor does with a provider credential when the sandbox sends traffic out:

  • The gateway rejects a policy that binds a credential to an endpoint it can't inspect (L4-only or tls: skip), unless the endpoint opts in.
  • A REST body sent upstream carries the credential placeholder, or the real secret when request_body_credential_rewrite is on.
  • A WebSocket binary frame, which can't be inspected for the credential, isn't forwarded.
  • Binding a provider with policy set adds its environment variable to a running sandbox, and unbinding removes it.

Porting them needed four things. Each was added as a generic helper, and host-gateway, an existing feature test that had its own copies, now uses the shared ones.

  1. An upstream server that records what arrived. Most of the assertions are about what reached upstream, so the test needs a server that the sandbox can reach and that records what it received. New crate tests/support/host-server (host-server-support):
    • HttpRecorder::start(body) answers every request with 200 and body. wait_for_requests(n) returns the requests, head and body apart, once n have arrived. It reads Content-Length bodies, not chunked ones.
    • WebSocketRecorder::start() completes the WebSocket handshake. It then reads frames until a close frame, the end of the connection, or a 10s timeout. wait_for_handshake() waits for the handshake, and wait_until_finished() waits for the connection to end and reports whether a binary frame arrived.
    • Both listen on the test host, which sandboxes reach as host.openshell.internal, and stop when dropped. It lives in tests/support/, alongside the Keycloak fixture, because it's a service outside OpenShell that any suite can use. host-gateway replaces its hand-written server with HttpRecorder.
  2. Policies with credential settings on network endpoints. Each test needs a slightly different endpoint (protocol: rest or websocket, tls: skip, body rewriting, credential_binding, allow_uninspected_credentials). Before this PR, tests wrote out a whole policy as a YAML string. New testkit module testkit::policy:
    • base() reads base-policy.yaml: the filesystem and process settings every sandbox needs, and no network access.
    • allow(&mut policy, name, host, port, binaries) adds an endpoint. Its Endpoint setters are named after the YAML fields, e.g. .rest(Access::Full).credential_binding(&provider).
    • to_yaml() renders the policy for CLI commands such as policy set.
    • A unit test checks that each helper builds exactly what its YAML parses to.
    • PYTHON_BINARIES names the paths python3 runs from in testkit::python_image() images, which differ between the workload image and the pullable uv image.
    • The sandbox builder can now combine a policy with an image and providers. Before, cx.sandbox(..).policy(..) rejected both, and these tests need all three.
  3. A provider per test, cleaned up afterwards. Each test imports its own provider profile and creates a provider under a name unique to the run. New cx.import_profile(profile_yaml) imports a profile and returns its id, read from the YAML, and cx.create_provider(name, type, credentials) creates a provider. Each registers its cleanup before creating. cx.install_provider(name, profile_yaml, credentials) does both.
  4. Small CLI and exec plumbing. testkit::YamlFile writes YAML to a temporary file for CLI commands that read one (profile import, policy set); host-gateway replaces its PolicyFile copy with it. ExecOutput::success() fails a test on a non-zero exec exit, like CliOutput::success().

Helpers for OpenShell itself (sandboxes, policies, providers, the CLI) go in testkit. Fixtures for services outside OpenShell go in tests/support/.

Changes

  • conformance: add credential_gating, one file with the same six scenarios and unique names as test(e2e): split credential_gating into independent parallel tests #5264, built on the helpers above.
    • The tests assert on the request that reached the host server, not on verdict strings printed by the sandbox client. The REST client is a short urllib POST.
    • Dropped a client-side check: the WebSocket test no longer requires the client to see close code 1008. The original client counted a read timeout as "denied" too, so the check passed whether or not the supervisor closed the connection. The test now requires that the handshake reaches upstream, waits until upstream's connection has ended, and requires that no binary frame arrived on it.
    • The REST tests compare the body upstream exactly (the secret, or the placeholder between the client's prefix and suffix), and check that the secret appears nowhere in the request. Failure messages print the request with the secret redacted.
    • Dropped the "secret reached the sandbox" check, which could never fail.
    • The gateway's rejections are checked through the builder's create() error: the gRPC code must be FailedPrecondition, and the message keeps test(e2e): split credential_gating into independent parallel tests #5264's wording checks. testkit's Error now keeps the gateway's status code (grpc_code(), testkit::GrpcCode). The CLI is used for profile import, provider create and policy set --wait.
  • test image: testkit::python_image() still defaults to openshell/e2e-python:dev. The Docker and Podman runners now build it for conformance, as the tmachine installers already load it. The Kubernetes and VM runners set OPENSHELL_TEST_PYTHON_IMAGE to a pinned public uv Python image unless it is already set. A gateway outside these lanes has to set OPENSHELL_TEST_PYTHON_IMAGE; running conformance against any gateway without an override is a follow-up. host_gateway runs python3 from PATH and allows PYTHON_BINARIES, so it works with either image, and no longer retries its connection for 20s: the credential tests never did, and the server listens before the sandbox is created.
  • external Kubernetes driver lane: the Helm post-renderer now copies the chart's server.hostGatewayIP into the standalone driver's OPENSHELL_HOST_GATEWAY_IP. Before, only the in-process driver got it, so sandboxes on that lane couldn't resolve host.openshell.internal. The lane hadn't run a host-reachability test before. The renderer fails if the lane set the IP but the chart rendered no host.openshell.internal alias.
  • removal: delete e2e/rust/tests/credential_gating.rs, its [[test]] entry, and its PODMAN_CI_TESTS entry.
  • docs: the testkit README lists the new helpers. The conformance README and TESTING.md list the suite and state that conformance requires the machine running it to be reachable from sandboxes as host.openshell.internal.

Gaps found by the pilot (for #5296)

  • Step logging: there is no per-step progress output. A slow step shows up only as the test's total time.
  • Sandbox names: Podman caps sandbox names at 19 characters, which leaves 5 for cx.sandbox(suffix). testkit doesn't check this before sending the request.
  • Error-text assertions: the admission test matches phrases in the gateway's error ("credentialed endpoint", "L4-only", "tls:"/"skip"), as test(e2e): split credential_gating into independent parallel tests #5264 does. That is stricter than the conformance README's guidance on asserting behavior over wording.
  • Remote clusters: host servers run on the test host, so remote Kubernetes or OpenShift gateways can't reach them.
  • Host lanes on tmachine: the Docker and Podman host lanes still run through e2e/rust/e2e-*.sh scripts, not tmachine.

Testing

  • cargo fmt and cargo clippy --locked --all-targets -D warnings on testkit, support, features and conformance.

  • testkit unit tests: 43 passed.

  • check_cargo_lockfile_alignment.py and check-conformance-program.sh (22 tests) pass.

  • Local Podman (macOS, e2e/with-podman-gateway.sh): openshell-conformance run credential_gating passed 6/6 in 33.7s. host_gateway passed.

    Test Time
    policy_binding_requires_inspection_or_opt_in 2.1s
    policy_binding_rest_body_keeps_placeholder_without_rewrite 2.7s
    policy_binding_rest_body_rewrite_sends_secret_upstream 1.9s
    policy_binding_updates_provider_environment_live 22.4s
    profile_rest_endpoint_keeps_body_placeholder 2.8s
    profile_websocket_endpoint_denies_binary_frames 1.8s
  • Not run locally: Docker (Docker Desktop requires sign-in), k3s, k3s-ha. CI covers these rows.

CI: copy-pr-bot doesn't sync draft PRs, so E2E runs only after a maintainer comments /ok to test <sha>. In #5264, the WebSocket test took 60–120s on Kubernetes. If k3s or k3s-ha hit the per-test timeout, I'll raise it here rather than change .config/nextest.toml (see #5295).

  • Checks appropriate to the affected code and behavior pass
  • Unit tests added/updated (if applicable)
  • E2E tests added/updated (if applicable)

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (if applicable)

@matthewgrossman matthewgrossman added the test:e2e Requires end-to-end coverage label Nov 5, 2026
@copy-pr-bot

copy-pr-bot Bot commented Nov 5, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

Label test:e2e applied, but pull-request/5318 does not exist yet. A maintainer needs to comment /ok to test 1472ee7b123f994b562336888e2bb2fea87bb91d to mirror this PR. Once the mirror exists, re-apply the label or re-run Branch E2E Checks from the Actions tab.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

Base automatically changed from test/5275-migration-prep/matthewgrossman to main November 6, 2026 13:41
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Port the six credential gating scenarios from e2e/rust/tests/credential_gating.rs
into a conformance suite. Each test imports its own provider profile and
creates a provider under a run-unique name, creates sandboxes from the
Python test image through the CLI, and reaches probe servers on the host
through host.openshell.internal.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
The conformance suite now creates sandboxes from testkit::python_image().
Docker and Podman runners build openshell/e2e-python:dev also in
e2e-conformance-only mode, and the Kubernetes and VM runners default
OPENSHELL_TEST_PYTHON_IMAGE to the pullable uv Python image.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Add testkit::policy (a base policy from base-policy.yaml, allow(), and
Endpoint settings), install_provider(), YamlFile, ExecOutput::success(),
and PYTHON_BINARIES, so suites build policies and providers the same way.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Add host-server-support with HttpRecorder and WebSocketRecorder, servers on
the test host that sandboxes reach through host.openshell.internal. Move the
host-gateway feature test onto them and onto testkit's base policy.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Use testkit's policy helpers and install_provider and host-server-support's
recorders, and assert on the requests that reached the host server instead
of verdicts the sandbox clients print.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman
matthewgrossman force-pushed the test/5296-credential-gating-pilot/matthewgrossman branch from 8e5c8d7 to 566b39d Compare November 6, 2026 18:59
@matthewgrossman

Copy link
Copy Markdown
Member Author

/ok to test 566b39d

The external-driver lane renders server.hostGatewayIP, but only the
in-process driver reads it from the gateway TOML. The standalone driver
takes OPENSHELL_HOST_GATEWAY_IP, which the post-renderer never set, so
sandbox policy DNS refused host.openshell.internal on that lane.

Copy the chart's rendered value into the driver container when present.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman

Copy link
Copy Markdown
Member Author

/ok to test 99bbed4

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

Compatibility review (advisory)

New or updated breaking changes; see the full report for all findings.

1. cli — No explicit risk acceptance found

OPENSHELL_E2E_KUBERNETES_FEATURES=e2e,e2e-kubernetes mise run e2e:kubernetes previously excluded host-dependent credential tests but now runs them unconditionally through openshell-conformance.

  • Impact: Remote Kubernetes/OpenShift operators and driver authors encounter failed connections to host.openshell.internal and conformance exit 1 even when their gateway satisfies the existing contract.
  • Action: For v0.1.8, preserve the existing host-gateway opt-in or move these tests to a feature suite; the new warning does not address this patch-train break.
  • PR: test(conformance): port the credential gating e2e tests #5318
Review details
  • Execution: complete
  • Candidate: PR #5318 (c29689d502b2329fb012721ec158f81b7e83c8e1)
  • Baseline: cc4acdcfdeda9fdeeb0fe94a537d47ad8414eb3d (cc4acdcfdeda9fdeeb0fe94a537d47ad8414eb3d)
  • Reviewed: rust-sdk, python-sdk, go-sdk, typescript-sdk, api-behavior, cli, configuration, policy, helm, state

Advisory review; publication gates are unchanged.

Branch check run

Conformance has to run against any gateway, and the unpublished
openshell/e2e-python:dev image only exists where a lane built or loaded
it. Default to the pinned uv Python image, which any gateway can pull,
and keep OPENSHELL_TEST_PYTHON_IMAGE as the override.

Host lanes no longer build the workload image for conformance. The base
test policy names no workload user, so drivers use a default non-root
user on images without one, and host_gateway runs python3 from PATH.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman

Copy link
Copy Markdown
Member Author

/ok to test 61504a3

- The WebSocket recorder reads frames until the connection ends, and the
  test waits for that before checking that no binary frame arrived.
- The HTTP recorder keeps the request head and body apart. The REST tests
  compare the body exactly and check the secret appears nowhere, and
  failure messages redact it.
- testkit errors keep the gateway's gRPC status code, and the admission
  test checks for FailedPrecondition.
- The policy create request destructures the SDK spec, so a new field
  fails to compile instead of being dropped.
- host_gateway no longer retries its connection.
- Docs state that conformance requires sandboxes to reach the test
  machine as host.openshell.internal.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman

Copy link
Copy Markdown
Member Author

/ok to test 3a31220

@elezar elezar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the port. The recorder-based assertions are a clear improvement on the old verdict strings, and keeping all six scenarios while moving them onto shared helpers is the right shape for the pilot.

I'd like two things changed before this merges, plus some smaller suggestions inline:

  1. Drop 61504a338 (the default Python image change) and handle "conformance runs on any gateway" in a follow-up.
  2. Give the WebSocket test a positive control, so the denial assertion can fail.

Remote-cluster behaviour (inline comment on TESTING.md) is fine as a tracked follow-up.

Comment thread tests/testkit/testkit/src/image.rs Resolved
Comment thread TESTING.md Resolved
Comment thread tests/support/host-server/src/lib.rs Resolved
Comment thread tests/testkit/testkit/src/provider.rs Resolved
Comment thread tests/testkit/testkit/src/policy.rs Resolved
Comment thread tests/testkit/testkit/src/context.rs Resolved
Comment thread tests/testkit/testkit/src/image.rs Resolved
@matthewgrossman
matthewgrossman marked this pull request as ready for review November 9, 2026 16:29
…mage

The tmachine installers load it and the Docker and Podman lanes build it,
so those lanes don't pull an image inside a test's timeout or lose curl,
git and openssl. The Kubernetes and VM lanes keep using the pinned public
image through OPENSHELL_TEST_PYTHON_IMAGE, and the base test policy names
the sandbox user again. Running conformance against any gateway without
an override is left to a follow-up.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman

Copy link
Copy Markdown
Member Author

/ok to test 9ab1be1

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Compatibility review (advisory)

New or updated breaking changes; see the full report for all findings.

1. cli — Risk explicitly accepted

OPENSHELL_E2E_KUBERNETES_FEATURES=e2e,e2e-kubernetes mise run e2e:kubernetes previously excluded host-dependent credential tests but now runs them unconditionally through openshell-conformance run --junit &lt;path&gt;.

  • Impact: Remote Kubernetes/OpenShift operators and driver authors now encounter failed connections to host.openshell.internal and conformance exit 1 despite satisfying the previously supported gateway contract.

  • Action: The earlier host-gateway opt-in or feature-suite remediation remains necessary for v0.1.8; the documented restriction and accepted follow-up do not resolve this patch-train violation.

  • PR: test(conformance): port the credential gating e2e tests #5318

  • Acceptance: comment

Review details
  • Execution: complete
  • Candidate: PR #5318 (328cde47374d56cd8cbd425adc3dd8a2ad2d210c)
  • Baseline: cc4acdcfdeda9fdeeb0fe94a537d47ad8414eb3d (cc4acdcfdeda9fdeeb0fe94a537d47ad8414eb3d)
  • Reviewed: rust-sdk, python-sdk, go-sdk, typescript-sdk, api-behavior, cli, configuration, policy, helm, state

Advisory review; publication gates are unchanged.

Branch check run

The test passed whenever no binary frame reached upstream, whatever the
reason. It now first sends the same frame from a sandbox without the
provider and requires that it arrives, so the denial check can fail.
Unit tests check that WebSocketRecorder sees a binary frame after a long
text frame, and none among text frames.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman

Copy link
Copy Markdown
Member Author

/ok to test 4ff621b

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman

Copy link
Copy Markdown
Member Author

/ok to test e5e72bf

Use tungstenite's derive_accept_key instead of hashing with sha1 and
base64 directly, and split host-server into http.rs and websocket.rs.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
… is missing

The post-renderer copied the host gateway IP whenever the chart rendered
any hostAliases. It now matches the host.openshell.internal entry, and
fails if the lane set server.hostGatewayIP but no such entry rendered.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
…r create

import_profile reads the profile's id from its YAML and returns it, and
create_provider takes the provider name and type separately, so the
provider name no longer has to match the profile id. install_provider
does both. Document that policy::allow merges binaries under one name
and that an override Python image must use a PYTHON_BINARIES path.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman

Copy link
Copy Markdown
Member Author

/ok to test e22087f

…gating-pilot/matthewgrossman

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman

Copy link
Copy Markdown
Member Author

/ok to test aeec397

testkit creates things through Context methods, such as cx.sandbox(..)
and cx.on_cleanup(..), so import_profile, create_provider and
install_provider become cx methods instead of free functions.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman

Copy link
Copy Markdown
Member Author

/ok to test 6ebaf4d

…gating-pilot/matthewgrossman

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman

Copy link
Copy Markdown
Member Author

/ok to test 16e7f7e

@elezar elezar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving. Both of my requested changes are addressed (the default image is back to openshell/e2e-python:dev, and the WebSocket test has a positive control), and the six credential-gating tests passed on the Kubernetes workspace-managed, Kubernetes external-driver, VM and k3s-ha lanes in run 42763840495.

Remote clusters are still a known gap, tracked in #5409, where I added a proposal to move the four host-dependent tests to a features suite.

Nits, non-blocking:

  • Use Error::from_status at the remaining from_error(&status) sites in context.rs (:217, :559, :576, :594), so grpc_code() is kept wherever a tonic::Status is mapped.
  • HttpRecorder has no unit tests. A few covering Content-Length bodies and the chunked case would match what the WebSocket recorder has.

@elezar
elezar added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit eee2b9f Nov 10, 2026
152 checks passed
@elezar
elezar deleted the test/5296-credential-gating-pilot/matthewgrossman branch November 10, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:e2e Requires end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(conformance): port the credential gating e2e tests

2 participants