Builders Documentation
Overview
Builders assemble and persist deployment artifacts from fully-loaded services. They follow a three-phase lifecycle (before_build → build → after_build) and accumulate errors and messages instead of raising exceptions.
Available builders:
Class |
Module |
Purpose |
|---|---|---|
|
|
Abstract base — error/message accumulation + lifecycle hooks |
|
|
Assembles |
|
|
Generates |
|
|
Generates |
|
|
Generates |
|
|
Generates |
|
|
Generates CycloneDX 1.6 JSON SBOM from the platform artifact |
BaseBuilder
Abstract base class that all builders extend.
from abc import ABC, abstractmethod
from pathlib import Path
class BaseBuilder(ABC):
def __init__(self, verbose: bool = False) -> None: ...
def has_errors(self) -> bool: ...
def has_messages(self) -> bool: ...
def get_errors(self) -> List[str]: ...
def get_messages(self) -> List[str]: ...
@abstractmethod
def build(self, deployment_service, work_path, build_path, dry_run=False) -> bool: ...
@abstractmethod
def before_build(self, deployment_service, work_path, build_path) -> bool: ...
@abstractmethod
def after_build(self, deployment_service, work_path, build_path, dry_run=False) -> bool: ...
Extending BaseBuilder
from pathlib import Path
from strata.builders.base_builder import BaseBuilder
class MyBuilder(BaseBuilder):
def before_build(self, deployment_service, work_path, build_path):
if not deployment_service.is_validated():
self._errors.append("Service not validated")
return False
return True
def build(self, deployment_service, work_path, build_path, dry_run=False):
# assemble artifacts
return True
def after_build(self, deployment_service, work_path, build_path, dry_run=False):
# verify output files
return True
PlatformBuilder
Assembles a PlatformArtifactModel from a fully-loaded DeploymentService hierarchy and writes platform.json / platform.yaml to the deployment build path.
from pathlib import Path
from strata.builders.strata_builder import PlatformBuilder
builder = PlatformBuilder(verbose=True, configuration_service=config_svc)
if not builder.before_build(deployment_service, work_path, build_path):
print("Pre-build failed:", builder.get_errors())
elif not builder.build(deployment_service, work_path, build_path, dry_run=False):
print("Build failed:", builder.get_errors())
elif not builder.after_build(deployment_service, work_path, build_path):
print("Post-build failed:", builder.get_errors())
else:
print("Built:", builder._last_platform_model.meta.name)
Constructor
PlatformBuilder(
verbose: bool = False,
configuration_service=None, # optional — used for artifact_path computation
)
Three-Phase Pipeline
before_build(deployment_service, work_path, build_path) → bool
Verifies
deployment_service.is_validated()— error if not validatedVerifies
deployment_service.get_workspace_service()returns a workspace — error if None
build(deployment_service, work_path, build_path, dry_run=False) → bool
Calls
_build_platform(deployment_service)to assemble thePlatformArtifactModelin memoryIf
dry_run=True: logs planned output file paths, returnsTruewithout writing to diskIf
dry_run=False: persistsplatform.jsonandplatform.yamlto the deployment build path
The assembled model is stored in builder._last_platform_model for downstream use (e.g. passing to TerraformBuilder.build()).
after_build(deployment_service, work_path, build_path, dry_run=False) → bool
If
dry_run=True: returnsTrueimmediately (skips file-existence checks)Verifies
platform.jsonandplatform.yamlexist in the deployment build path
Build Path
The deployment build path is build_path / "{deployment-name}-{deployment-version}" as returned by deployment_service.get_build_path(build_path).
TerraformBuilder
Generates Terraform .tfvars.json files from a PlatformArtifactModel. The builder tracks all variable, feature, and secret references encountered and writes requirement manifests alongside the generated artifacts.
Security note:
TerraformBuilderNEVER writes resolved values for variables, secrets, or features. It only documents which keys are required.
from pathlib import Path
from strata.builders.terraform_builder import TerraformBuilder
builder = TerraformBuilder(verbose=True)
# dry_run with a pre-assembled model (avoids reading platform.json from disk)
if not builder.before_build(deployment_service, work_path, build_path, dry_run=True):
print("Pre-build failed:", builder.get_errors())
elif not builder.build(
deployment_service, work_path, build_path,
dry_run=True,
platform_model=platform_builder._last_platform_model,
):
print("Build failed:", builder.get_errors())
Constructor
TerraformBuilder(verbose: bool = False)
Requirement Tracking
The builder accumulates references to variables, features, and secrets during _build_terraform_vars(). These are tracked in:
Attribute |
Type |
Contents |
|---|---|---|
|
|
Variables referenced by resources/providers/modules |
|
|
Feature flags referenced |
|
|
Secrets referenced |
Each entry has: key, description, required, suggested_env_var (variables only), used_by.
The refs are reset at the start of each build() call to avoid stale data from previous runs.
Three-Phase Pipeline
before_build(deployment_service, work_path, build_path, dry_run=False) → bool
Verifies
deployment_service.is_validated()— error if not validatedIf
dry_run=False: verifiesplatform.jsonexists in the deployment build path — error if missing (runPlatformBuilderfirst)
build(deployment_service, work_path, build_path, dry_run=False, platform_model=None) → bool
Resets requirement tracking dicts
If
platform_modelis supplied: uses it directly (no disk read)If
platform_model=Noneanddry_run=False: loadsplatform.jsonfrom the deployment build pathCalls
_build_terraform_vars()to assemble all payloadsIf
dry_run=True: logs planned file paths and requirement counts, returnsTruewithout writingIf
dry_run=False: writes all files to{deployment_build_path}/terraform/
after_build(deployment_service, work_path, build_path, dry_run=False) → bool
If
dry_run=True: returnsTrueimmediatelyVerifies the 7 base artifact files exist in
{deployment_build_path}/terraform/
Output Files
All files are written to {deployment_build_path}/terraform/:
File |
Contents |
|---|---|
|
Workspace identity, labels, metadata |
|
Provider configurations keyed by name |
|
Topology definitions with components/volumes |
|
Module source paths and properties |
|
Resources grouped by |
|
Required variable keys (no values) |
|
Required feature flag keys (no values) |
|
Required secret keys (no values) |
AnsibleBuilder
Generates Ansible-native YAML variable files from a PlatformArtifactModel. The files are written into each Ansible provisioner’s build directory. The AnsibleDeployer auto-discovers them at deploy time and passes them as --extra-vars @file.yml arguments.
Security note:
AnsibleBuilderNEVER writes resolved variable, feature, or secret values. It documents workspace structure and resource configuration so playbooks know what is available — secrets must be injected separately at deploy time.
from strata.builders.ansible_builder import AnsibleBuilder
builder = AnsibleBuilder(verbose=True)
# dry_run with a pre-assembled model (avoids reading platform.json from disk)
if not builder.before_build(deployment_service, work_path, build_path, dry_run=True):
print("Pre-build failed:", builder.get_errors())
elif not builder.build(
deployment_service, work_path, build_path,
dry_run=True,
platform_model=platform_builder._last_platform_model,
):
print("Build failed:", builder.get_errors())
Constructor
AnsibleBuilder(verbose: bool = False)
Three-Phase Pipeline
before_build(deployment_service, work_path, build_path, dry_run=False, solution_controller=None) → bool
Verifies
deployment_service.is_validated()— error if not validatedIf
dry_run=False: verifiesplatform.jsonexists in the deployment build path — error if missing (runPlatformBuilderfirst)
build(deployment_service, work_path, build_path, dry_run=False, platform_model=None, solution_controller=None) → bool
If
platform_modelis supplied: uses it directly (no disk read)If
platform_model=None: loads and validatesplatform.jsonviaPlatformServiceCalls
_build_ansible_vars()to assemble all payloadsIf
dry_run=True: logs planned file paths, returnsTruewithout writingIf
dry_run=False: writes all files to each resolved Ansible provisioner build path
after_build(deployment_service, work_path, build_path, dry_run=False, solution_controller=None) → bool
If
dry_run=True: returnsTrueimmediatelyFor each resolved Ansible provisioner path: verifies at least one
strata_*.ymlfile exists
Output Files
All files are written to {provisioner_build_path}/ (one set per Ansible provisioner found in the deployment).
File names use the prefix strata_ so the deployer can discover them via glob.
File |
Top-level YAML key |
Contents |
|---|---|---|
|
|
Workspace name, version, environment, labels |
|
|
Provider configurations keyed by name |
|
|
Topology definitions with components and volumes |
|
|
All resources in a single dict keyed by name |
|
|
Resources grouped by |
|
|
Module source paths and properties |
|
|
Namespace definitions |
|
|
Firewall rules keyed by name |
|
|
DNS zone definitions |
|
|
Network configurations |
Playbook Variable Access
# strata_workspace.yml
- name: Configure deployment
hosts: all
vars_files:
- strata_workspace.yml
tasks:
- debug:
msg: "Deploying {{ strata_workspace.name }} v{{ strata_workspace.version }}"
# strata_resources.yml
- name: Provision resources
hosts: all
tasks:
- name: Iterate all resources
loop: "{{ strata_resources | dict2items }}"
debug:
msg: "Resource {{ item.key }}: {{ item.value.type }}"
# strata_resx_<type>.yml (e.g. strata_objectstorage.yml)
- name: Create object storage buckets
hosts: all
tasks:
- name: Create bucket
loop: "{{ strata_objectstorage | dict2items }}"
# ...
When AnsibleDeployer discovers these files, it passes them automatically — no manual vars_files block is needed in the playbook. Variable precedence: files passed via -e @file.yml take priority over vars_files and inventory vars.
ComposeBuilder
Generates a docker-compose.yml file for each namespace that contains at least one module with spec.type: compose.
Security note:
ComposeBuilderNEVER writes resolved secret, variable, or feature values. References are emitted as${KEY}substitution tokens and injected by the deployer via a.envfile at deploy time.
Output Location
{deployment_build_path}/{namespace}/docker-compose.yml
One file per namespace. All compose modules in the same namespace are merged into a single file.
Service Naming
Condition |
Service name in compose |
|---|---|
|
|
|
|
Entries in depends_on are automatically rewritten from their short module-local names to their full prefixed form using the same rule.
Cross-Module Dependencies
To depend on a service in another module within the same namespace, use @module/service syntax:
Syntax |
Meaning |
|---|---|
|
Intra-module: service |
|
Cross-module: service |
|
Cross-module shorthand: service whose name equals the module name |
Cross-module refs are validated at build time — the builder checks that the target module exists in the namespace and the service exists in that module. Intra-module refs are validated at module load time.
Example:
# Module: mod_web (depends on mod_auth in the same namespace)
spec:
type: compose
services:
- name: website
image: nginx:alpine
depends_on:
- "@mod_auth/server" # cross-module
- database # intra-module
- name: database
image: postgres:16
Generated output:
services:
mod_web-website:
image: nginx:alpine
depends_on:
- mod_auth-server
- mod_web-database
mod_web-database:
image: postgres:16
Environment Variable Sources
YAML source field |
Value emitted in compose |
|---|---|
|
|
|
|
|
|
|
|
Volume Conventions
Mount type |
YAML field |
Compose entry |
Top-level volume declared? |
|---|---|---|---|
Named Docker volume |
|
|
Yes — |
Bind mount |
|
|
No |
Healthcheck Types
|
|
Docker Compose |
|---|---|---|
any |
|
|
|
— |
|
|
— |
|
Module YAML Example
apiVersion: strata.huybrechts.xyz/v1
kind: module
meta:
name: mymod
spec:
type: compose
services:
- name: redis
image: redis:7-alpine
restart: unless-stopped
environment:
- key: REDIS_PASSWORD
secret: REDIS_PASSWORD
ports:
- "6379:6379"
mounts:
- volume_ref: redis-data
target_path: /data
healthcheck:
type: command
command: ["redis-cli", "ping"]
interval: 30s
timeout: 5s
retries: 3
- name: api
image: myapp:latest
depends_on: [redis]
environment:
- key: REDIS_URL
value: redis://redis:6379
Generated docker-compose.yml
Given the module above in namespace myns:
services:
mymod-redis:
image: redis:7-alpine
restart: unless-stopped
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
ports:
- "6379:6379"
volumes:
- myns_mymod_redis-data:/data
healthcheck:
test: [CMD, redis-cli, ping]
interval: 30s
timeout: 5s
retries: 3
mymod-api:
image: myapp:latest
depends_on:
- mymod-redis
environment:
REDIS_URL: redis://redis:6379
volumes:
myns_mymod_redis-data: {}
.env File and Secret Injection
All ${KEY} tokens in the generated compose file are resolved by Docker Compose at startup from a .env file placed next to docker-compose.yml. The deployer is responsible for writing that file — ComposeBuilder produces no .env output. See the deployer documentation for how secrets and variables are sourced and written at deploy time.
configuration: Escape Hatch
Any keys placed under configuration: on a service entry are merged verbatim into the generated compose service block (last-wins). Use this for compose-specific fields that the Strata module model does not natively expose — for example logging, cap_add, or sysctls:
spec:
services:
- name: api
image: myapp:latest
configuration:
logging:
driver: json-file
options:
max-size: "10m"
cap_add:
- NET_ADMIN
Three-Phase Pipeline
before_build → bool
Verifies
deployment_service.is_validated()— error if not validatedVerifies
deployment_service.get_workspace_service()returns a workspace — error if None
build(deployment_service, work_path, build_path, dry_run=False) → bool
Iterates all namespaces from
deployment_service.get_namespace_services()For each namespace: loads each referenced module, skips modules where
spec.type != composeRenders services and named volumes into a compose document
If
dry_run=True: logs the planned output path (when verbose), returnsTruewithout writingIf
dry_run=False: writesdocker-compose.ymlto{deployment_build_path}/{namespace}/
after_build → bool
Always returns True. A namespace that contains no compose modules is not an error condition.
HelmBuilder
Generates values.yaml and meta.yaml artifacts for each module with spec.type: helm, ready for use with helm install --values.
Security note:
HelmBuilderNEVER writes resolved variable, secret, or feature values. References are emitted as${KEY}substitution tokens and injected by the deployer viahelm install --setflags or a secrets values file at deploy time.
Output Location
{deployment_build_path}/{namespace_name}/{module_name}/values.yaml
{deployment_build_path}/{namespace_name}/{module_name}/meta.yaml
Two files per module. Each module with spec.type: helm produces its own pair.
Service Naming
Service keys in values.yaml follow the same prefix rule as ComposeBuilder:
Condition |
Key in |
|---|---|
|
|
|
|
Environment Variable Sources
YAML source field |
Value emitted in |
|---|---|
|
|
|
|
|
|
|
|
PVC Persistence
Mounts with a storage_class field generate a persistence.{mount_name} block in the service entry. Bind mounts and volume refs are not emitted by HelmBuilder — only Kubernetes PVC mounts produce output.
Mount field |
|
|---|---|
|
block key (e.g. |
|
|
|
|
|
|
meta.yaml Contents
Field |
Source |
Fallback |
|---|---|---|
|
|
module name |
|
|
strata namespace name |
Module YAML Example
apiVersion: strata.huybrechts.xyz/v1
kind: module
meta:
name: authentik
spec:
type: helm
release_name: authentik
kubernetes_namespace: auth
references:
secrets: [AUTHENTIK_SECRET_KEY, DB_PASSWORD]
variables: [AUTHENTIK_VERSION]
services:
- name: server
image: ghcr.io/goauthentik/server:latest
environment:
- key: AUTHENTIK_SECRET_KEY
secret: AUTHENTIK_SECRET_KEY
- key: VERSION
var: AUTHENTIK_VERSION
mounts:
- name: media
storage_class: standard
access_mode: ReadWriteOnce
storage_size: 5Gi
target_path: /media
- name: worker
image: ghcr.io/goauthentik/server:latest
command: [worker]
environment:
- key: AUTHENTIK_SECRET_KEY
secret: AUTHENTIK_SECRET_KEY
Given the module above in namespace myns:
Generated myns/authentik/values.yaml
authentik-server:
env:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
VERSION: ${AUTHENTIK_VERSION}
persistence:
media:
storageClass: standard
accessMode: ReadWriteOnce
size: 5Gi
authentik-worker:
env:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
Generated myns/authentik/meta.yaml
releaseName: authentik
namespace: auth
${KEY} Injection at Deploy Time
All ${KEY} tokens in values.yaml are not resolved by the builder. At deploy time the deployer substitutes them either via:
helm install --set KEY=value ...flags, ora secrets values file passed as
helm install --values secrets.yaml
HelmBuilder produces no secrets output of its own. See the deployer documentation for how variables and secrets are sourced and injected.
configuration: Escape Hatch
Any keys placed under configuration: on a service entry are merged verbatim into that service’s block in values.yaml (last-wins). Use this for chart-specific fields that the Strata module model does not natively expose:
spec:
services:
- name: server
image: ghcr.io/goauthentik/server:latest
configuration:
resources:
requests:
cpu: 100m
memory: 256Mi
podAnnotations:
prometheus.io/scrape: "true"
Three-Phase Pipeline
before_build → bool
Verifies
deployment_service.is_validated()— error if not validatedVerifies
deployment_service.get_workspace_service()returns a workspace — error if None
build(deployment_service, work_path, build_path, dry_run=False) → bool
Iterates all namespaces from
deployment_service.get_namespace_services()For each namespace: loads each referenced module, skips modules where
spec.type != helmFor each helm module: renders
values.yamlandmeta.yamlin memoryIf
dry_run=True: logs the planned output paths (when verbose), returnsTruewithout writingIf
dry_run=False: writes both files to{deployment_build_path}/{namespace}/{module}/
after_build → bool
Always returns True. A namespace that contains no helm modules is not an error condition.
SbomBuilder
Generates a CycloneDX 1.6 JSON Software Bill of Materials from an existing platform.json artifact and writes sbom.json to the same deployment build directory.
Output Location
{deployment_build_path}/sbom.json
Report Modes
--report selects what strata build sbom produces:
Mode |
Output |
Use for |
|---|---|---|
|
|
Supply-chain scanning, compliance |
|
Human-readable grouped listing to stdout (or |
Onboarding, quick platform overview |
# Write sbom.json (default)
strata build sbom -f xyz-deploy-prd.yaml
# Print a human-readable inventory to the terminal
strata build sbom -f xyz-deploy-prd.yaml --report inventory
# Save the inventory to a file
strata build sbom -f xyz-deploy-prd.yaml --report inventory --output-file inventory.txt
Both modes run the same collector pipeline — no second pass.
Collector Pattern
SbomBuilder delegates component discovery to pluggable collectors. Each collector implements BaseSbomCollector and is responsible for one artifact type. The default set (in execution order):
Collector |
Source |
PURL type |
Scan mode |
|---|---|---|---|
|
|
|
— |
|
|
|
✅ |
|
Provisioners with |
|
— |
|
|
|
✅ |
|
|
|
✅ |
|
|
|
✅ |
|
|
|
✅ |
|
Dependency manifests in workspace repos (see below) |
|
✅ |
Scan mode (strata build sbom --scan PATH) runs all collectors against an arbitrary
directory without requiring a strata workspace, deployment file, or platform.json.
Model-dependent collectors (ContainerImageCollector, HelmChartCollector) return
empty results gracefully. File-based collectors scan the directory tree normally.
Collectors are injectable — pass collectors=[…] to the constructor for testing or extension:
from strata.builders.sbom_builder import SbomBuilder
from strata.builders.sbom.image_collector import ContainerImageCollector
builder = SbomBuilder(collectors=[ContainerImageCollector()])
builder.before_build(deployment_service, work_path, build_path)
builder.build(deployment_service, work_path, build_path)
builder.after_build(deployment_service, work_path, build_path)
ref = builder.sbom_reference # SbomReferenceModel with path, sha256, component_count
Three-Phase Pipeline
before_build
Verifies
deployment_service.is_validated()— error if not validatedUnless
dry_run=True, verifiesplatform.jsonexists in the build path — error if missing
build
Runs each collector in order, draining warnings to the logger immediately
If
dry_run=True: logs planned component count, returnsTruewithout writingIf
dry_run=False: builds CycloneDX BOM, serializes to JSON, writessbom.json, computes SHA-256, storesSbomReferenceModelonself.sbom_reference
after_build
Verifies sbom.json exists on disk (skipped in dry-run). Returns False and adds an error if the file is absent.
DependencyFileCollector — Scope Control
DependencyFileCollector scans workspace repos for dependency manifests
(requirements*.txt, pyproject.toml, uv.lock, package-lock.json, go.sum).
To keep build times predictable it limits its search:
Scan paths — taken from solution.json → spec.repositories (the repos registered
in the solution). If no repositories are registered, it falls back to walking
work_path.
Skip dependency scanning entirely — pass --no-deps to strata build sbom to
exclude DependencyFileCollector from the run. Useful for large repos where lockfile
scanning is slow and dependency coverage is not needed for the current build.
strata build sbom -f xyz-deploy-prd.yaml --no-deps
Ignore patterns — controlled by .strata/sbom-ignore.yaml (optional):
# .strata/sbom-ignore.yaml
ignore_paths:
- "**/node_modules" # always ignored by default
- "**/.venv" # always ignored by default
- "docs/**" # example: skip docs folder
ignore_files:
- "requirements-dev.txt"
- "requirements-test.txt"
Default ignored paths include **/node_modules, **/.venv, **/venv, **/dist,
**/build, **/__pycache__, and **/.git. Entries in sbom-ignore.yaml are
additive — they extend the defaults rather than replace them.
CVE Audit (--audit)
After generating the SBOM, strata can optionally scan it for known vulnerabilities
using a locally-installed CVE scanner. No network calls to external APIs — the
scanner runs locally against the generated sbom.json.
Prerequisites: install Trivy (preferred) or Grype in your PATH. Strata auto-detects whichever is available (Trivy takes priority if both exist).
# Advisory mode — prints findings, never fails the build
strata build sbom -f xyz-deploy-prd.yaml --audit
# CI gate — fail (exit code 3) if HIGH or CRITICAL vulnerabilities exist
strata build sbom --scan . --audit --fail-on HIGH
# Only report CRITICAL vulnerabilities
strata build sbom --scan . --audit --severity CRITICAL --fail-on CRITICAL
Flag |
Default |
Description |
|---|---|---|
|
off |
Enable CVE scanning after SBOM generation |
|
|
Minimum severity to include: |
|
(none) |
Exit code 3 if findings at this severity or above exist |
Console output shows a severity summary table and the top 10 findings.
NDJSON output (--output ndjson) emits each finding as a data event with
an audit_finding payload. JSON output includes an audit key with
severity counts.
When no scanner is found in PATH, --audit prints a warning and continues — it
never blocks a build due to missing tooling.
Built-in lockfile formats
File pattern |
Language / Ecosystem |
Parser |
|---|---|---|
|
Python / |
|
|
Python / |
|
|
Python / |
|
|
Node.js / |
|
|
Go / |
|
|
.NET / |
|
|
.NET / |
|
|
Java / |
|
|
Java / |
|
|
Ruby / |
|
|
Rust / |
|
|
PHP / |
|
Each parser lives in strata.builders.sbom.lockfile_parsers.<language> and is
exported from the package __init__.py.
Additional formats are added via workspace lockfile parser plugins — see below.
Extending the SBOM
Workspace plugins
Teams can add collectors and lockfile parsers without forking strata. There are two ways to add a lockfile parser:
Option A — Drop folder (zero config): place any .py file in
.strata/lockfile_parsers/ and it is imported automatically. No YAML entry
needed. Files starting with _ are skipped.
Option B — Explicit config: declare the plugin in .strata/collectors.yaml.
This is required for full BaseSbomCollector plugins, and is an alternative for
lockfile parsers when you want to store them outside the drop folder.
Plugin declarations in .strata/collectors.yaml:
# .strata/collectors.yaml
collectors:
# A full BaseSbomCollector subclass — appended after the built-ins
- name: cargo
path: .strata/plugins/cargo_collector.py
class: CargoCollector
type: collector
# A LockfileParser subclass — auto-registered into DependencyFileCollector
- name: cargo-lock
path: .strata/plugins/cargo_lockfile_parser.py
type: lockfile_parser # class is optional — __init_subclass__ handles it
path is relative to the workspace root. Use module instead of path to load
from an installed package:
- name: cargo-lock
module: my_company.strata_plugins.cargo
type: lockfile_parser
strata sln init creates .strata/plugins/ with two annotated starter templates,
plus a commented-out collectors.yaml stub and an sbom-ignore.yaml stub:
my_collector.py— skeleton for aBaseSbomCollectorsubclassmy_lockfile_parser.py— skeleton for aLockfileParsersubclasscollectors.yaml— plugin declarations (all examples commented out by default)sbom-ignore.yaml— ignore rules forDependencyFileCollector
Writing a collector plugin
A collector plugin reads from any source (files, APIs, environment) and returns
SbomComponentModel instances:
# .strata/plugins/my_collector.py
from pathlib import Path
from typing import List
from strata.builders.sbom.base_sbom_collector import BaseSbomCollector
from strata.models.sbom_model import SbomComponentModel
class CargoCollector(BaseSbomCollector):
def get_collector_name(self) -> str:
return "cargo"
def collect(
self,
platform, # PlatformArtifactModel
work_path: Path,
deployment_build_path: Path,
) -> List[SbomComponentModel]:
components = []
# ... scan files, call APIs, etc.
# Use self._add_warning(msg) for non-fatal issues.
components.append(SbomComponentModel(
name="serde",
version="1.0.193",
purl="pkg:cargo/serde@1.0.193",
source_collector=self.get_collector_name(),
))
return components
Then declare it in .strata/collectors.yaml with type: collector.
Writing a lockfile parser plugin
A lockfile parser plugin is even simpler — it reads one file format and returns
(name, version) pairs. DependencyFileCollector handles purl construction,
deduplication, and SbomComponentModel creation:
# .strata/plugins/cargo_lockfile_parser.py
import tomllib
from pathlib import Path
from typing import List
from strata.builders.sbom.lockfile_parsers import LockfileParser, RawDependency
# Defining this class is all that's needed.
# __init_subclass__ fires on class body execution and calls
# DEFAULT_REGISTRY.register(CargoLockParser()) automatically.
class CargoLockParser(LockfileParser):
@property
def ecosystem(self) -> str:
return "cargo"
def filename_patterns(self) -> List[str]:
return ["Cargo.lock"]
def parse(self, path: Path) -> List[RawDependency]:
with path.open("rb") as fh:
data = tomllib.load(fh)
return [
RawDependency(name=pkg["name"], version=pkg.get("version"))
for pkg in data.get("package", [])
]
Option A — Drop folder (zero config): save the file as
.strata/lockfile_parsers/cargo.py and it is imported automatically on the next
build. No YAML entry needed.
Option B — Explicit config: declare it in .strata/collectors.yaml:
collectors:
- name: cargo-lock
path: .strata/plugins/cargo_lockfile_parser.py
type: lockfile_parser # class is optional
Test isolation
Because LockfileParser.__init_subclass__ fires at class-definition time into the
module-level DEFAULT_REGISTRY, test parsers must use register=False to avoid
polluting the registry across test runs:
class FakeParser(LockfileParser, register=False):
...
Alternatively inject a fresh registry:
from strata.builders.sbom.lockfile_parsers import LockfileParserRegistry
from strata.builders.sbom.deps_collector import DependencyFileCollector
registry = LockfileParserRegistry()
registry.register(MyTestParser())
collector = DependencyFileCollector(registry=registry)
Typical Build Sequence
from pathlib import Path
from strata.builders.strata_builder import PlatformBuilder
from strata.builders.terraform_builder import TerraformBuilder
work_path = Path("/workspace")
build_path = Path("/workspace/.strata/build/")
# Step 1: Build the platform model
platform_builder = PlatformBuilder(verbose=True, configuration_service=config_svc)
platform_builder.before_build(deployment_svc, work_path, build_path)
platform_builder.build(deployment_svc, work_path, build_path, dry_run=False)
platform_builder.after_build(deployment_svc, work_path, build_path)
# Step 2: Build Terraform artifacts using the in-memory model
tf_builder = TerraformBuilder(verbose=True)
tf_builder.before_build(deployment_svc, work_path, build_path, dry_run=False)
tf_builder.build(
deployment_svc, work_path, build_path,
dry_run=False,
platform_model=platform_builder._last_platform_model,
)
tf_builder.after_build(deployment_svc, work_path, build_path)
# Check for errors
for err in platform_builder.get_errors() + tf_builder.get_errors():
print("ERROR:", err)