Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Unused MCP Edge in system-test stacks

The application fixtures keep a Connection Service URL for the standalone MCP Edge. The required Pod scenarios, VM suites and Firecracker suite do not exercise external MCP Connections; they use Actions' command-box flow or ordinary Sandbox behavior. Those stacks were still rendering the MCP Edge Deployment and importing its image.

`FixtureProfile.NO_MCP_EDGE` removes the edge construct for the `pod_scenarios`, VM and Firecracker fixtures. The app keeps its unused Connection routes and reports the service unavailable. Full `pod`, deletion, real-model and MCP fixture profiles retain the edge. The image manifest selects common service images separately from the MCP Edge image, so the omitted construct also removes the configured OCI image input. Actions and its command-box executor remain in every fixture.

The renderer tests compare the Pod fixture with the full fixture and require every retained object to be identical, with only edge-named resources absent. They also compare VM and Firecracker output against full renderings, and assert that the full default and MCP fixtures still contain the real edge deployment/configuration. These tests are the scoped rendering proof; they do not exercise a running stack or measure wall-time savings. The change preserves scenario bodies and test frequency.

The architecture branch also exercised the unchanged Pod, full VM and both Firecracker cases at source `91f72591a1`, and a [synthetic selector probe](https://app.buildbuddy.io/invocation/c51d318c-90ea-4fe0-a892-6110260728ef) selected 15 rather than 18 tests for one isolated MCP-edge source change: only those three unrelated suites disappeared. MCP acceptance remained selected. That is source-specific graph evidence; the backport requires its own exact-head CI, and no representative workload frequency or wall-time saving is inferred.
16 changes: 16 additions & 0 deletions agentplane/system_tests/deployment/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,19 @@ py_test(
size = "medium",
srcs = ["test_render.py"],
data = [
":rendered/local-model.json",
":rendered/local-model-firecracker.json",
":rendered/local-model-mcp-fixtures.json",
":rendered/local-model-pod.json",
":rendered/local-model-vm.json",
"@nodejs_linux_amd64//:bin/node",
],
env = {
"JSII_NODE": "$(rootpath @nodejs_linux_amd64//:bin/node)",
"K3S_DEFAULT_FIXTURE_RLOCATION": "$(rlocationpath :rendered/local-model.json)",
"K3S_FIXTURE_RLOCATION": "$(rlocationpath :rendered/local-model-mcp-fixtures.json)",
"K3S_FIRECRACKER_FIXTURE_RLOCATION": "$(rlocationpath :rendered/local-model-firecracker.json)",
"K3S_POD_FIXTURE_RLOCATION": "$(rlocationpath :rendered/local-model-pod.json)",
"K3S_VM_FIXTURE_RLOCATION": "$(rlocationpath :rendered/local-model-vm.json)",
},
deps = [
Expand Down Expand Up @@ -387,6 +391,18 @@ genrule(
visibility = ["//agentplane/system_tests:__subpackages__"],
)

genrule(
name = "render_local_model_pod",
testonly = True,
outs = ["rendered/local-model-pod.json"],
cmd = "JSII_NODE=$(execpath @nodejs_linux_amd64//:bin/node) K3S_FIXTURE_VARIANT=local-model-pod K3S_FIXTURE_DIR=$(RULEDIR)/rendered $(execpath :render_fixture_bin)",
tools = [
":render_fixture_bin",
"@nodejs_linux_amd64//:bin/node",
],
visibility = ["//agentplane/system_tests:__subpackages__"],
)

genrule(
name = "render_local_model_vm",
testonly = True,
Expand Down
15 changes: 9 additions & 6 deletions agentplane/system_tests/deployment/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ delete an image before a later Sandbox uses it; these fixture-local tags cannot
At import-group boundaries, the stack removes untagged, unpinned CRI images
that no container uses. Named caches stay warm, including k3s's own images. Before importing, it also
removes unused fixture-local images (`git.allegedly.works/ducktape-ci/…`) the target's manifest does
not declare: Pod and MCP suites share a recycling key, and pinned MCP layers would otherwise stay in a
Pod stack's tmpfs. An MCP suite after a Pod suite reimports them. Cleanup also runs with
not declare: Pod and MCP suites share a recycling key, while their stack profiles can select
different service images. A later MCP suite imports any edge or fixture-server images it needs.
Cleanup also runs with
the kubelet stopped during a cold rebuild, before restored obsolete layers can trigger evictions
while a fresh kubelet waits for its image-GC minimum age. See the
[storage-pressure reproduction](../k3s/debug/2026_11_02_image_store_pressure.md).
Expand Down Expand Up @@ -136,10 +137,12 @@ Actions and Notifications apply Goose migrations from their serving processes be
listeners or starting workers. Their startup probes wait for initialization; the fixture renders no
separate migration containers for either service.
The fixture selects that runtime explicitly; staging selects its own independently.
The standalone MCP edge runs in every stack because the app's Connection Service endpoint targets it;
its own PostgreSQL database, embedded Goose migrations, caller-token RBAC and startup/readiness
probes are rendered through the shared construct. The serving binary migrates before opening its
gRPC listener. MCP fixture stacks add the edge OAuth Secret and Dex trust.
The full `pod`, `deletion`, `real_model` and `mcp` stack profiles include the standalone MCP edge
because the app's Connection Service endpoint targets it. Its PostgreSQL database, embedded Goose
migrations, caller-token RBAC and startup/readiness probes are rendered through the shared
construct. The `pod_scenarios`, VM and Firecracker profiles omit the edge because their scenarios do not exercise
external Connections; the app retains its unused Connection routes and reports the service
unavailable. MCP fixture stacks still include the edge and add its OAuth Secret and Dex trust.
One PostgreSQL 18 Deployment uses the
pinned published CNPG minimal image, an emptyDir data volume and
[`postgres_entrypoint.sh`](postgres_entrypoint.sh) with [`bootstrap.sql`](bootstrap.sql) to create
Expand Down
55 changes: 39 additions & 16 deletions agentplane/system_tests/deployment/fixture_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import json
from copy import deepcopy
from dataclasses import dataclass, replace
from enum import StrEnum
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -159,12 +160,19 @@ def add_secret(
)


class FixtureProfile(StrEnum):
FULL = "full"
# These scenarios do not exercise external MCP Connections. The app retains unused
# Connection routes and reports Connection Service unavailable in system status.
NO_MCP_EDGE = "no-mcp-edge"


@dataclass(frozen=True)
class FixtureChart:
chart: Chart
app: Construct
actions: Construct
mcp_edge: Construct
mcp_edge: Construct | None
app_login: dex.StaticClient
mcp_linkage: dex.StaticClient

Expand Down Expand Up @@ -386,6 +394,7 @@ def objects(
vm_sandboxes: bool,
firecracker_sandboxes: bool = False,
central_sampler: bool = False,
profile: FixtureProfile = FixtureProfile.FULL,
) -> list[dict[str, Any]]:
"""Synthesize the common one-node fixture with optional VM or production Firecracker Sandboxes."""
if vm_sandboxes and firecracker_sandboxes:
Expand All @@ -398,7 +407,13 @@ def objects(
central_sampler=central_sampler,
)
fixture = build_chart(
ca_cert=ca_cert, ca_key=ca_key, system_roots=system_roots, cluster_ca=cluster_ca, models=models, env=env
ca_cert=ca_cert,
ca_key=ca_key,
system_roots=system_roots,
cluster_ca=cluster_ca,
models=models,
env=env,
profile=profile,
)
return synthesize(fixture.chart, vm_sandboxes=vm_sandboxes)

Expand Down Expand Up @@ -433,7 +448,14 @@ def _add_fixture_server_dns_names(documents: list[dict[str, Any]]) -> None:


def build_chart(
*, ca_cert: str, ca_key: str, system_roots: str, cluster_ca: str, models: Models, env: Environment
*,
ca_cert: str,
ca_key: str,
system_roots: str,
cluster_ca: str,
models: Models,
env: Environment,
profile: FixtureProfile = FixtureProfile.FULL,
) -> FixtureChart:
"""Construct the shared stack and return its chart before synthesis for fixture-specific additions."""
app_origin = str(env.app.origin)
Expand Down Expand Up @@ -483,9 +505,6 @@ def build_chart(
.key("uri")
.env_value(chart, "session-dispatcher-database-url")
)
mcp_edge_database_url = (
SecretRef(namespace=NAMESPACE, name="postgres-mcp-edge").key("uri").env_value(chart, "mcp-edge-database-url")
)
# Control-plane `memory_request`s below are the measured peak working set (`memory/peaks.json`
# in a suite's undeclared outputs) plus about half again, so the node keeps room for Sandboxes.
llm_ingress.LlmIngress(
Expand Down Expand Up @@ -599,16 +618,20 @@ def build_chart(
memory_request=Size.mebibytes(48),
memory_limit=None,
)
mcp_edge_service = mcp_edge.McpEdge(
chart,
"mcp-edge",
env,
database_url=mcp_edge_database_url,
placement=None,
registry_secret_name=None,
image_pull_policy=ImagePullPolicy.IF_NOT_PRESENT,
memory_request=Size.mebibytes(32),
)
mcp_edge_service: Construct | None = None
if profile == FixtureProfile.FULL:
mcp_edge_service = mcp_edge.McpEdge(
chart,
"mcp-edge",
env,
database_url=SecretRef(namespace=NAMESPACE, name="postgres-mcp-edge")
.key("uri")
.env_value(chart, "mcp-edge-database-url"),
placement=None,
registry_secret_name=None,
image_pull_policy=ImagePullPolicy.IF_NOT_PRESENT,
memory_request=Size.mebibytes(32),
)
environment_sandbox.EnvironmentSandbox(
chart,
"environment-sandbox",
Expand Down
1 change: 1 addition & 0 deletions agentplane/system_tests/deployment/mcp_fixture_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def objects(
fixture = fixture_chart.build_chart(
ca_cert=ca_cert, ca_key=ca_key, system_roots=system_roots, cluster_ca=cluster_ca, models=models, env=env
)
assert fixture.mcp_edge is not None
fixture_chart.add_secret(
fixture.chart,
_MCP_EDGE_OAUTH_SECRET,
Expand Down
31 changes: 27 additions & 4 deletions agentplane/system_tests/deployment/render_fixture.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Render one ordinary or VM fixture chart into `$K3S_FIXTURE_DIR`.

`K3S_FIXTURE_VARIANT` selects `local-model`, `public-litellm`, `local-model-vm`, or
`local-model-firecracker`.
`K3S_FIXTURE_VARIANT` selects an ordinary, Pod, VM or Firecracker fixture.
The selector is an environment variable because the app's Settings parse `sys.argv` as their own.
"""

Expand All @@ -28,6 +27,7 @@

class FixtureVariant(StrEnum):
LOCAL_MODEL = "local-model"
LOCAL_MODEL_POD = "local-model-pod"
LOCAL_MODEL_VM = "local-model-vm"
LOCAL_MODEL_FIRECRACKER = "local-model-firecracker"
PUBLIC_LITELLM = "public-litellm"
Expand Down Expand Up @@ -56,6 +56,7 @@ def _render(
vm_sandboxes: bool,
firecracker_sandboxes: bool = False,
central_sampler: bool = False,
profile: fixture_chart.FixtureProfile = fixture_chart.FixtureProfile.FULL,
) -> None:
models = MODEL_PLACEHOLDERS[backend]
fixture = RenderedFixture(
Expand All @@ -81,6 +82,7 @@ def _render(
vm_sandboxes=vm_sandboxes,
firecracker_sandboxes=firecracker_sandboxes,
central_sampler=central_sampler,
profile=profile,
),
)
write_fixture(output / f"{variant}.json", fixture, models=models, mcp=None)
Expand All @@ -90,10 +92,31 @@ def render_variant(output: Path, variant: FixtureVariant) -> None:
match variant:
case FixtureVariant.LOCAL_MODEL:
_render(output, variant, Backend.LOCAL_MODEL, vm_sandboxes=False)
case FixtureVariant.LOCAL_MODEL_POD:
_render(
output,
variant,
Backend.LOCAL_MODEL,
vm_sandboxes=False,
profile=fixture_chart.FixtureProfile.NO_MCP_EDGE,
)
case FixtureVariant.LOCAL_MODEL_VM:
_render(output, variant, Backend.LOCAL_MODEL, vm_sandboxes=True)
_render(
output,
variant,
Backend.LOCAL_MODEL,
vm_sandboxes=True,
profile=fixture_chart.FixtureProfile.NO_MCP_EDGE,
)
case FixtureVariant.LOCAL_MODEL_FIRECRACKER:
_render(output, variant, Backend.LOCAL_MODEL, vm_sandboxes=False, firecracker_sandboxes=True)
_render(
output,
variant,
Backend.LOCAL_MODEL,
vm_sandboxes=False,
firecracker_sandboxes=True,
profile=fixture_chart.FixtureProfile.NO_MCP_EDGE,
)
case FixtureVariant.PUBLIC_LITELLM:
_render(output, variant, Backend.PUBLIC_LITELLM, vm_sandboxes=False)

Expand Down
71 changes: 67 additions & 4 deletions agentplane/system_tests/deployment/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import os
from dataclasses import asdict
from pathlib import PurePosixPath
from typing import Any
from urllib.parse import urlsplit
Expand Down Expand Up @@ -65,16 +66,16 @@ def _host_port(address: str) -> tuple[str, int]:
return parsed.hostname, parsed.port


def _filled_fixture(runfiles: Runfiles) -> rendered_fixture.RenderedFixture:
cached = rendered_fixture.read(runfiles.root() / os.environ["K3S_FIXTURE_RLOCATION"])
def _filled_fixture(runfiles: Runfiles, location: str = "K3S_FIXTURE_RLOCATION") -> rendered_fixture.RenderedFixture:
cached = rendered_fixture.read(runfiles.root() / os.environ[location])
return rendered_fixture.fill(
cached,
RuntimeInputs(
ca_cert="fixture-ca\n", ca_key="fixture-key\n", system_roots="system-roots\n", cluster_ca="cluster-ca\n"
),
_TEST_LOCAL_MODEL,
_MCP,
None,
_MCP if cached.mcp_fixtures is not None else None,
"test-operator-public-key" if cached.operator_tokens is not None else None,
)


Expand All @@ -88,6 +89,68 @@ def vm_fixture(_vm_fixture_template: rendered_fixture.RenderedFixture) -> render
return _vm_fixture_template.model_copy(deep=True)


def test_pod_profile_omits_only_mcp_edge_and_keeps_app_and_action_flows(runfiles: Runfiles) -> None:
ordinary = _filled_fixture(runfiles, "K3S_DEFAULT_FIXTURE_RLOCATION")
pod = _filled_fixture(runfiles, "K3S_POD_FIXTURE_RLOCATION")
assert pod.model_dump(exclude={"objects"}) == ordinary.model_dump(exclude={"objects"})
ordinary_objects = {(doc["kind"], doc["metadata"]["name"]): doc for doc in ordinary.objects}
pod_objects = {(doc["kind"], doc["metadata"]["name"]): doc for doc in pod.objects}
removed = ordinary_objects.keys() - pod_objects.keys()
assert ("Deployment", "mcp-edge") in removed
assert ("ConfigMap", "mcp-edge-settings") in removed
assert ("ServiceAccount", "mcp-edge") in removed
assert ("Role", "mcp-edge") in removed
assert ("RoleBinding", "mcp-edge") in removed
assert all(name.startswith("mcp-edge") for _, name in removed)
# Retained resources are identical, including the app, Actions' command-box executor,
# notifications, policy, certificates and storage.
assert pod_objects == {key: value for key, value in ordinary_objects.items() if key not in removed}
assert not any(
"agentplane-mcp-edge:" in container["image"]
for doc in pod.objects
if doc["kind"] == "Deployment"
for container in doc["spec"]["template"]["spec"]["containers"]
)
_resource(pod.objects, "Deployment", "actions")
_resource(pod.objects, "Deployment", "notifications")
# The app keeps its unused Connection routes; external Connections are out of scope here.
app_config = yaml.safe_load(_resource(pod.objects, "ConfigMap", "app-config")["data"]["config.yaml"])
assert AnyHttpUrl(app_config["connection_service_url"]) == AnyHttpUrl(fixture_chart.MCP_EDGE_SERVICE.url)


@pytest.mark.parametrize("location", ["K3S_DEFAULT_FIXTURE_RLOCATION", "K3S_FIXTURE_RLOCATION"])
def test_default_and_mcp_profiles_keep_real_connection_service(runfiles: Runfiles, location: str) -> None:
fixture = _filled_fixture(runfiles, location)
edge = _resource(fixture.objects, "Deployment", "mcp-edge")
assert any(
"agentplane-mcp-edge:" in container["image"] for container in edge["spec"]["template"]["spec"]["containers"]
)
_resource(fixture.objects, "ConfigMap", "mcp-edge-settings")
_resource(fixture.objects, "Role", "mcp-edge")
_resource(fixture.objects, "RoleBinding", "mcp-edge")


@pytest.mark.parametrize("location", ["K3S_VM_FIXTURE_RLOCATION", "K3S_FIRECRACKER_FIXTURE_RLOCATION"])
def test_virtualized_profiles_preserve_every_resource_except_unused_mcp_edge(runfiles: Runfiles, location: str) -> None:
fixture = rendered_fixture.read(runfiles.root() / os.environ[location])
full = fixture_chart.objects(
**asdict(rendered_fixture.PLACEHOLDERS),
operator_token_key=rendered_fixture.OPERATOR_TOKEN_KEY_PLACEHOLDER,
models=rendered_fixture.MODEL_PLACEHOLDERS[rendered_fixture.Backend.LOCAL_MODEL],
vm_sandboxes=fixture.vm_sandboxes,
firecracker_sandboxes=fixture.firecracker_sandboxes,
)
control = {(doc["kind"], doc["metadata"]["name"]): doc for doc in full}
candidate = {(doc["kind"], doc["metadata"]["name"]): doc for doc in fixture.objects}
removed = control.keys() - candidate.keys()
assert ("Deployment", "mcp-edge") in removed
assert all(name.startswith("mcp-edge") for _, name in removed)
assert candidate == {key: value for key, value in control.items() if key not in removed}
_resource(fixture.objects, "Deployment", "actions")
template = environment_firecracker.NAME if fixture.firecracker_sandboxes else SUPERVISION_TEMPLATE
_resource(fixture.objects, "SandboxTemplate", template)


def test_direct_fixture_uses_gateway_and_embedded_migrations(runfiles: Runfiles) -> None:
fixture = _filled_fixture(runfiles)
assert fixture.central_sampler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"local-model.json",
"local-model-firecracker.json",
"local-model-mcp-fixtures.json",
"local-model-pod.json",
"local-model-vm.json",
"public-litellm.json",
}
Expand Down
Loading
Loading