This page defines how code in cloudemu is named and where it goes. The goal is that a service has the same name in every layer, files are where you expect them, and new code has an obvious place. Read it before adding a service, a resource or a sub-surface.
For the reasons behind the three-layer design, see architecture.md. This page only covers naming and placement.
Every service spans up to three layers, in three parallel trees:
services/<capability>/ # portable API + driver interface (cross-cloud abstraction)
providers/<cloud>/<service>/ # in-memory mock for one cloud
server/<cloud>/<service>/ # SDK-compat wire handler for one cloud
<cloud>is one ofaws,azure,gcp.- Not every service has all three layers. A product-specific mock (e.g.
bedrock) may have no cross-cloudservices/abstraction, and some services have no wire handler yet. The naming rules below still apply to whichever layers exist.
Large opt-in add-ons live in their own Go modules under contrib/, so their
dependencies stay out of the core cloudemu module:
contrib/realengine/ # opt-in real engines, no Docker (embedded-postgres, miniredis, subprocess)
contrib/dockerengine/ # opt-in real engines, Docker required (mysql, compute, containers, azure functions)
contrib/server/ # the `cloudemu-server` binary (real engines wired onto the wire servers)
contrib/testcontainers/ # a Testcontainers Go module
A real-engine implementation goes in the matching contrib/<realengine|dockerengine>/<capability>/
subpackage and satisfies the config.<X>Engine interface from config/engine.go.
cloudemu is an SDK-compat emulator, so pointing a real SDK or CLI at it should feel natural. The provider and wire layers keep the name that cloud's own SDK uses, and the shared abstraction uses a generic capability name.
The shared abstraction is named for the capability, never a product:
storage, compute, networking, loadbalancer, iam, database,
relationaldb, dns, monitoring, logging, secrets, messagequeue,
eventbus, serverless, cache, containerregistry, notification.
Product-specific services with no cross-cloud abstraction keep their product name
everywhere (bedrock, vertexai, databricks, bigtable, ecs, sagemaker, …).
The mock and wire layers use the service's own short name, without a cloud prefix.
That is the real SDK/CLI name where a service clearly has one (s3, ec2, elbv2),
and a short capability name otherwise (cache, dns, monitor, sql). Azure's SDK
calls these armredis/armdns/armmonitor, but the short form reads better and has
no prefix:
| Capability | AWS | Azure | GCP |
|---|---|---|---|
| Storage | s3 |
blobstorage |
gcs |
| Compute | ec2 |
virtualmachines |
compute |
| Networking | vpc |
vnet |
vpc |
| IAM | iam |
iam |
iam |
| Load balancer | elbv2 |
loadbalancer |
loadbalancer |
| Database (NoSQL) | dynamodb |
cosmosdb |
firestore |
| DNS | route53 |
dns |
clouddns |
| Cache | elasticache |
cache |
memorystore |
| Monitoring | cloudwatch |
monitor |
monitoring |
Two hard rules:
- No redundant cloud prefix. The parent directory already names the cloud, so an
aws/azure/gcpprefix on the leaf adds nothing. Drop it:awsiam→iam,azurecache→cache,azuredns→dns,azureiam→iam,azurelb→loadbalancer,azuremonitor→monitor,azuresql→sql,azureai→ai,azuresearch→search,gcpiam→iam,gcplb→loadbalancer,gcpvpc→vpc. - Provider and wire names match within a cloud. For a given cloud, the
providers/<cloud>/Xandserver/<cloud>/Xdirectories have the same nameX, so the two layers of a service line up by name (e.g.providers/azure/blobstorage↔server/azure/blobstorage).
Names don't have to be identical across clouds at the provider/wire layer.
s3/blobstorage/gcseach read naturally for their own cloud, and that is the intent. The cross-cloud name lives only inservices/.
Exceptions to the "provider and wire share a name" rule:
- AWS
providers/aws/vpc↔server/aws/ec2. AWS's own SDK puts VPC operations under the EC2 service, so the wire handler lives inec2, while the mock keeps the clearervpcname. This is intentional. services/azureai/services/azuresearchkeepazurein their name, while their providers dropped it (providers/azure/ai,providers/azure/search). They are product-specific services (Azure AI, Azure AI Search) with no cross-cloud abstraction, so per §2.1 the product name, including theazurethat is part of it, is kept at theservices/layer.
snake_case, full words, no abbreviations, in every layer.natgw.go→nat_gateway.go,eip.go→elastic_ip.go,eni.go→network_interface.go,igw.go→internet_gateway.go,trafficmirror.go→traffic_mirror.go.- A feature uses the same filename in all three layers, so one
grep(or one filename) finds the interface, the mock and the wire handler for that feature.
| Feature | Filename (all layers) |
|---|---|
| NAT gateway | nat_gateway.go |
| Internet gateway | internet_gateway.go |
| Elastic IP / public IP | elastic_ip.go |
| Network interface | network_interface.go |
| Traffic mirroring | traffic_mirror.go |
Test files sit next to what they test: <feature>_test.go.
driver/driver.go # the driver interface(s), always in a driver/ subpackage
<capability>.go # portable API + do()/pipeline wiring
<capability>_test.go
<feature>.go # optional: one file per portable-API feature area
<service>.go # store + core CRUD
<service>_test.go
<feature>.go # one file per feature (same filename as the wire layer's)
clone.go # optional: copy-on-write helpers
tags.go # optional: tagging, if the service is tagged
handler.go # routing / dispatch (Matches + ServeHTTP)
types.go # wire DTOs + mapping to/from the driver
<feature>.go # one wire file per feature (same filename as the provider's)
Every portable-API service in services/ puts its interface in a driver/
subpackage. These are not portable-API services, so they have no driver/:
services/kubernetes: a self-contained data-plane engine with its own HTTP surface.services/resourcediscovery: a cross-service engine that consumes other drivers.services/cost,services/pricing,services/scope: cross-cutting utilities, not a cloud capability (pricingis the rate-table lookup that feeds thecostestimate model).
A new portable-API service must have driver/.
Cross-cutting behaviors (not capabilities) live under features/ instead:
recorder, metrics, ratelimit, inject, chaos, topology, plus vcr
(wire record/replay), timetravel (named state save/rewind/fork) and quota
(per-service quota registry). These have no driver/ either.
When a service grows a large or self-contained sub-surface, move it into a subdirectory
with the same name in each layer instead of keeping many flat files. The example below
is only an illustration. No service is laid out this way today (see the note on flat
exceptions below); traffic mirroring is currently the flat
providers/aws/vpc/traffic_mirror.go / server/aws/ec2/traffic_mirror.go:
services/networking/traffic_mirror/ # interface fragment
providers/aws/networking/traffic_mirror/ # mock
server/aws/networking/traffic_mirror/ # wire
Rule of thumb: a sub-surface with more than about 5 files, or a self-contained sub-API,
gets its own same-named subdirectory in the layers it touches. Smaller sub-surfaces stay
as flat <feature>.go files. Either way the file or directory name is the same in every
layer, so each feature has one home.
Subdirectories are for new services designed that way from the start, where each
sub-surface owns its own state. They are not retrofitted onto existing large
single-Mock providers such as providers/aws/vpc and providers/azure/databricks,
which stay flat on purpose. In Go a directory is a package, so moving a sub-surface
out would mean splitting one Mock (vpc: 256 methods, 57 fields under a single mutex)
into sub-packages. That needs a shared internal helper package, callbacks between
surfaces, and a change to the single-lock concurrency model. The behavior risk isn't
worth it, since the per-feature snake_case files (§3) already make the code easy to
navigate. For these services, keep one package with one <feature>.go per sub-surface.
A new resource on an existing service: add <feature>.go (same filename) to the
provider mock and the wire handler, and, if it is part of the portable API, to the
driver/ interface and the services/<capability> wrapper. Add <feature>_test.go
next to each.
A new service: create services/<capability>/driver/driver.go +
services/<capability>/<capability>.go, then providers/<cloud>/<service>/ and
server/<cloud>/<service>/ for each cloud you implement, all following §2 to §4. Wire it
into the provider factory (providers/<cloud>/<cloud>.go) and SetMonitoring() if it
emits metrics.
The rules above are the target. Older code that predates them is brought in line a little at a time, not all at once:
- One service per PR. A rename is a mechanical
git mvplus import fixes and a factory rename, with no behavior change in that PR. - Start with the clearest wins (drop the cloud prefixes in §2.2; the
snake_casefilename fixes in §3). - Keep each PR small so it rebases cleanly on top of other work in progress. Almost every branch edits the factory and wire files, so a repo-wide rename would conflict with everything.
The Structure CI job fails on a new cloud-prefixed directory, and an advisory step
flags a feature whose filename differs between the provider and wire layers.