Schema Server#
Overview#
Schema Server is a Go library and optional standalone gRPC service that parses YANG modules from disk using the SDC goyang fork, serialises the resulting schema objects into a persistent embedded database (Badger v4), and serves schema metadata to other SDC components on demand. It is the single source of truth for all type, namespace, and constraint information derived from YANG.
Deployment model. The schema-server Go module (github.com/sdcio/schema-server) is imported directly by data-server. In the current production deployment, data-server embeds the schema store in-process — no separate schema-server pod is required. The standalone gRPC binary exists for future split-out deployments. The cache component follows the same pattern: it can be embedded (type: local) or remote (type: remote).
Statefulness.
| Store type | Backing | Persistence | Notes |
|---|---|---|---|
persistent (default) | Badger v4 embedded KV at schema-store.path | Survives restarts | Keys: <vendor>@<name>@<version>/<path>; values: proto.Marshal(sdcpb.SchemaElem). If a key already exists on startup it is not re-parsed, so restarts are fast. |
memory | map[SchemaKey]*Schema behind sync.RWMutex | Lost on restart | Useful for testing or ephemeral environments. |
A background goroutine runs bdb.RunValueLogGC(0.7) every minute to reclaim Badger value-log space.
persistStore can optionally wrap Badger lookups with a ttlcache.Cache[cacheKey, *sdcpb.GetSchemaResponse] (capacity + TTL configurable) to avoid repeated protobuf unmarshalling on hot paths.
Architecture Diagram (Mermaid)#
graph TD
subgraph k8s["Kubernetes StatefulSet Pod"]
DS["Data Server\n(embeds schema-server in-process)"]
SS_GRPC["SchemaServer RPCs\nserved by data-server listener\n:56000 (embedded default)"]
BADGER[("Badger DB\npvc-schema-db → /schemadb")]
YANG_VOL[("Raw YANG files\npvc-schema-store → /schemas")]
DS -- "localClient\n(direct Go call)" --> SCHEMA_STORE["persistStore / memStore\n(pkg/store)"]
SCHEMA_STORE -- "read/write\nproto.Marshal(SchemaElem)" --> BADGER
SS_GRPC -- "delegates RPCs" --> SCHEMA_STORE
SCHEMA_STORE -- "parse .yang on CreateSchema\nor UploadSchema" --> YANG_PARSER["YANG parser\npkg/schema\n(goyang fork)"]
YANG_PARSER -- "reads" --> YANG_VOL
end
CTRL["Config Server\n(controller)"] -- "gRPC: CreateSchema\nDeleteSchema\nGetSchemaDetails" --> SS_GRPC
CTRL -- "git clone → YANG files" --> YANG_VOL For split-out deployments, a standalone schema-server binary can expose the same RPCs on its own listener (default :55000).
Responsibilities#
- Parse YANG. Consume
.yangfiles (plus include/exclude globs), build ayang.Entrytree via the SDC goyang fork, resolve cross-moduleleafref,augment, andusesreferences. - Persist schemas. Serialise each
yang.Entrynode as asdcpb.SchemaElemproto into Badger, keyed by vendor/name/version and path. Skip already-persisted keys on restart for fast cold-start. - Serve schema metadata. Answer
GetSchema,GetSchemaElements,ExpandPath, and related RPCs used by data-server to validate paths and values, resolve XML namespaces, and expand wildcarded paths. - Lifecycle management. Register, reload, and delete schemas at runtime without restarting the process.
- Upload gateway (future). Accept raw YANG file bytes over a client-streaming RPC (
UploadSchema), write them to disk, then parse and persist.
Schema Ingestion#
Static configuration at startup#
Schemas listed under schema-store.schemas in the YAML config are parsed concurrently at startup (one goroutine per schema entry). If the SchemaKey{Name, Vendor, Version} already exists in Badger the schema is skipped — restarts are idempotent and fast.
CreateSchema RPC#
The caller provides the vendor, name, version, and paths to YANG files already on disk. The store parses them immediately and writes the result to the configured schema store; in persistent mode that store is Badger.
UploadSchema RPC (three-phase client streaming)#
- First message —
UploadSchemaRequest_CreateSchema: identifies the schema (vendor, version, exclude patterns). - Subsequent messages —
UploadSchemaRequest_SchemaFile: chunked file bytes with an optional integrity hash (MD5, SHA-256, or SHA-512). - Files are written to
<schemas-directory>/<name>_<vendor>_<version>/. - On stream close the server parses the uploaded files and stores them to Badger.
UploadSchemais not actively used in the current K8s deployment; YANG files are supplied via a shared PVC cloned by the controller.
YANG parsing (pkg/schema/schema.go)#
schema.NewSchema(cfg)calls the SDC goyang fork to parse all.yangfiles matching the configuredfiles+directoriespaths, minus anyexcludespatterns.- A root
yang.Entrytree (__root__) is built where top-level children are individual YANG modules. buildReferencesAnnotation()walks the tree and resolves cross-moduleleafref,uses, andaugmentreferences, annotating eachyang.Entrynode.- Each node is serialised as
proto.Marshal(sdcpb.SchemaElem)and written to Badger with the key<vendor>@<name>@<version>/__root__/<module>/<path...>.
Disk layout#
<schema-store.path>/ # Badger DB (SSTs, value log, MANIFEST)
<schemas-directory>/
<name>_<vendor>_<version>/ # one directory per uploaded schema
*.yang # raw YANG source files
K8s deployment flow#
controller (Schema Reconciler)
→ git clone YANG files → pvc-schema-store (/schemas)
→ gRPC CreateSchema (paths on /schemas)
data-server (embedded schema-server)
→ parse YANG from /schemas
→ write Badger DB → pvc-schema-db (/schemadb)
Schema Serving#
GetSchema request flow:
- Client calls
GetSchema(GetSchemaRequest{Schema: {Vendor, Version}, Path: <path>}). persistStore.getSchemaconstructs a Badger key from the schema key + path elements and performs a point lookup.- Module prefix resolution: if the first path element contains
:the prefix selects the correct top-level module entry. - The raw bytes are unmarshalled from
proto.Marshal(sdcpb.SchemaElem)and returned. - TTL response cache: if configured,
persistStorechecks attlcache(keyed by{SchemaKey, path-as-string}) before hitting Badger, avoiding repeated unmarshalling on hot paths.
Hot-reload (ReloadSchema): re-parses YANG files from their original configured paths. Schema.Reload() calls NewSchema(sc.config) to produce a fresh Schema value and then store.AddSchema replaces the Badger entries atomically.
Per-datastore schema path cache (SchemaClientBound): each data-server datastore holds a SchemaClientBoundImpl that wraps the schema Client and binds it to a fixed {vendor, name, version} triple. It maintains a sync.Map indexed by keyless path string → *SchemaIndexEntry. On the first lookup of a path the response is stored in the index; subsequent lookups return the cached entry without touching the store. This is the primary per-datastore hot-path cache.
Configuration & Deployment#
CLI flags#
| Flag | Default | Description |
|---|---|---|
--config / -c | schema-server.yaml | Path to YAML config file |
--debug / -d | false | Set log level to DEBUG |
--trace / -t | false | Set log level to TRACE |
--version / -v | false | Print version and commit SHA |
Full YAML config reference#
grpc-server:
address: ":55000"
max-recv-msg-size: 4194304 # bytes
rpc-timeout: 1m
tls:
ca:
cert:
key:
skip-verify: false
schema-server:
schemas-directory: ./schemas-dir # landing dir for UploadSchema
schema-store:
type: persistent # "persistent" | "memory"
path: ./schema-store # Badger DB directory
read-only: false
cache:
ttl: 5m
capacity: 10000
with-description: false
schemas:
- name: srl
vendor: Nokia
version: 23.7.1
files:
- ./yang/srl/srl_nokia/models
directories:
- ./yang/srl/ietf
excludes:
- .*tools.*
prometheus:
address: ":9090"
gRPC interface#
Service: SchemaServer (proto package sdcpb, defined in sdc-protos/schema.proto)
Default standalone listen address: :55000
In the default embedded deployment, SchemaServer RPCs are served by the data-server process endpoint (commonly :56000), not by a separate :55000 listener.
| RPC | Streaming | Description |
|---|---|---|
GetSchema | Unary | Returns one SchemaElem for a gNMI-style path |
GetSchemaElements | Server-streaming | Streams all SchemaElems along a path |
GetSchemaDetails | Unary | Full details of a registered schema |
ListSchema | Unary | List known schemas (name, vendor, version, status) |
CreateSchema | Unary | Register schema metadata and trigger parse |
ReloadSchema | Unary | Re-parse YANG files for an existing schema |
DeleteSchema | Unary | Remove a schema from the store |
UploadSchema | Client-streaming | Upload raw YANG file bytes then trigger parse |
ToPath | Unary | Convert path element strings to a structured Path |
ExpandPath | Unary | Enumerate all concrete sub-paths under a wildcarded path |
All RPC handlers in pkg/server/rpcs.go are thin wrappers that delegate directly to the store.Store interface.
Startup behaviour#
main.go wraps server.Serve in a goto START loop with a 1-second back-off, so the process automatically recovers from transient initialisation errors without an external supervisor.
External dependencies at startup: none — the schema-server dials no external gRPC services; all communication is inbound.
Volume layout (K8s StatefulSet)#
| Volume | PVC | Mount path | Contents |
|---|---|---|---|
schema-store | pvc-schema-store | /schemas (controller + data-server) | Raw .yang files cloned from Git |
schema-db | pvc-schema-db | /schemadb (data-server only) | Badger DB of parsed SchemaElem protos |
Interactions#
| Direction | Peer component | Mechanism | Purpose |
|---|---|---|---|
| Inbound (embedded, current) | Data Server | Direct Go call via localClient (pkg/schema/local.go) | Validate paths and values, resolve XML namespaces, convert paths. No gRPC hop — in-process. |
| Inbound (remote, future) | Data Server | gRPC SchemaServer :55000 | Same as above when data-server is configured with a schema-server address instead of a local schema-store |
| Inbound (gRPC) | Config Server (controller) | gRPC SchemaServer to the schema-server K8s Service (resolves to the data-server pod) | Schema Reconciler calls GetSchemaDetails, CreateSchema, DeleteSchema |
| None | Cache | — | Schema-server has no dependency on the cache component |
Internal Package Tour#
| Package | Purpose | Key types / functions |
|---|---|---|
pkg/server | gRPC server init and lifecycle | Server, NewServer(), Serve(), Stop() |
pkg/server/rpcs.go | Thin RPC handlers delegating to store.Store | all SchemaServer RPC methods |
pkg/config | YAML config unmarshalling, TLS setup, validation | Config, SchemaStoreConfig, SchemaConfig, GRPCServer |
pkg/store | Store interface and SchemaKey type | Store, SchemaKey, Key() |
pkg/store/persiststore | Badger-backed store with optional TTL response cache | persistStore, New() |
pkg/store/memstore | In-memory store | memStore, New() |
pkg/schema | YANG parsing, yang.Entry → sdcpb.SchemaElem conversion | Schema, NewSchema(), SchemaElemFromYEntry() |
pkg/schema/container.go | Converts container/list nodes | containerFromYEntry() |
pkg/schema/leaf.go | Converts leaf/leaf-list nodes | leafFromYEntry() |
pkg/schema/references.go | Resolves leafref / augment / uses cross-module references | buildReferencesAnnotation() |
pkg/schema/expand.go | Path expansion for ExpandPath RPC | Expand() |