Plugins
5 minute read
Drasi’s sources, reactions, bootstrap providers, secret stores and identity providers
are plugins: self-contained native libraries (.so, .dylib, .dll) that this
host loads at runtime, exactly as drasi-server does. All five types can be loaded and
selected from Python. That means a Python application
can follow Postgres, Kafka or Kubernetes without anyone writing Python bindings for
each one.
Installing one
resolved = await drasi.install_plugin("source/postgres")
That resolves the reference, picks the build matching this host, downloads it, loads
it, and returns what it found. Short references are expanded against the default
registry, so source/postgres means ghcr.io/drasi-project/source/postgres.
Once loaded, the kind is available to add_source, add_reaction or the bootstrap
argument:
await drasi.add_source("postgres", "db", {...})
Finding one
for hit in await drasi.search_plugins("postgres"):
print(hit["reference"], [v["version"] for v in hit["versions"]])
Passing no query lists everything. list_plugin_tags shows the tags of a single
repository, and resolve_plugin reports what a reference would resolve to on this
host without downloading it.
The registry carries source, reaction, bootstrap, identity and secret-store plugins —
postgres, mysql, mssql, kafka, kubernetes and neo4j sources among them, and
dashboard, http, grpc, sse and mcp reactions. Rather than trust a list here
that goes stale as the registry grows, ask it:
kinds = {}
for hit in await drasi.search_plugins():
kinds.setdefault(hit["plugin_type"], []).append(hit["kind"])
Where plugins come from
Plugins are published as OCI artifacts:
ghcr.io/drasi-project/{type}/{kind}:{version}-{arch}
ghcr.io/drasi-project/source/postgres:0.2.7-linux-amd64
ghcr.io/drasi-project/reaction/dashboard:0.1.3-darwin-arm64
Note the per-architecture tags: these are not multi-arch image indexes, so the architecture is part of the tag.
Compatibility
A plugin is native code loaded into your process, so it is only usable by a host that matches it. There are three independent gates:
- Registry annotations — before downloading, the resolver compares the artifact’s
sdk-version,core-versionandlib-versionannotations against this host’s. They must match onmajor.minor. - FFI ABI version — after loading, the host compares the plugin’s reported SDK
version against its own
FFI_SDK_VERSION, which describes the layout of the structs crossing the boundary and is deliberately decoupled from crate versions. - Target triple — must match exactly. A
linux-amd64build cannot load into anaarch64-apple-darwinhost.
Inspect what this host offers:
>>> import drasi
>>> drasi.host_info()
{'arch_suffix': 'darwin-arm64',
'core_version': '0.5.7',
'ffi_sdk_version': '0.11.0',
'index_backends': ['memory', 'rocksdb'],
'lib_version': '0.8.9',
'sdk_version': '0.10.0',
'target_triple': 'aarch64-apple-darwin'}
An incompatible plugin raises PluginCompatibilityError at resolve time rather than
crashing later, and a reference with no build for this platform raises
PluginNotFoundError.
Signature verification
Plugins can be verified with cosign keyless signatures:
await drasi.install_plugin(
"source/postgres",
verify=True,
require_signed=True,
trusted_identities=[
("https://github.com/drasi-project/.*", "https://token.actions.githubusercontent.com")
],
)
verify=True checks a signature when one is present. require_signed=True additionally
refuses to install an unsigned plugin.
Verification failure raises PluginSignatureError.
Pinning with a lockfile
Resolving a floating reference gives you whatever is newest, which is not what you want in a deployment. Write a lockfile once:
await drasi.install_plugin("source/postgres")
await drasi.write_lockfile("plugins")
and install exactly those digests everywhere else:
await drasi.install_from_lockfile("plugins")
Drasi.read_lockfile(directory) returns the entries, each with the reference, resolved
digest and file hash.
Loading from disk
Plugins already on disk load without touching a registry, which is the usual choice in an air-gapped or container-built environment:
summary = await drasi.load_plugins("/opt/drasi/plugins")
print(summary["sources"], summary["reactions"], summary["bootstrap"])
watch_plugins(directory) keeps watching, picking up plugins added later.
A file is treated as a plugin if its name begins with drasi_ or libdrasi_
and it exports the plugin entry point; the name is a filter, the entry point is
the decision. Shared libraries in the directory that were not loaded are
reported as skipped, so a plugin that is present but unrecognised shows up as
a number rather than as nothing at all.
Plugin configuration
Configuration keys belong to the plugin, not to this library, so they are passed through untouched. Ask a loaded plugin what it accepts:
schema = await drasi.source_config_schema("postgres")
There are reaction_config_schema and bootstrap_config_schema equivalents. What those
keys mean, and which combinations are valid, is the plugin’s business — consult the
plugin’s own documentation rather than expecting this library to describe it.
Secrets
Rather than putting credentials in a config dict, a plugin can ask the host to resolve a reference:
drasi = await Drasi.create("app", secrets={"PGPASSWORD": os.environ["PGPASSWORD"]})
await drasi.add_source(
"postgres",
"db",
{
"host": "localhost",
"port": 5432,
"user": "postgres",
"database": "app",
"password": {"kind": "Secret", "name": "PGPASSWORD"},
"tables": ["public.orders"],
"tableKeys": [{"table": "orders", "keyColumns": ["id"]}],
},
)
The plugin never sees the value until it asks for it, and the config you log or store
holds only the reference. {"kind": "EnvironmentVariable", "name": "PGPASSWORD"} works
the same way, and takes an optional default.
Resolving from a secret store
Passing every secret to create means having them all to hand. A secret-store plugin
resolves them on demand instead:
await drasi.install_plugin("secret-store/file")
await drasi.use_secret_store("file", {"path": "/etc/myapp/secrets.json"})
From then on a reference a plugin cannot find in the mapping given to create is
looked up in the store. azure-keyvault and keyring are published alongside file;
ask each what it needs with secret_store_config_schema(kind).
A resolved value is cached, because the lookup crosses into the store’s own runtime and a plugin that re-reads its configuration on every reconnect would otherwise pay for it each time.
Identity providers
password and token are built into the engine. Any other identity kind names an
identity/* plugin:
drasi = await Drasi.create(
"app",
identity={"kind": "azure", "client_id": "..."},
plugins_dir="/opt/drasi/plugins",
)
plugins_dir is required here, and only here. An identity provider reaches the engine
through its builder, which runs before anything could call install_plugin, so the
plugin has to be on disk when create is called. Download it ahead of time with
install_plugin(..., load=False, directory=...), or ship it in your image.
Whether the credentials are then used is up to the component: a source or reaction
asks its identity provider only if it authenticates that way. Of the published sources,
dataverse does; postgres and the rest take their credentials from their own
configuration, so giving one an identity provider changes nothing.
Next
- Concepts: bootstrap — loading what already exists.
- Postgres dashboard example — plugins end to end.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.