A change to a service template can produce valid Kubernetes manifests and still break every request to an application. Suppose the template gives a Service the wrong pod selector. The gateway controller accepts its route, the application is running, and the Service has no ready endpoints. I want platform tests that catch that failure before an application team has to diagnose it.
This article describes how to structure your internal platform around the capabilities you offer your customers, the components that deliver them, and the systems or applications those components use. I then show how to run tests of their intended behaviour locally, giving agents a way to verify their work before merging.
Illustrative relationships: systems support components, and components deliver capabilities. The public HTTPS capability also needs DNS and certificate handling beyond the routing components shown here.
Full diagram description
Illustrative composition from capabilities to components to systems. Exposing a service over HTTPS depends on a gateway controller and a service template. Running a container depends on the same service template and a cluster foundation. Delivering a service from Git depends on a delivery controller and the service template. The gateway controller uses Envoy Gateway and Gateway API; the service template uses Helm and Kubernetes; the cluster foundation uses GKE and VPC and DNS; the delivery controller uses Argo CD. Shared components can deliver more than one capability.
The example is hypothetical. The design below is how I would describe and test a platform's behaviour, with illustrative files and test results.
What the application team expects#
An application engineer asks for a service to be reachable over HTTPS. They shouldn't need to understand the gateway controller's configuration to find out what the platform supports. They need an accepted input, an expected result and the conditions under which that result holds.
I call that a capability. "Expose a service over HTTPS" is a capability; so is "deliver a service from Git". Each describes an outcome an application team can use.
A component is the part of the platform I maintain to deliver that outcome. A service template includes the templates, configuration rules and supported inputs that turn an application declaration into Kubernetes resources. A gateway component includes its controller configuration and the interface I support for routing traffic. Its boundary gives me somewhere to make a change, own a failure and test the behaviour.
In this article, I use system for the underlying technology. Envoy Gateway implements the gateway controller, Helm renders templates, and Kubernetes runs the workloads. Gateway API defines the route resources those components exchange. These technologies play different roles; the diagram groups them to show what each maintained component depends on.
Argo CD makes the distinction useful. A delivery component includes Argo CD plus the platform's reconciliation configuration and conventions. The capability is that an application team's change in Git reaches the cluster. Naming Argo CD alone doesn't explain which repositories the platform accepts or how it reports a failed deployment.
The service template appears under several capabilities because it contributes to several outcomes. That matters when it changes: a template edit can affect both whether an application runs and whether requests reach it.
Two checks, different answers#
For the routing example, the application team submits this declaration:
name: echo
image: example/echo
port: 8080
http:
hostname: echo.testThe template renders a Deployment, Service and HTTPRoute. The controller uses the route to configure the gateway. example/echo is a hypothetical test application, or fixture: for this example, its response body is echo-fixture. That gives the test a way to recognise the intended application.
A controller check supplies a known Service and HTTPRoute directly. It checks Accepted=True and ResolvedRefs=True for the fixture Gateway. Those conditions describe the controller's handling of the route and references. The Gateway API specification defines that status for each route parent.
A capability check starts further back, with the application declaration. It uses the service template to create the resources, then sends a plain HTTP request to the local gateway with Host: echo.test and expects the fixture's response. That exercises the connection between the template and gateway component.
With the wrong selector from the opening example, the results could look like this:
| Check | Illustrative result | What it establishes |
|---|---|---|
| Controller accepts the fixture route | Pass | The route and its references are accepted |
| Request reaches the declared application | Fail: echo-fixture is absent | The generated application resources do not deliver the routing promise |
The controller can accept a reference to a Service that exists while that Service selects no pods. Repeating the controller check wouldn't answer the missing question. The capability check needs to make the request.
A 200 response alone is also too weak. Another application could return it. I would check the expected response body as well as the status, and keep the rendered Service selector and endpoint information when the request fails.
This local check covers hostname routing. Public HTTPS needs more evidence: public DNS must resolve to the intended frontend, the TLS certificate must be valid for the hostname, and the request must reach the application through that frontend. I would test those requirements in a cloud sandbox. The opening diagram shows some implementation relationships; it doesn't list every dependency needed for public HTTPS.
Write the guarantee before the test#
I would write the routing promise down as a contract so a component replacement has something explicit to preserve. For this example, a contract could look like this:
capability: gateway
version: v1
guarantees:
- id: hostname-routing
given:
- Valid service declaration for echo.test
- Ready echo fixture listening on the declared port
expect: Requests for echo.test reach the echo fixture
checks:
- hostname-routing-local
- id: unmatched-hostname
given:
- The fixture Gateway and echo service are ready
expect: Requests for unknown.test return HTTP 404 without the echo fixture marker
checks:
- unmatched-hostname-local
exclusions:
- Availability of an unhealthy application backend
- Request ordering across backend replicas
cloudValidation:
requiredBeforePublicSupport:
- Public DNS, TLS and cloud frontend checksA guarantee names the conditions and expected observation. A check is the executable attempt to establish that guarantee in a particular environment. Here, hostname-routing-local is the test that starts with the service declaration and looks for the fixture response.
The unknown-hostname rule is a deliberate choice for this proposed contract. I expect unknown.test to return 404 without the fixture marker; I wouldn't assume every gateway implementation has that behaviour. The exclusions also matter: a test of platform routing needs a healthy application fixture, and this contract makes no promise about request ordering across replicas.
I would keep separate contracts for components and capabilities. The component contract records the gateway interface I maintain. The capability contract records the outcome the application team depends on. Changing a controller or its configuration should preserve both where they apply.
Select tests from the relationships#
Validate definitions, select checks, render a candidate, execute in local or sandbox scopes, and report evidence for review.
Full diagram description
The capability, component and contract files feed a validator and test selector. The runner renders the candidate, executes checks in local or sandbox scopes, and writes evidence to a report.
I would store the specification beside the implementation in Git. A component definition records its implementation paths, prerequisites and contract. A capability definition names the components that deliver it. Each check records the guarantee it tests, its suite and whether it runs locally or in a cloud sandbox.
Those references let the runner select checks for a change. An edit to implementation/service-template/ selects the template's own checks and the capability checks that consume it. An edit to the gateway component selects the controller checks and routing capability checks. Shared fixtures, renderers and test code need mappings too, so a change to the tests themselves can't escape the selection.
I would validate those references before creating a test environment. Duplicate identifiers, missing checks and dependency cycles should stop the run. If a changed path has no reliable mapping, I'd run a broader suite rather than accept an empty selection.
The prerequisites determine setup. A gateway controller that requires Gateway API definitions gets those definitions before installation. The routing check also needs the service template and a ready test application. I can test that group without installing every platform component.
Kind could supply the disposable local cluster, with Chainsaw running resource assertions. I would begin with one command-line tool for validation, selection and reporting. The specification and runner are proposed here; the examples are not a working tool to install.
What I would expect in a review#
The runner needs to render the candidate revision with the same inputs and rendering method used for delivery. Its report should identify that revision, the contract versions, fixture and toolchain, then show the result of every selected check.
For the selector defect, I would want the failed request alongside the generated manifests, route conditions and Service endpoints. That lets a reviewer follow the failure back to the template change. Applying the resources directly tests routing; a delivery capability check would separately exercise Argo CD's reconciliation from Git.
An unavailable fixture image needs a different result. The worker couldn't establish the test prerequisites, so routing remains untested. I would report it as blocked. A selected check with no result is not run, and a required check in either state prevents a passing report.
I would start with one capability and the checks needed to explain a real failure within it. If the routing test passes locally but the sandbox cannot validate the hostname's certificate, I can approve the local routing change only within that scope. The public HTTPS promise still needs evidence.