github stephrobert/feint v0.14.0

3 hours ago

Added

  • A block snapshot imported from Object Storage, through both doors
    (#807): block/v1/API.ImportSnapshotFromObjectStorage and
    block/v1alpha1/API.ImportSnapshotFromObjectStorage.

    The client sends {bucket, key} and the real cloud reads that object
    server-side, so nothing in the Terraform provider's path speaks S3 on this
    call. That is what separates it from its export half, which stays declined:
    there the provider builds the S3 endpoint from the region in code, so pointing
    it at this emulator would take DNS interception and a certificate it accepts.

    What is recorded is the intention. A snapshot appears, available, with no
    parent volume and a size of zero, because no bytes moved and none could have.
    The divergence is stated in docs/limits.md rather than hidden: the real API
    refuses an object that does not exist, and there is no Object Storage here to
    check a bucket and a key against.

  • SCIM, the whole chain, and a spot start — the nightly drift of 2026-09-29
    (#800), triaged by serving rather than declining.

    The scan found iam/v1alpha1/API.GetScimToken new upstream and untriaged while
    its six neighbours were declined. Serving that one alone was the option that
    looked cheapest and was the worst: the route would have answered 404 to every
    identifier, and counted as implemented in coverage/scaleway-coverage.json,
    because creating the token it reads needs a SCIM configuration and enabling one
    was refused. A coverage number that counts an unreachable route is the one
    thing this project cannot afford.

    So the chain is served end to end — iam/v1alpha1/API.EnableOrganizationScim,
    iam/v1alpha1/API.GetOrganizationScim, iam/v1alpha1/API.DeleteScim,
    iam/v1alpha1/API.CreateScimToken, iam/v1alpha1/API.ListScimTokens,
    iam/v1alpha1/API.GetScimToken, iam/v1alpha1/API.DeleteScimToken — one
    configuration per account, tokens under
    it, and the configuration's deletion takes its tokens with it because upstream
    has no operation that ever would. The bearer token is answered once and kept
    nowhere, which is both how the product behaves and the only shape that leaves
    no secret for PUT /_feint/state to hand back.

    What is not claimed: the /scim/v2/Users and /scim/v2/Groups endpoints a
    directory actually calls. This is the control plane a client configures;
    provisioning users from a directory is a second product and it is not emulated.

    The CLI drives it. scw iam scim and scw iam scim-tokens exist, so the
    conformance suite walks the family with the real client rather than declaring
    that nobody calls it; only the three operations the CLI has no subcommand for
    — enabling SCIM, reading the configuration, reading a single token — carry a
    Route.Undriven reason, and the suite reaches the first of them with curl
    because the three the CLI does drive all take the identifier it returns.

Changed

  • instance/v2alpha1/API.StartSpotServer is declined, and the reason is the
    contract
    (#800). It arrived in the same drift and the first answer was to
    serve it: its response carries no spot field at all, so the body could not have
    promised a client anything about billing or interruption, which are the two
    halves of "spot" an emulator cannot honour.

    What refuses it is measured. The operation is in the Go SDK and not in the
    document Scaleway publishes
    — contracts/scaleway.json knows spot_info on
    a ServerType and no such operation — and there is no exemption in that
    direction, deliberately: contract.CheckRoutes answers "the API defines no
    such operation", and a route whose response nothing can validate breaks one of
    the three mechanisms this project rests on. coverage/contract-only.json
    covers the opposite case, a document the SDK never wrapped.

    So it waits for the document, and the nightly scan brings it back the day that
    moves. No official client reaches it either: scw instance server has no spot
    subcommand.

  • The API gateway's own route: scw/Client.GetAPIMetadata (#776).
    GET /metadata answers {"platform", "partition", "domain"}, which the SDK
    reads to build a srn://<service>.<domain>/… client-side for every product
    that gained one.

    Measured 2026-09-14 through feint proxy --record: one apply plus destroy
    of the conformance fixture sent 148 of these and this emulator answered 404
    to every one
    — 148 of that run's 169 refusals. Nothing failed, because the
    SDK discards the error and the SRN stays empty. That is luck rather than a
    decision, and the shape of #257 exactly.

    The instrument had to move before the handler could. Route.Operation
    must name an operation the drift scan finds, and the scan walked
    api/<product>/<version> only, where Client.GetAPIMetadata is not — so the
    route would have been an orphan, and all three baselines carry zero. The scan
    now reads the gateway package on a criterion of its own: a method that
    builds a request rather than one that carries someone else's. Reusing the
    product walk's matcher was measured wrong in both directions — it found
    Client.Do, the transport every call goes through, and Config.String, a
    formatter, and missed the one operation that matters.

    The contract describes it too, through --gateway, the same escape the
    refusal shape already uses: Scaleway publishes one document per product and
    the gateway is not one. So the response is validated rather than exempted.

    The three values are the real cloud's, read on a read-only shot at an fr-par
    account and committed as corpus/scaleway/scw-gateway.jsonl. A committed
    corpus is sanitised, so it grades the status and the field tree and not the
    values; those are held by the contract fragment and by a test that writes them
    out rather than reading them from the handler.

  • A server gives up a private interface: instance/v2alpha1/API.DetachServerPrivateNetworkInterface.
    POST /instance/v2alpha1/zones/{zone}/servers/{id}/detach-private-network-interface
    dissociates an interface from its server and answers the server. Terraform
    provider 2.83.0 sends it before every delete of an interface
    (fix(instance): detach private network interface before deleting it,
    upstream #4354), and a 501 there failed every terraform destroy.

    The refusal it leaves said "no client this project drives reaches for these
    operations"
    — a sentence that already carried one expiry date, from 2.81.0,
    and has now collected a second. Only the detach is withdrawn: a recording of a
    full apply plus destroy shows it twice and the symmetric
    AttachServerPrivateNetworkInterface never, so that one keeps the reason.

    It dissociates rather than deletes, and that is measured. The client's next
    two calls are a read that must answer 200 and a DELETE it sends itself;
    deleting at the detach answers the read with a 404, the client skips its
    delete, and the suite passes for a reason that is not the stated one. Both
    behaviours are green — only one matches what the client does.

    Serving it needed the server rendered in v2alpha1's own shape, which this pack
    did not have. Three vocabularies differ from instance/v1 and the contract
    check caught each: a server is started where v1 says running, an interface
    is available where a server is started, and a volume v1 spells
    sbs_volume is sbs. b_ssd and unified have no v2alpha1 spelling and
    answer unknown_volume_type rather than a guessed equivalence.

  • A load balancer can be migrated: lb/v1/ZonedAPI.MigrateLB (#762).
    POST /lb/v1/zones/{zone}/lbs/{id}/migrate accepts a new offer and the read
    that follows shows it. It is the one Day-2 action this API has, and no client
    could exercise it: the real account bills the only migration it accepts, so an
    example could not take that decision on somebody's behalf.

    The route was declined on the ground that answering "would confirm a resize
    nothing performed"
    . That argument did not survive its own neighbours:
    UpdateServer already accepts a new commercial_type and stores it, and
    CreateLB already accepts any type string. The reason is withdrawn rather
    than reworded, and the same withdrawal was applied to vpcgw/v2 where the
    gateway upgrade cited it.

    One divergence, in docs/limits.md: the real API refuses a migration to the
    type the balancer already carries, with 400 invalid_arguments. This does
    not, for the reason CreateLB already carries — corpus:check replays
    recordings whose values are synthetic, so a refusal keyed on a value turns a
    recorded 200 into a 400 on replay.

  • The volume catalogue is served: instance/v1/API.ListVolumesTypes (#758).
    GET /instance/v1/zones/{zone}/products/volumes answers the two types this
    emulator mints, l_ssd and scratch, with the display names, snapshot
    capability and size constraints a real fr-par-1 returned. A client that
    reads the menu before it creates no longer meets a 501 there.

    The route was declined until now, and the decline named its own expiry: "no
    recording of /products/volumes exists in corpus/ [...] the day that recording
    lands, this becomes a serve"
    . #758 landed it, as a feint proxy transcript
    of a real account taken on 2026-09-10. Every figure in the answer is a line of
    that transcript, held by a test.

    The catalogue and what POST /volumes accepts are now derived from one table,
    so the menu cannot offer a type the create refuses.

Proved against

The clients

Client Version Pinned in Drives, in CI
scw 2.56.3 .github/workflows/conformance.yml (SCW_VERSION) Scaleway
Terraform 1.13.3 .github/workflows/conformance.yml (TERRAFORM_VERSION) Exoscale, Outscale, Scaleway
OpenTofu 1.12.5 .github/workflows/conformance.yml (TOFU_VERSION) Exoscale, Outscale, Scaleway
octl 0.0.32 .github/workflows/conformance.yml (OCTL_VERSION) Outscale
exo 1.95.6 .github/workflows/conformance.yml (EXO_VERSION) Exoscale

The Terraform providers

The client above is the engine; what answers this emulator is the provider.
Each row is one required_providers entry, read where it is written.

Fixture or stack Provider Constraint What that is worth Applied in CI
tools/conformance/faults outscale/outscale ~> 1.7 constraint: resolved fresh on each run, so the version that answered is not knowable here yes
tools/conformance/outscale/terraform outscale/outscale ~> 1.7 constraint: resolved fresh on each run, so the version that answered is not knowable here yes
tools/conformance/outscale/terraform-doorway outscale/outscale ~> 1.7 constraint: resolved fresh on each run, so the version that answered is not knowable here yes
tools/conformance/scaleway/terraform scaleway/scaleway 2.83.0 exact: the version that answered yes
examples/stacks/exoscale exoscale/exoscale 0.72.0 exact: the version that answered yes
examples/stacks/outscale outscale/outscale ~> 1.7 constraint: resolved fresh on each run, so the version that answered is not knowable here yes
examples/stacks/outscale/modules/net outscale/outscale ~> 1.7 constraint: resolved fresh on each run, so the version that answered is not knowable here yes
examples/stacks/scaleway scaleway/scaleway 2.83.0 exact: the version that answered yes
examples/quickstart/outscale outscale/outscale ~> 1.7 constraint: resolved fresh on each run, so the version that answered is not knowable here yes
examples/quickstart/scaleway scaleway/scaleway 2.83.0 exact: the version that answered yes

Read the third column narrowly, because it is the one a consumer pins against.
An exact constraint names the version that answered. A constraint is
resolved by terraform init -upgrade on the runner, every run, so the version
that answered is not knowable from this repository — it is a floor, not a
proof. Not pinned is not pinned: nothing here says which version ran, and
no number is invented to fill the cell.

Every stack under examples/stacks/ is applied on every pull request.

Generated from the conformance workflow and the fixtures at this tag.
A newer client is not known to work until the pin moves and the suite
passes on it: docs/clients.md says what each cell is worth.

Don't miss a new feint release

NewReleases is sending notifications on new releases.