The OmegaConf Inventory Backend
Overview
The OmegaConf inventory backend is a powerful alternative to reclass, offering enhanced interpolation capabilities, custom resolvers, and deferred evaluation.
Warning
OmegaConf is currently in experimental mode. If you encounter unexpected errors or bugs, please let us know and create an issue.
How It Works
flowchart TD
A[Load Target YAML] --> B[Load Classes Recursively]
B --> C[Merge Parameters]
C --> D[First Resolution Pass]
D --> E{Escaped Interpolations?}
E -->|Yes| F["Unescape \${...} → `${...}`"]
F --> G[Second Resolution Pass]
E -->|No| H[Process Literal Markers]
G --> H
H --> I[Final Resolved Inventory]
style D fill:#e1f5fe
style G fill:#e1f5fe
style H fill:#fff3e0
The OmegaConf backend uses a double-pass resolution strategy:
- First pass: Resolves all standard
${...}interpolations - Second pass: Resolves previously escaped
\${...}interpolations (now unescaped)
This enables deferred evaluation - interpolations that resolve based on their final context after merging.
Installation
pip install kapitan[omegaconf]
Usage
Command Line
kapitan compile --inventory-backend=omegaconf
Permanent Configuration
Add to your .kapitan config file:
global:
inventory-backend: omegaconf
Quick Start Examples
Basic Interpolation
# inventory/targets/my-app.yml
parameters:
app_name: my-application
environment: production
# Simple interpolation
full_name: ${app_name}-${environment} # → "my-application-production"
# Nested access
config:
database:
host: db.example.com
port: 5432
# Reference nested values with dot notation
db_url: "postgresql://${config.database.host}:${config.database.port}"
# → "postgresql://db.example.com:5432"
Using Default Values
parameters:
# Provide defaults with oc.select
timeout: ${oc.select:custom_timeout, 30} # Uses 30 if custom_timeout not set
# Environment variables with defaults
log_level: ${oc.env:LOG_LEVEL, INFO}
Deferred Evaluation with \${...}
Use escaped interpolations when you need values to resolve after merging:
# inventory/classes/base-deployment.yml
parameters:
deployment_template:
name: \${parentkey:} # Resolves to the KEY name after merge
namespace: ${namespace} # Resolves immediately
# inventory/targets/my-app.yml
parameters:
namespace: production
deployments:
web-frontend: ${deployment_template} # name → "web-frontend"
api-backend: ${deployment_template} # name → "api-backend"
Result:
deployments:
web-frontend:
name: web-frontend # Resolved from \${parentkey:}
namespace: production
api-backend:
name: api-backend # Resolved from \${parentkey:}
namespace: production
Built-in Resolvers
OmegaConf Native Resolvers
| Resolver | Description | Example |
|---|---|---|
oc.env |
Access environment variable | ${oc.env:HOME} |
oc.select |
Default value for interpolation | ${oc.select:key, default} |
oc.dict.keys |
Get dictionary keys as list | ${oc.dict.keys:my_dict} |
oc.dict.values |
Get dictionary values as list | ${oc.dict.values:my_dict} |
Key & Path Resolvers
| Resolver | Description | Example |
|---|---|---|
key |
Current node's key name | ${key:} |
parentkey |
Parent node's key name | ${parentkey:} |
fullkey |
Full path to current key | ${fullkey:} |
relpath |
Convert absolute path to relative interpolation | ${relpath:some.absolute.path} |
filename |
Current file's name flag | ${filename:} |
parent_filename |
Parent node's filename flag | ${parent_filename:} |
path |
Current file's path flag | ${path:} |
parent_path |
Parent node's path flag | ${parent_path:} |
Conditional Resolvers
| Resolver | Description | Example |
|---|---|---|
if |
Return value if condition is true, else empty dict | ${if:${enabled}, {key: value}} |
ifelse |
Return first value if true, else second | ${ifelse:${prod}, prod-val, dev-val} |
and |
Boolean AND of all arguments | ${and:${a}, ${b}, ${c}} |
or |
Boolean OR of all arguments | ${or:${a}, ${b}} |
not |
Boolean NOT | ${not:${disabled}} |
equal |
Check if all arguments are equal | ${equal:${env}, production} |
Data Transformation Resolvers
| Resolver | Description | Example |
|---|---|---|
merge |
Merge multiple OmegaConf objects | ${merge:${base}, ${overrides}} |
dict |
Convert list of single-key dicts to one dict | ${dict:${list_of_dicts}} |
list |
Convert dict to list of single-key dicts | ${list:${my_dict}} |
yaml |
Render a key's value as YAML string | ${yaml:config.section} |
add |
Add or concatenate two values | ${add:${a}, ${b}} |
access |
Access keys containing dots | ${access:key.with.dots, subkey} |
Utility Resolvers
| Resolver | Description | Example |
|---|---|---|
escape |
Create an interpolation for next resolution pass | ${escape:some.path} → ${some.path} |
default |
Chain of fallback values using oc.select | ${default:key1, key2, fallback} |
write |
Write resolved content to another inventory location | ${write:destination.path, source.path} |
from_file |
Read content from a file | ${from_file:path/to/file.txt} |
Conditional Logic Examples
parameters:
environment: production
enable_debug: false
replicas: 3
# Simple condition
debug_config: ${if:${enable_debug}, {log_level: DEBUG, verbose: true}}
# → {} (empty, because enable_debug is false)
# If-else
resource_limits: ${ifelse:${equal:${environment}, production},
{cpu: "2", memory: "4Gi"},
{cpu: "500m", memory: "512Mi"}
}
# → {cpu: "2", memory: "4Gi"} (production)
# Combined conditions
enable_monitoring: ${and:${equal:${environment}, production}, ${replicas}}
# → true (both conditions met)
Custom Resolvers
Create powerful custom resolvers by adding a resolvers.py file in your inventory directory.
Basic Example
# inventory/resolvers.py
def uppercase(text: str) -> str:
"""Convert text to uppercase."""
return text.upper()
def prefix(text: str, prefix: str = "app") -> str:
"""Add a prefix to text."""
return f"{prefix}-{text}"
def get_suffix(name: str) -> str:
"""Extract suffix after last hyphen."""
return name.split("-")[-1]
# Required: Return dict of resolver_name -> function
def pass_resolvers():
return {
"upper": uppercase,
"prefix": prefix,
"get_suffix": get_suffix,
}
Usage:
parameters:
app_name: my-service
upper_name: ${upper:${app_name}} # → "MY-SERVICE"
prefixed: ${prefix:${app_name}, api} # → "api-my-service"
suffix: ${get_suffix:${app_name}} # → "service"
Advanced: Accessing Root Context
Custom resolvers can access the entire inventory using the _root_ parameter:
# inventory/resolvers.py
def vault_secret(path: str, key: str, _root_) -> str:
"""Generate a Vault reference using target context."""
target_name = _root_.target_name
return f"?{{vaultkv:{target_name}/{path}:{key}}}"
def helm_input(component: str, _root_) -> dict:
"""Generate Helm input configuration from component settings."""
comp = _root_[component]
return {
"input_type": "helm",
"input_paths": [f"charts/{comp.chart_name}/{comp.chart_version}"],
"output_path": f"k8s/{comp.namespace}",
"helm_params": {
"namespace": str(comp.namespace),
"name": str(comp.chart_name),
},
"helm_values": f"\\${{{component}.helm_values}}", # Deferred!
}
def pass_resolvers():
return {
"vault": vault_secret,
"helm_input": helm_input,
}
Usage:
parameters:
target_name: my-target
nginx:
chart_name: nginx
chart_version: "15.0.0"
namespace: web
helm_values:
replicaCount: 3
secrets:
db_password: ${vault:database, password}
# → "?{vaultkv:my-target/database:password}"
kapitan:
compile:
- ${helm_input:nginx}
Migration from Reclass
Automatic Migration
kapitan compile --inventory-backend=omegaconf --migrate
Danger
Backup your inventory before migrating! Use version control to easily revert changes if needed.
What Gets Migrated
| Before (Reclass) | After (OmegaConf) |
|---|---|
${path:to:key} |
${path.to.key} |
${_reclass_...} |
${_kapitan_...} |
\${escaped} |
${tag:escaped} |
Manual Changes May Be Needed
- Keys containing
.(dots) need special handling with theaccessresolver - Some reclass-specific features like
exportsare not yet supported
Differences from Reclass
✅ Supported
- compose-node-name
- key overwrite prefix
~ - interpolations (with dot notation)
- relative class names
- init class files
- nested interpolations
- escaped/deferred interpolations
❌ Not Yet Supported
- exports
- inventory queries
- interpolation to YAML keys containing
.
Complete Example: Multi-Environment Deployment
# inventory/classes/base/deployment.yml
parameters:
# Template that adapts to where it's merged
deployment_base:
name: \${parentkey:}
namespace: ${namespace}
labels:
app: \${parentkey:}
environment: ${environment}
image: ??? # Mandatory - must be set
replicas: ${oc.select:default_replicas, 1}
resources: ${ifelse:${equal:${environment}, production},
{limits: {cpu: "2", memory: "4Gi"}},
{limits: {cpu: "500m", memory: "512Mi"}}
}
# inventory/classes/components/api.yml
parameters:
api_config:
image: myregistry/api:${api_version}
replicas: 3
env:
DATABASE_URL: ${database.url}
# inventory/targets/production.yml
classes:
- base.deployment
- components.api
parameters:
environment: production
namespace: prod
api_version: "2.1.0"
database:
url: "postgres://prod-db:5432/app"
deployments:
api-server: ${merge:${deployment_base}, ${api_config}}
Resolved Result:
deployments:
api-server:
name: api-server # From \${parentkey:}
namespace: prod
labels:
app: api-server # From \${parentkey:}
environment: production
image: myregistry/api:2.1.0
replicas: 3
resources:
limits:
cpu: "2"
memory: "4Gi" # Production resources
env:
DATABASE_URL: "postgres://prod-db:5432/app"