Documentation for v2.6.1. Read the latest, v2.12.0.
Developer
sanshain.yaml
Every official client reads the same sanshain.yaml from the project root and turns it
into provide,
require and
catalog calls. This chapter is the file's
reference. What each client adds on top (flags, properties, its own caching) is in
Clients.
What changed in 2.6.0
The catalog push now also records the system's owner, description, source location and links (the catalog context); the fields Sanshain reads are listed under the catalog phase. The file format is unchanged.
What the file declares#
- Connection: the server URL (better set in the environment) and whether to ask for gzip.
- Provides: the spec files this project publishes. The version is never configured here; it lives in the spec file. The stability is not configured here either; the build's ga switch decides it.
- Requires: the endpoints this project depends on, each pinned to an exact version.
-
Catalog info (optional): the project's Backstage
catalog-info.yaml, whosedependsOnnames its peripheral resources and whose owner, description and links introduce the system on its node and page.
A complete example#
A gateway that provides an OpenAPI spec and a proto file, and consumes REST, messaging and gRPC from three other systems:
serviceName: gateway-service
compression: true
catalogInfo: catalog-info.yaml # optional
# sanshainUrl is omitted: SANSHAIN_URL in the environment
provides:
- file: src/main/resources/openapi.yaml # version from info.version
- file: src/main/resources/gateway.proto # version from // sanshain-version:
apiType: proto
requires:
- serviceName: user-service
apiType: openapi
version: 2.3.0
outputDirectory: target/generated-sources/sanshain/user-service
endpoints:
- method: GET
path: /api/v1/users/{id}
- serviceName: notification-service
apiType: asyncapi
version: 1.1.0
outputDirectory: target/generated-sources/sanshain/notification-service
endpoints:
- method: PUB
path: notifications.email
- serviceName: inventory-service
apiType: proto
version: 3.0.1
outputDirectory: target/generated-sources/sanshain/inventory-service
endpoints:
- method: GetProduct
path: inventory.v1.InventoryService
Top-level fields#
| Field | Required | Default | Meaning |
|---|---|---|---|
serviceName |
yes | — |
The name of this system: the
producername of its provides, the consumername of its requires,
the systemname of its catalog push.
|
sanshainUrl |
no | — |
Base URL of the instance. Prefer SANSHAIN_URL in the environment so the URL is
not committed; the environment overrides the file.
|
compression |
no | false |
Ask for gzip-compressed answers. Honoured by the Maven (default true there), Go
and JavaScript clients; the Rust and Conan clients accept the key and ignore it.
|
bestEffort |
no | false |
Turn failed calls into warnings. Read by the Rust, Go, Maven and Conan clients; the
JavaScript client takes SANSHAIN_BEST_EFFORT instead.
|
catalogInfo |
no | — | Path to the Backstage catalog-info.yaml the build pushes. |
strict |
no | false |
Strict mode: missing configuration and any
Sanshain-Warning fail the build instead of warning.
|
Every relative path in the file (file, outputDirectory,
catalogInfo) resolves against the directory that contains
sanshain.yaml (named with --config on the Go, JavaScript and Conan CLIs,
config_path in the Conan helper, configFile in Maven), not the working
directory, so a build that points its client at another config file finds the same files.
The file carries no secrets. The token comes from SANSHAIN_TOKEN or the build tool's
credential store, and is sent as Authorization: Bearer san_….
How stability is decided#
Sanshain never sees your git repository and the clients do no git detection. The rule is binary:
- Every provide is a snapshot by default. A developer building locally cannot release by accident.
-
GA is an explicit act: the build sets the ga switch,
SANSHAIN_GA=truein every client,-Dsanshain.ga=truein Maven,--gaon a CLI. CI sets it on its protected-branch pipelines and nobody else touches it. Publishing GA needs the releaser role.
The provides section#
A list under provides, or a single provide object. Omit it entirely when
the project only consumes.
| Field | Required | Default | Meaning |
|---|---|---|---|
file |
yes | — | The specification file. |
apiType |
no | openapi |
openapi, asyncapi or proto. |
retired |
no | false |
The project no longer provides this family; see below. |
The version is read from the file: MAJOR[.MINOR[.PATCH]], optionally
v-prefixed; omitted parts are zero and the stored form is always three-part. For
OpenAPI and AsyncAPI it is info.version. A proto file must carry exactly one
// sanshain-version: MAJOR.MINOR.PATCH comment; a missing, malformed or conflicting
marker rejects the provide with 400. The marker travels into the split files
Consumers download.
Retiring a protocol#
Sanshain cannot tell a dropped protocol from a pipeline that stopped running, so removing a
provides entry changes nothing on the server. To
retire a family, keep the entry and set
retired: true. The client sends a provide for that family with
retired: true and no body. Sanshain clears the family's capability tag (messaging
or grpc), closes its open trunk pins and releases its AsyncAPI
message contracts, and answers what it shed:
{ "tag_cleared": "messaging", "trunk_pins_closed": 3, "contracts_released": 2 }
Version history stays and Consumers pinned to old versions keep resolving. A retire carries no
body, no stability, no trunk and no tag; sending any of
them alongside retired: true is refused with 400. Retiring needs the
releaser role or a maintainer grant on the
Producer; under dry_run the permission check runs and nothing is retired.
The requires section#
One entry per Producer and version. An entry with several endpoints becomes one
POST /require-bundle; an entry with one endpoint becomes GET /require (or
/require/asyncapi, /require/grpc).
| Field | Required | Default | Meaning |
|---|---|---|---|
serviceName |
yes | — | The Producer to require from. |
version |
yes | — | The exact pin, MAJOR.MINOR.PATCH. No ranges, no latest. |
outputDirectory |
yes | — | Where the answered document is written. |
endpoints |
yes | — | A list of method and path pairs; see the matching table. |
apiType |
no | openapi |
openapi, asyncapi or proto. |
format |
no | — |
Only openapi, only with apiType: asyncapi: receive the payload
schemas as an OpenAPI document (below).
|
The pin is the whole contract: what the build downloads changes only when someone edits
version. A pin that does not exist fails with 404 (unknown); a pin that exists but lacks an endpoint fails with 410 (absent), and a bundle names the missing endpoints.
Payload schemas as OpenAPI (format: openapi)#
A Consumer whose code generator only speaks OpenAPI can ask for the message payloads of an
AsyncAPI requirement as an OpenAPI 3.0.3 document. The client sends format=openapi;
the answer has info.title <producername> message schemas,
info.version the pin, empty paths, and in
components.schemas only the payload schemas of the required channels' messages with
everything they reference. A payload given as $ref: '#/components/schemas/X' keeps
the name X; an inline payload is named <MessageKey>Payload. Every
$ref resolves inside the file, so openapi-generator with
generateApis=false produces the DTOs. AsyncAPI 2.x and 3.x both work.
requires:
- serviceName: order-service
apiType: asyncapi
format: openapi
version: 2.0.0
outputDirectory: generated/order-service-dtos
endpoints:
- method: PUB
path: orders.created
To get both formats, list the entry twice with different outputDirectorys. The
service answers 400 when two different schemas would get the same name, a payload
uses a non-AsyncAPI schemaFormat (Avro, say), a $ref is external, or
format is set on another apiType. The ETag of the OpenAPI answer never
matches the AsyncAPI answer's.
How path and method match#
On a provide Sanshain splits the document and stores each endpoint under a
path (or channel, or service) and a method (or operation, or rpc). A require
looks those up at the pinned version. When the version exists but the values do not line up, the
require fails 410.
| Protocol | Sanshain stores | path |
method |
|---|---|---|---|
| OpenAPI | each paths.* entry and HTTP verb |
the OpenAPI path, e.g. /api/v1/users/{id} |
the verb, case-insensitive (GET, POST, …) |
| AsyncAPI 2.x | each channels.* key with publish / subscribe |
the channel key, e.g. orders.created |
PUB or SUB |
| AsyncAPI 3.x |
each operation, resolved to the channel address, send /
receive
|
the channel address, not the key | PUB or SUB |
| Proto | each service and rpc |
the fully qualified service, e.g. inventory.v1.InventoryService |
the rpc name, case-sensitive (GetProduct) |
OpenAPI path matching is lenient: parameter names are normalised, so
/users/{id} and /users/{userId} match; doubled separators and a trailing
slash are ignored too. Two paths in one document that are the same endpoint after normalisation
reject the provide with 400.
An OpenAPI example#
A user-service document with GET /api/v1/users,
POST /api/v1/users and GET /api/v1/users/{id} is stored as three
endpoints. To require two of them:
requires:
- serviceName: user-service
version: 1.0.0
outputDirectory: generated/user-service
endpoints:
- method: GET
path: /api/v1/users/{id}
- method: POST
path: /api/v1/users
The client calls POST /require-bundle and receives one merged OpenAPI document with
the two operations and each shared schema once, which is why bundles exist: separate snippets
would give typed languages two copies of CreateUserRequest that are not
interchangeable.
AsyncAPI: the perspective convention#
Sanshain reads AsyncAPI 2.x publish and subscribe from the
application's perspective: publish means
this service publishes to the channel, subscribe means
this service consumes it. The official 2.x specification defines the keywords from the
client's perspective, exactly inverted. Sanshain uses the application reading because it matches
the unambiguous 3.x send / receive mapping. A 2.x document authored with
the spec-literal reading registers its
message contracts and harvests its
subscriptions exactly backwards.
So a Consumer requires a channel the Producer publishes with method: PUB. A
Consumer's own subscribe operations need no require at all: they are
harvested from its provide (see the
API lifecycle).
Moving from 2.x to 3.x changes the identifier: 2.x stores the channel key, 3.x the
channel address, the key being only a local label. Write the address in
path; when a 3.x channel has no address, Sanshain falls back to the key.
# AsyncAPI 3.x:
# channels:
# OrderCreated: # a local key
# address: orders.created # what Sanshain stores
requires:
- serviceName: order-service
apiType: asyncapi
version: 2.0.0
outputDirectory: generated/order-service
endpoints:
- method: PUB
path: orders.created # the address
Proto#
The answer to a proto require is a .proto file with the service, only the requested
rpcs, and every message they reference, transitively, with the
// sanshain-version: marker in it.
requires:
- serviceName: inventory-service
apiType: proto
version: 1.0.0
outputDirectory: generated/inventory-service
endpoints:
- method: GetProduct
path: inventory.v1.InventoryService
- method: ListProducts
path: inventory.v1.InventoryService
What the clients send#
The provide phase#
For each entry: read the file, decide the stability from the ga switch, and call
POST /provide, /provide/asyncapi or /provide/grpc with the
document, the stability and, when the build declares a
stream, trunk or tag. Every
provide answers 202 Accepted:
{
"version": "1.4.0",
"stability": "ga",
"content_hash": "sha256:…",
"changes": { "inserts": 1, "updates": 2, "deletes": 0 },
"promoted": false
}
changes is the endpoint diff against what that exact version stored before; a new
version reports its whole endpoint set as inserts, and byte-identical content reports all zero,
the idempotent no-op. An AsyncAPI provide
adds harvested_subscriptions. A 409 carries
proposed_version: set it in the spec file and republish, there is nothing to pull or
merge. A 400 means the document itself is invalid, most often a missing or non-semver
version.
The catalog phase#
A step of its own (sanshain catalog, sanshain:catalog,
Sanshain(self).catalog()), because a pure Consumer never provides and a pure Producer
may never require. Once per build the client reads the file at catalogInfo and calls
PUT /catalog/<serviceName> with
{"catalog_info_yaml": "…", "trunk": true|false}; trunk follows the same
switch as provides and requires. Without catalogInfo the client warns and skips, or
fails under strict. Sanshain reads only the Component whose
metadata.name equals serviceName (case-insensitive), and of it:
| Field | What becomes of it |
|---|---|
spec.dependsOn |
Only resource: refs become
peripheral resources. A ref without
a kind is read as a resource; one without a namespace takes the Component's namespace (default
when it has none); refs are lower-cased. component: refs and
consumesApis are ignored: edges between systems are what requires record.
|
spec.owner |
Shown as the owner with the ref's kind and name: a bare team-orders is
group:default/team-orders, shown as team-orders (group).
|
metadata.description |
Shown under the title; trimmed, at most 1000 characters. |
metadata.annotations["backstage.io/source-location"] |
The repository chip, without the url: prefix; kept only as an
http(s) URL of at most 2048 characters, labelled by its host.
|
metadata.links |
Link chips by title (the URL's host when there is none); type and
icon kept as text. A link that is not http(s) is dropped; at most
20 links.
|
Everything else is ignored. Each push replaces the previous state: a resource no longer listed
leaves the current graph (it stays in the timeline), a file without owner or links leaves those
empty, and a push whose content has not changed changes nothing. A trunk push updates the main
graph and the dev graph; a plain push only the dev graph. The answer is
{"system", "trunk", "resources": [...], "recorded": true}. When Sanshain cannot read
the file as this system's Component (invalid YAML, no or more than one matching Component, usually
a metadata.name that is not the serviceName), it still answers
200 with "recorded": false, records nothing, keeps the previous state
and names the reason in a Sanshain-Warning header. Older services answer
404; the catalog push needs Sanshain Service 2.4.0 or later.
The require phase#
For each entry: one bundle or one single require, then the answer is written to
outputDirectory and code generation runs on it. Every require answer carries an
ETag; the clients store it and send
If-None-Match next time, so an unchanged pin answers 304 and no file is
rewritten. A GA pin never changes; a snapshot pin can, and the ETag is what detects that.
Checked against v2.6.0.