README
¶
openmock-go
Go reference implementation of OpenMock.
Status: engine + all four protocol servers implemented. The engine
package is a working parse → match → render core that passes every case in
openmock-dev/openmock's
conformance corpus, across all four v0.2 protocols (HTTP, gRPC, GraphQL,
WebSocket). The server package serves a document over HTTP, gRPC, GraphQL,
and WebSocket: the protocol adapters plus the standalone-server profile of
docs/serving.md (port resolution and the admin API). openmock serve
runs it from the command line. gRPC serving uses a server's descriptorSet
(spec §3.4) to translate protobuf wire bytes; GraphQL and WebSocket are served
over HTTP. A gRPC server with no descriptorSet (whose messages can't be
decoded) is reported as unbound in the admin topology.
OpenMock is first a specification; this repository is its Go reference tooling. See openmock-dev/openmock for:
- the specification
- the conformance corpus
this engine is validated against, vendored under
engine/testdata/conformanceand run bygo test ./engine/... IMPLEMENTATIONS.md, whose reference-tooling roadmap names this repository's engine as milestone 1 ("Go engine (canonical)"), ahead of the HTTP adapter, the CLI, and a future TypeScript portdocs/serving.md, which defines the engine / embedding-host / standalone-server roles that this repository's package split follows
Layout
One Go module, split into three packages along the lines docs/serving.md
draws between the resolution engine and the roles that serve it:
engine/ pure parse -> match -> render core; no I/O, no server
assumption. Importable standalone by an embedding host.
Implemented: HTTP, gRPC, GraphQL, and WebSocket, conformant.
server/ protocol adapters + serving (HTTP, gRPC, GraphQL, and
WebSocket) over the engine, plus the admin/discovery API from
docs/serving.md, for the standalone-server role.
cmd/openmock/ the CLI: `serve` implemented; validate, test to follow.
Serving
$ openmock serve mocks.yml
serving "main" on http://127.0.0.1:3000
admin API on http://127.0.0.1:4400
- Ports resolve per
docs/serving.md§2:--port name=n(repeatable) orOPENMOCK_PORTS="a=3000,b=3001", then the document'sporthint, then the stable default for a document's sole server of a type (http3000,graphql3001,websocket3002,grpc3003), else an OS-ephemeral port. A configured/hinted port that can't bind fails loudly. - Admin API on
4400(--admin-port,0for ephemeral), loopback by default:GET /servers(the resolved topology),GET /health,POST /reset(clear call counters). --bindsets the interface (default127.0.0.1;0.0.0.0to expose).- gRPC servers need a
descriptorSet(spec §3.4): the compiledFileDescriptorSetthe transport uses to decode/encode protobuf, which is not self-describing. The field is optional in the document (the document stays valid, and the engine/embedding hosts never need it), but it is effectively required for standalone gRPC serving. A gRPC server without one is not bound; it's listed inGET /serverswith anotesaying why, and the startup log shows how to fix it. Compile one withprotoc --include_imports --descriptor_set_out=schema.binpb your.proto(orbuf build -o schema.binpb), then point a client at the mock using that same file as a protoset (seeexamples/). - GraphQL servers are served over HTTP (GraphQL-over-HTTP): POST (or GET) a
{query, operationName, variables}request to the port; the operation type and name are read from the query document. No schema is needed to serve, but if a server declares aschema(spec §3.5), it powers introspection (__schema/__type), so GraphiQL/Apollo/Postman can explore the mock. A missing or invalid schema is a warning, not an error. - WebSocket servers are served over an HTTP upgrade. The connection is routed once by its path at establishment (an unroutable path is refused with a 404, never accepted); then every client message is resolved, with a per-connection call counter, into zero or more reply messages and an optional close.
Try it
$ make build
$ ./bin/openmock serve examples/01-minimal.yml &
$ curl -s localhost:3000/health # {"status":"ok"}
$ curl -s localhost:4400/servers # the resolved topology
Or run the scripted, self-contained walkthrough (starts the server, curls a polling lifecycle and the admin API, shuts down):
$ make demo
The examples/ directory has seven runnable documents:
path params, templating and faker, delays and errors, the calls polling
lifecycle, exists/pattern matchers with multi-line Set-Cookie, and a
two-server topology, each with copy-paste curl commands in
examples/README.md.
Development
$ make # list targets
$ make build # ./bin/openmock
$ make check # gofmt check + go vet + go test -race (what CI runs)
$ make unit # engine + cmd tests, skipping the conformance corpus
$ make conformance # the vendored openmock-dev/openmock conformance corpus
$ make e2e # server package + CLI walkthrough, end to end
$ make run EXAMPLE=examples/05-polling.yml # serve an example in the foreground
CI and releases
.github/workflows/ci.yml runs on every push
and pull request:
- lint:
gofmt,go vet,govulncheck - unit:
engineandcmdtests, minus the conformance corpus - conformance: the vendored corpus from openmock-dev/openmock
- e2e:
serverpackage tests (real HTTP/gRPC/GraphQL/WebSocket listeners) plus the scripted CLI walkthrough (examples/demo.sh), on Linux, macOS, and Windows - build: cross-compiles
cmd/openmockfor every release target (linux/darwin/windows × amd64/arm64) so a platform-specific build break surfaces before a tag is ever cut
Tags are cut automatically. Every push to main
(.github/workflows/auto-tag.yml) bumps
a semver tag: a feat: commit bumps minor, a BREAKING CHANGE/! commit
bumps major, anything else bumps patch, so a merge is a release unless its
commit message contains [skip release]. It then explicitly dispatches
(a tag pushed with the workflow's own token doesn't fire other workflows'
tag triggers, so relying on that chaining silently no-ops):
.github/workflows/release.yml: the test suite once more as a safety gate, then GoReleaser (config:.goreleaser.yaml), which buildsopenmockfor Linux, macOS, and Windows (amd64/arm64), publishes.tar.gz/.ziparchives and checksums to a GitHub Release, and generates release notes from the merged pull requests since the last tag. Try it locally without publishing:make release-snapshot(needsgoreleaserinstalled)..github/workflows/docker.yml: builds and pushes the container image (see Docker below) forlinux/amd64andlinux/arm64to GHCR, tagged with the full version, itsmajor.minorandmajor, andlatest. The same workflow also runs on every push tomain, publishing anedgetag.
Docker
Images are published to
ghcr.io/openmock-dev/openmock-go.
--bind 0.0.0.0 is required in a container: the server's default
127.0.0.1 only accepts connections from inside it.
Ports
The image only fixes one port: the admin API, 4400 by default
(EXPOSEd, and always bound unless --admin-port overrides it). The
protocol servers' ports are resolved per document (see
Serving above); a fixed EXPOSE list can't describe a moving
target, so pick one of these instead:
Pin the ports you know you want, and publish exactly those: the straightforward option once you know your document's servers:
$ docker run --rm \
-e OPENMOCK_PORTS="main=3000" \
-p 3000:3000 -p 4400:4400 \
-v "$(pwd)/examples/01-minimal.yml:/mocks/mocks.yml:ro" \
ghcr.io/openmock-dev/openmock-go:latest \
serve mocks.yml --bind 0.0.0.0
$ curl -s localhost:3000/health
--network host (Linux only) sidesteps the mapping problem
entirely: whatever port a server resolves to (including an
OS-ephemeral one) is reachable directly at localhost:
$ docker run --rm --network host \
-v "$(pwd)/examples/01-minimal.yml:/mocks/mocks.yml:ro" \
ghcr.io/openmock-dev/openmock-go:latest serve mocks.yml
Discover the resolved topology from the admin API: useful when you
don't want to precompute ports by hand. Publish 4400, start the
container, then ask it what it bound:
$ docker run --rm -p 4400:4400 \
-v "$(pwd)/examples/01-minimal.yml:/mocks/mocks.yml:ro" \
ghcr.io/openmock-dev/openmock-go:latest \
serve mocks.yml --bind 0.0.0.0 &
$ curl -s localhost:4400/servers # [{"name":"main","type":"http","port":3000,...}]
This tells you what to -p/OPENMOCK_PORTS on the next run; the
protocol port itself isn't reachable this way unless it was also
published.
docker run ... ghcr.io/openmock-dev/openmock-go:latest version prints the
build's version/commit/date. Image tags: latest and vX.Y.Z/X.Y/X
track releases; edge tracks the tip of main.
Dockerfile is a two-stage, fully distroless build, both
stages from Chainguard for a minimal,
continuously patched, low-CVE footprint end to end: the build stage
(cgr.dev/chainguard/go) compiles a static (CGO_ENABLED=0) binary, and the
runtime stage (cgr.dev/chainguard/static) is just that binary,
ca-certificates, and a nonroot user: no shell, no package manager, nothing
else. The resulting image is a few MB. There's no shell to docker exec
into; debug from docker logs or by running the binary directly. Build it
yourself with make docker-build / run with make docker-run.
Using the engine directly
doc, err := engine.Parse(yamlBytes) // spec §3; refuses invalid documents
if err != nil {
log.Fatal(err)
}
eng := engine.New(doc)
resp := eng.Resolve(&engine.Request{
Protocol: "http",
Method: "GET",
Path: "/users/42",
})
Resolve returns a map[string]any shaped like the protocol's normalized
response in openmock-dev/openmock's
conformance/README.md,
e.g. {"status": 200.0, "headers": {...}, "body": {...}} for HTTP. The
server package maps that onto real HTTP traffic; embedding hosts can drive
Resolve directly.
License
MIT, see LICENSE.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
openmock
command
Command openmock is the OpenMock reference CLI.
|
Command openmock is the OpenMock reference CLI. |
|
Package engine implements OpenMock's pure parse -> match -> render core.
|
Package engine implements OpenMock's pure parse -> match -> render core. |
|
Package server adapts the engine to network protocols and serves it.
|
Package server adapts the engine to network protocols and serves it. |