Skip to content
Français
Cloud medium

Understanding feint: why an emulator rather than a mock

2 min de lecture

Read this page in French

AWS has LocalStack, Azure has Azurite, European clouds had nothing. Their users test against a paying account or not at all. feint emulates the Scaleway, Outscale and Exoscale APIs so that SDKs, CLIs and Terraform run on a workstation. This page explains the decisions that keep an official client from noticing, and why those same decisions produce the limits documented elsewhere.

What you will understand

By the end of this page, you will know why an emulator finds defects a mock cannot find, how this project copes with APIs that move faster than anyone can follow by hand, and where the limits documented elsewhere come from.

  • Tell apart an emulator and a mock server, on two real defects.
  • Understand why the surface is measured rather than copied.
  • Place the boundary between the neutral core and the provider packs.
  • Connect each documented limit to the decision that produces it.

Where the name comes from

The name is a fencing move: the feint, from Old French feindre, a movement made to look like the real one so that the opponent commits. That is exactly the test playing out here: the official client commits, and cannot tell the difference. The project's mark draws the same argument, with a solid line for the surface actually served and a dashed one for what the provider announced and nobody has driven yet.

Why not a mock server

A mock returns what you told it to return. It therefore cannot tell you that the field you invented does not exist, that the CLI reads three other endpoints before creating anything, or that the API renamed an operation last month.

Two defects found in this project make the difference concrete, and they are more convincing than a demonstration.

The first came from an invented field. The machine-creation operation on Outscale read a VmCount field the API does not have: it exposes MinVmsCount and MaxVmsCount. Every request therefore created one machine whatever it asked for. A mock configured with the same wrong field would have agreed with the defect.

The second is subtler. The Scaleway pack once served an invented private address that its own test suite then read back. The emulator was proving itself against itself, and nothing in that loop could report the error. The countermeasure is not one more test, it is an outside arbiter: responses are checked against the provider's own API description, and 648 of Outscale's 655 schemas declare additionalProperties: false, which makes an undefined field a violation the provider wrote down themselves.

Why the surface is measured, not copied

An API's surface moves faster than any team can follow by hand. The figure is measured rather than estimated: over the twelve months to 28 July 2026, Scaleway added 363 operations and removed 25. The reading comes from running the project's surface scan on two dated checkouts of the official SDK, then diffing the operation lists.

Writing that emulation by hand, as the competing approach does for AWS with its 200 000 lines, therefore means being wrong within a quarter without knowing which one. Three mechanisms replace that work, and they outrank any other design consideration.

The surface scan reads the provider's official Go SDK, which is generated from their own interface description language and therefore exact. No network call, no assumption.

The baseline is versioned. An operation that appears upstream and that nobody triaged fails continuous integration. The resulting status is not "not yet supported" but "undecided", which is a different and far more useful thing to know.

The conformance suites replay the real clients, namely scw, octl, exo, Terraform and OpenTofu, on every proposed change. A weekly job runs the scan and opens a pull request the moment the upstream surface moves; the human work reduces to triage.

One port, three clouds

The three providers do not collide in URL space, and that is what makes the multi-cloud case practical. Scaleway serves /<product>/v<N>/…, Outscale answers POST /api/v1/<Action>, Exoscale uses /v2/<resource>.

A single HTTP multiplexer therefore hosts all three packs, and the server refuses to start if two packs claim the same route pattern. A collision is a defect that must surface at boot, never as a request served by the wrong pack.

For you, the consequence fits in one sentence: one process, one endpoint, one thing to start in a pipeline.

The core that knows no provider

The architecture rests on two directories. The core knows how an emulated cloud behaves; the packs know how this cloud says it.

The rule is absolute: the core must never know a provider exists, not by name, not by prefix, not as a special case. It earns its keep because violations are silent. The machine-event watcher once filtered on three provider prefixes written into the core. Nothing failed. A fourth pack would simply have had to edit the core for its own events to be reported.

The test to apply to any line in a pack fits in one question: could this line be written identically for another provider? If yes, it belongs to the core. What stays in the pack is what only that provider knows: its image catalogue, the login its machines use, the field its API publishes an address under, or the fact that it publishes none.

What varies is a field, not a convention

This is the finest distinction in the architecture, and confusing it is expensive in both directions.

The shapes must differ. Scaleway answers a server object, Outscale a Vms array with a response context, Exoscale an asynchronous operation. Flattening that into one shape would destroy the point of the project: each client must find its own cloud.

The behaviour must not differ. Powering on a machine follows the same sequence everywhere: render the boot payload if the client supplied none, name and label the machine, keep its name out of the API's reach, publish the address, withdraw it on stop, and degrade without breaking the control plane when no runtime is configured. Written twice, a defect fixed on one side survives on the other, and it did.

The variation therefore takes the form of a field rather than a duplication: a prefix, a login, an address key. Three packs, one sequence, no copy.

A refusal must be legible on the wire

When a pack does not serve an operation, a client meets the refusal, and it must be able to tell a non-served operation from a missing resource. Without that distinction, a refusal reads as an empty answer, and a green run then proves something false.

The problem is that no status and no body suits all three providers, and that is a measurement rather than a preference.

PackStatusWhy that one
Scaleway501their SDK does not map that type, so it cannot confuse it with a missing resource
Outscale404their envelope has a field for the marker, and a 501 costs their CLI 12 seconds of retries
Exoscale404their envelope has nowhere for a marker, and a 501 breaks instance creation

Exoscale is the case that settles it: their CLI calls a reverse-DNS operation after every create and treats anything but a 404 as fatal. The status a program could branch on is therefore exactly the one that breaks the client. The solution went out of band, with a header no real cloud sends:

X-Feint-Not-Emulated: exoscale

It is set once, in the shared layer, with the pack's name read from the process's mount table and never from the path the client sent. It never marks an ordinary 404, or the marker would mean nothing.

No dependencies, and that is a security argument

The go.mod file is three lines long, and a pre-commit check keeps it that way. Routing, JSON and Go source parsing all come from the standard library.

This is not minimalism for its own sake. An emulator that runs in everybody's continuous integration is a supply chain surface, and the cheapest way to secure a dependency is not to have it. Any new dependency has to be justified in the pull request that adds it. The same logic governs the verification of published binaries, detailed in Installing feint.

Key takeaways

  • A mock agrees with your mistakes; an emulator checked against the API description does not.
  • The surface moves too fast to follow by hand: 363 operations added on Scaleway in twelve months.
  • An untriaged operation fails CI, which distinguishes "undecided" from "unsupported".
  • A single port serves three clouds, and a route collision prevents start-up.
  • The core ignores providers: their differences enter it as field values, never as names.
  • Shapes differ, behaviour does not: that is what stops a defect fixed on one side from surviving on the other.
  • Zero dependencies is a security decision, not an affectation.

Next steps

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn