Core Resolvers¶
HoloConf ships with four essential resolvers that cover the most common configuration needs. Let's explore each one and see how they solve real-world problems.
Environment Variables¶
The most common use case for dynamic configuration is reading from environment variables. This lets you change settings between development, staging, and production without editing files.
Let's start with a simple example:
Now try to use it:
The error is helpful - it prevents us from accidentally using undefined values. But we want our configuration to work during development without setting every variable. Let's add defaults:
Now it works both ways:
import os
from holoconf import Config
# Without environment variables - uses defaults
config = Config.load("config.yaml")
host = config.database.host
print(f"Host: {host}")
# Host: localhost
# With environment variables - uses env values
os.environ["DB_HOST"] = "prod-db.example.com"
config = Config.load("config.yaml")
host = config.database.host
print(f"Host: {host}")
# Host: prod-db.example.com
use holoconf::Config;
use std::env;
// Without environment variables
let config = Config::load("config.yaml")?;
let host: String = config.get("database.host")?;
println!("Host: {}", host);
// Host: localhost
// With environment variables
env::set_var("DB_HOST", "prod-db.example.com");
let config = Config::load("config.yaml")?;
let host: String = config.get("database.host")?;
println!("Host: {}", host);
// Host: prod-db.example.com
Perfect! Now your configuration works in development (using defaults) and production (using environment variables).
Self-References: Avoiding Duplication¶
As your configuration grows, you'll find yourself repeating values. Self-references let you define a value once and reuse it everywhere.
Let's say you have shared defaults:
defaults:
timeout: 30
host: localhost
database:
host: ${defaults.host}
timeout: ${defaults.timeout}
port: 5432
api:
host: ${defaults.host}
timeout: ${defaults.timeout}
port: 8000
Now when you access these values, they resolve to the shared defaults:
from holoconf import Config
config = Config.load("config.yaml")
db_host = config.database.host
api_host = config.api.host
print(f"Database: {db_host}, API: {api_host}")
# Database: localhost, API: localhost
# Both reference the same value
db_timeout = config.database.timeout
api_timeout = config.api.timeout
print(f"Timeouts: {db_timeout}, {api_timeout}")
# Timeouts: 30, 30
Relative References¶
But what if you want to reference a value that's nearby in the configuration tree? Absolute paths like ${defaults.host} work, but they're verbose. Use relative paths instead:
database:
host: localhost
port: 5432
# Reference sibling 'host' using dot prefix
connection_string: "postgres://${.host}:${.port}/mydb"
services:
database:
host: prod-db.example.com
api:
# Reference parent's sibling using double-dot
db_host: ${..database.host}
Let's see what these resolve to:
from holoconf import Config
config = Config.load("config.yaml")
# Sibling reference
conn_str = config.database.connection_string
print(f"Connection: {conn_str}")
# Connection: postgres://localhost:5432/mydb
# Parent's sibling reference
api_db = config.services.api.db_host
print(f"API uses DB: {api_db}")
# API uses DB: prod-db.example.com
When to Use Relative vs Absolute References
- Use
${.sibling}for values in the same section (like building connection strings) - Use
${..parent.sibling}for nearby values (like services referencing shared settings) - Use
${absolute.path}for shared defaults used across the entire config
Preventing Circular References¶
What happens if you create a circular reference?
HoloConf detects this and gives you a clear error:
The error shows you the exact reference chain, making it easy to fix.
File Includes: Splitting Large Configurations¶
Sometimes configuration is too large for a single value. Maybe you have a multi-line certificate, a JSON blob, or a YAML snippet. The file resolver lets you include content from external files.
Let's say you have a private key in a separate file:
When you access these values, the file content is included:
Including Structured Data¶
But what if the file contains YAML or JSON? HoloConf automatically parses it:
from holoconf import Config
config = Config.load("app.yaml")
# Returns parsed YAML as a list
users = config.users
print(f"Users: {users}")
# Users: [{'name': 'alice', 'role': 'admin'}, {'name': 'bob', 'role': 'user'}]
# Can reference into the structure
admin = config.admin_name
print(f"Admin: {admin}")
# Admin: alice
Relative File Paths¶
File paths are relative to the configuration file, not your current directory:
project/
├── config/
│ ├── app.yaml
│ └── database.yaml
└── secrets/
└── db-password.txt
The paths database.yaml and ../secrets/db-password.txt are resolved relative to config/app.yaml, not where you run your program.
RFC 8089 File URI Syntax¶
HoloConf supports RFC 8089 file: URI syntax for explicit absolute paths. This is useful when you want to be explicit about absolute paths:
# RFC 8089 file: URIs (all equivalent for absolute paths)
system_config: ${file:///etc/myapp/config.yaml}
explicit_local: ${file://localhost/etc/myapp/config.yaml}
minimal_form: ${file:/etc/myapp/config.yaml}
All three forms reference the same absolute path /etc/myapp/config.yaml. The resolver normalizes them to the appropriate path format.
Remote file URIs are rejected:
# This will error - remote file URIs not supported
remote_file: ${file://server.example.com/path/to/file}
HoloConf only supports local file access for security reasons. Remote file URIs (with a non-localhost hostname) are rejected with a clear error message.
When to Use RFC 8089 Syntax
Use plain paths for most cases: ${file:./config.yaml} or ${file:/etc/app/config.yaml}.
Use RFC 8089 syntax when: - Interoperating with systems that use file: URIs - You want to be explicit about the path being absolute - Your configuration documents the URI format for clarity
Handling Missing Files¶
What if the file doesn't exist?
You can provide a default instead:
HTTP/HTTPS: Remote Configuration¶
For centralized configuration management, you can fetch values from HTTP endpoints. This is useful for:
- Fetching config from a configuration server
- Loading shared defaults from a central location
- Integrating with REST APIs
HoloConf provides separate http and https resolvers that auto-prepend the appropriate protocol, making your configs cleaner:
# Clean syntax - resolver auto-prepends https://
feature_flags: ${https:config.example.com/flags.json}
# Also works with http
api_config: ${http:api.internal/config.json}
But wait - HTTP fetching is disabled by default for security. You need to explicitly enable it:
Let's enable HTTP fetching:
Security: HTTP Disabled by Default
HTTP fetching is disabled by default because:
- It can leak sensitive configuration paths to network logs
- It introduces network dependencies during config loading
- It may expose your infrastructure to SSRF attacks
Only enable it when you specifically need remote configuration.
Handling Network Errors¶
What happens if the HTTP request fails?
You can provide a fallback:
Now if the HTTP request fails, it uses the empty object instead of erroring.
Custom Request Timeouts¶
The default timeout is 30 seconds. For slower endpoints, increase it:
Working with Internal Certificate Authorities¶
Many organizations use internal certificate authorities for HTTPS services. Let's see how to configure HoloConf to trust these.
First, let's try fetching from an internal service:
The error occurs because your organization's CA isn't in the default trust store. We can fix this by providing your CA certificate:
This replaces the default CA bundle with your custom one. But what if you need to trust BOTH public CAs (for external APIs) AND your internal CA? Use http_extra_ca_bundle instead:
This adds your CA to the existing trust store, so you can fetch from both internal and external HTTPS endpoints.
Using Certificate Variables¶
Instead of storing certificates as files, you can load them from environment variables or other resolvers. This is useful for:
- Containerized environments where secrets are injected as environment variables
- Secret management systems that provide certificates dynamically
- CI/CD pipelines where certificates are stored in secure vaults
PEM Certificates from Environment Variables¶
Let's load client certificates for mTLS from environment variables:
# config.yaml
secure_api:
data: ${https:api.corp.com/config,client_cert=${env:CLIENT_CERT_PEM},client_key=${env:CLIENT_KEY_PEM}}
import os
from holoconf import Config
# Set certificates in environment (in practice, these come from your secret management system)
os.environ["CLIENT_CERT_PEM"] = """-----BEGIN CERTIFICATE-----
MIICijCCAXICCQC1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
...
-----END CERTIFICATE-----"""
os.environ["CLIENT_KEY_PEM"] = """-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAK1234567890
...
-----END PRIVATE KEY-----"""
config = Config.load("config.yaml", allow_http=True)
data = config.secure_api.data # Uses certificates from environment
CA Bundle from File Resolver¶
You can also load CA bundles using the file resolver with parse=text:
# config.yaml
internal_api:
data: ${https:internal.corp.com/config,ca_bundle=${file:./certs/ca-bundle.pem,parse=text}}
This reads the CA bundle file and passes its PEM content directly to the HTTPS resolver.
P12/PFX Binary Certificates (Python)¶
For P12/PFX certificates (which contain both certificate and key), use parse=binary:
# config.yaml
secure_data:
value: ${https:secure.example.com/data,client_cert=${file:./certs/identity.p12,parse=binary},key_password=${env:P12_PASSWORD}}
Auto-Detection: HoloConf automatically detects whether you're providing:
- A file path (string without -----BEGIN marker)
- PEM content (string containing -----BEGIN CERTIFICATE----- or -----BEGIN PRIVATE KEY-----)
- P12 binary (bytes type in Python, detected by .p12/.pfx extension for paths)
This means you can mix and match:
# Mixed mode: cert from environment, key from file path
api_config: ${https:api.example.com/config,client_cert=${env:CERT_PEM},client_key=/etc/ssl/private/key.pem}
Authentication Headers¶
Some configuration endpoints require authentication. Add custom headers:
use holoconf::{Config, ConfigOptions};
use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), "Bearer YOUR_TOKEN_HERE".to_string());
let options = ConfigOptions::default()
.allow_http(true)
.http_headers(headers);
let config = Config::load_with_options("config.yaml", options)?;
Caching HTTP Responses
HTTP values are fetched every time you access them. For frequently accessed values:
This avoids repeated network requests.
Quick Reference¶
Here's a summary of all core resolvers:
| Resolver | Syntax | Description | Example |
|---|---|---|---|
env |
${env:VAR} |
Environment variable | ${env:DB_HOST,default=localhost} |
| Self-reference | ${path} |
Absolute reference | ${defaults.timeout} |
| Self-reference | ${.key} |
Sibling reference | ${.host} |
| Self-reference | ${..parent.key} |
Parent's sibling | ${..shared.timeout} |
file |
${file:path} |
File content or RFC 8089 URI | ${file:secrets/key.pem} |
http |
${http:url} |
HTTP GET (auto-prepends http://) | ${http:api.example.com/config} |
https |
${https:url} |
HTTPS GET (auto-prepends https://) | ${https:api.example.com/config} |
All resolvers support:
default=value- Fallback if resolver failssensitive=true- Mark value for redaction
What You've Learned¶
You now understand:
- Using
${env:VAR}to read environment variables - Providing defaults with
default=value - Referencing other config values with absolute and relative paths
- Preventing circular references
- Including file content with
${file:path} - Fetching remote config with
${http:url}and${https:url} - Configuring HTTP timeouts, CA bundles, and auth headers
- Security implications of HTTP resolvers
Next Steps¶
- AWS Resolvers - Integrate with AWS SSM, CloudFormation, and S3
- Custom Resolvers - Write your own resolvers for custom data sources
See Also¶
- ADR-002 Resolver Architecture - Technical design
- FEAT-002 Core Resolvers - Full specification