Welcome to Brokkr
Brokkr is a control plane for Kubernetes that lets you dynamically create and manage applications across clusters. Define what you need, fire it off, and the controller loop takes care of the rest — your applications get created, configured, and reconciled automatically.
Use Cases
On-Demand Application Provisioning
A customer needs a new service spun up? A new tenant needs their own stack? Just create the deployment through Brokkr and it flows through the controller loop to your clusters. No manual kubectl, no waiting on CI pipelines — your infrastructure adapts to your needs in real time.
Dynamic Service Management
As your requirements change, Brokkr lets you define, reconfigure, and scale the services running across your clusters. Generators can programmatically create deployment objects, templates let you stamp out standardized configurations, and the agent reconciliation loop keeps everything in the desired state.
Multi-Cluster Orchestration
Manage applications across multiple Kubernetes clusters from a single control plane. Target specific clusters with labels, push updates to all of them at once, and let each agent independently reconcile its own state. Brokkr handles the coordination so you can focus on what to deploy, not where and how.
Explore Brokkr
- Getting Started — Install, configure, and get Brokkr running
- How-To Guides — Practical guides for common tasks
- Explanation — Architecture, concepts, and design decisions
- Reference — API reference and technical details
What Makes Brokkr Different?
While tools like FluxCD and ArgoCD excel at GitOps-based state management, Brokkr takes a different approach — it’s built for dynamic, on-demand application lifecycle management rather than static manifest synchronization.
Programmatic Resource Creation
Brokkr’s generators and templates let external systems programmatically create Kubernetes resources through an API. CI/CD pipelines, customer provisioning systems, or internal tools can fire off deployments without touching git repos or manifest files.
Controller Loop Reconciliation
Every agent runs its own reconciliation loop, continuously pulling its target state from the broker and applying it to its cluster. Resources drift? The agent corrects it. New deployment object pushed? The agent picks it up on the next poll.
Built for Dynamic Workloads
Where GitOps tools work best with a known, static set of manifests, Brokkr is designed for environments where the set of applications changes frequently — multi-tenant platforms, on-demand infrastructure, and systems where what needs to run is determined at runtime, not at commit time.
Getting Started with Brokkr
Welcome to Brokkr! This section will guide you through the process of installing and setting up Brokkr for your environment.
Prerequisites
Before you begin, ensure you have:
- Kubernetes cluster access
kubectlinstalled and configured- Rust toolchain (for building from source)
- Docker (for container deployments)
Quick Navigation
- Installation - Install Brokkr on your system
- Quick Start - Get up and running quickly
- Configuration - Configure Brokkr for your environment
What’s Next?
After completing the getting started guide, you can:
- Follow our tutorials for hands-on learning
- Check out the how-to guides for specific tasks
- Dive into the reference documentation for detailed information
Installing Brokkr
This guide will help you install Brokkr using Helm, the recommended installation method.
Prerequisites
Before installing Brokkr, ensure you have:
- Kubernetes cluster (v1.20 or later)
- kubectl CLI configured to access your cluster
- Helm 3.8 or later installed (installation guide)
Verifying Prerequisites
# Check Kubernetes version
kubectl version --short
# Check Helm version
helm version --short
# Verify cluster access
kubectl cluster-info
Quick Start
Get Brokkr up and running in under 10 minutes with a broker and agent in your Kubernetes cluster.
1. Install the Broker
Install the broker with bundled PostgreSQL for development:
# Install broker with bundled PostgreSQL
helm install brokkr-broker oci://ghcr.io/colliery-io/charts/brokkr-broker \
--set postgresql.enabled=true \
--wait
# Verify broker is running
kubectl get pods -l app.kubernetes.io/name=brokkr-broker
Expected output:
NAME READY STATUS RESTARTS AGE
brokkr-broker-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
2. Get Broker URL
# Port forward to access the broker locally
kubectl port-forward svc/brokkr-broker 3000:3000 &
# The broker is now accessible at http://localhost:3000
3. Create an Agent and Get PAK
Create an agent registration and retrieve its Prefixed API Key (PAK):
# Create a new agent
curl -X POST http://localhost:3000/api/v1/agents \
-H "Content-Type: application/json" \
-d '{
"name": "my-agent",
"cluster_name": "development"
}'
The response will include the agent’s PAK:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "my-agent",
"cluster_name": "development",
"status": "ACTIVE",
"pak": "brokkr_BRxxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
}
Save the pak value - you’ll need it to install the agent.
4. Install the Agent
Install the agent using the PAK from step 3:
# Install agent (replace <PAK> with the actual PAK from step 3)
helm install brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--set broker.url=http://brokkr-broker:3000 \
--set broker.pak="<PAK>" \
--wait
# Verify agent is running
kubectl get pods -l app.kubernetes.io/name=brokkr-agent
Expected output:
NAME READY STATUS RESTARTS AGE
brokkr-agent-xxxxxxxxxxx-xxxxx 1/1 Running 0 1m
5. Verify Installation
Check that both components are healthy:
# Check broker health
kubectl exec deploy/brokkr-broker -- wget -qO- http://localhost:3000/healthz
# Check agent health
kubectl exec deploy/brokkr-agent -- wget -qO- http://localhost:8080/healthz
# View agent registration in broker logs
kubectl logs deploy/brokkr-broker | grep -i agent
You should see “OK” from both health checks and agent registration messages in the broker logs.
Detailed Installation
Broker Installation
The broker is the central management service that coordinates deployments across your Kubernetes clusters.
Development Setup (Bundled PostgreSQL)
For development and testing, use the bundled PostgreSQL:
helm install brokkr-broker oci://ghcr.io/colliery-io/charts/brokkr-broker \
--set postgresql.enabled=true \
--set postgresql.auth.password=brokkr \
--wait
Using Provided Values Files
Brokkr includes pre-configured values files for different environments:
# Development (bundled PostgreSQL, minimal resources)
helm install brokkr-broker oci://ghcr.io/colliery-io/charts/brokkr-broker \
-f https://raw.githubusercontent.com/colliery-io/brokkr/main/charts/brokkr-broker/values/development.yaml
# Staging (external PostgreSQL, moderate resources)
helm install brokkr-broker oci://ghcr.io/colliery-io/charts/brokkr-broker \
-f https://raw.githubusercontent.com/colliery-io/brokkr/main/charts/brokkr-broker/values/staging.yaml
# Production (external PostgreSQL, production-grade resources)
helm install brokkr-broker oci://ghcr.io/colliery-io/charts/brokkr-broker \
-f https://raw.githubusercontent.com/colliery-io/brokkr/main/charts/brokkr-broker/values/production.yaml
You can also download these files and customize them:
# Download development values
curl -O https://raw.githubusercontent.com/colliery-io/brokkr/main/charts/brokkr-broker/values/development.yaml
# Edit as needed
vi development.yaml
# Install with custom values
helm install brokkr-broker oci://ghcr.io/colliery-io/charts/brokkr-broker \
-f development.yaml
View all available values files:
Agent Installation
The agent runs in each Kubernetes cluster you want to manage and communicates with the broker.
Basic Agent Installation
# Create agent via broker API (see Quick Start step 3)
# Then install with the returned PAK:
helm install brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--set broker.url=http://brokkr-broker:3000 \
--set broker.pak="<PAK_FROM_BROKER>" \
--wait
Using Provided Values Files
Brokkr includes pre-configured values files for agents:
# Development (minimal resources, cluster-wide RBAC)
helm install brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--set broker.url=http://brokkr-broker:3000 \
--set broker.pak="<PAK>" \
-f https://raw.githubusercontent.com/colliery-io/brokkr/main/charts/brokkr-agent/values/development.yaml
# Staging (moderate resources)
helm install brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--set broker.url=http://brokkr-broker:3000 \
--set broker.pak="<PAK>" \
-f https://raw.githubusercontent.com/colliery-io/brokkr/main/charts/brokkr-agent/values/staging.yaml
# Production (production-grade resources)
helm install brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--set broker.url=http://brokkr-broker:3000 \
--set broker.pak="<PAK>" \
-f https://raw.githubusercontent.com/colliery-io/brokkr/main/charts/brokkr-agent/values/production.yaml
View all available agent values files:
Chart Versions
Brokkr Helm charts are published to GitHub Container Registry (GHCR).
Installing Specific Versions
# Install a specific release version
helm install brokkr-broker oci://ghcr.io/colliery-io/charts/brokkr-broker \
--version 1.0.0 \
--set postgresql.enabled=true
# List available versions
# Visit: https://github.com/orgs/colliery-io/packages/container/package/charts%2Fbrokkr-broker
Development Builds
Development builds are available for testing:
# Development builds use semver pre-release format with timestamps
# Example: 0.0.0-develop.20251021150606
# Find the latest development build at:
# https://github.com/orgs/colliery-io/packages/container/package/charts%2Fbrokkr-broker
# Install development build (replace timestamp with actual version)
helm install brokkr-broker oci://ghcr.io/colliery-io/charts/brokkr-broker \
--version 0.0.0-develop.20251021150606 \
--set postgresql.enabled=true
Upgrading Brokkr
Upgrade your Brokkr installation to a newer version:
# Upgrade broker
helm upgrade brokkr-broker oci://ghcr.io/colliery-io/charts/brokkr-broker \
--version 1.1.0 \
--reuse-values
# Upgrade agent
helm upgrade brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--version 1.1.0 \
--reuse-values
Uninstalling Brokkr
Remove Brokkr from your cluster:
# Uninstall agent
helm uninstall brokkr-agent
# Uninstall broker (this will also remove bundled PostgreSQL if enabled)
helm uninstall brokkr-broker
# Note: PersistentVolumes may remain - delete manually if needed
kubectl get pv
kubectl delete pv <pv-name>
Verifying the Installation
Health Checks
# Broker health endpoint
kubectl exec deploy/brokkr-broker -- wget -qO- http://localhost:3000/healthz
# Agent health endpoint
kubectl exec deploy/brokkr-agent -- wget -qO- http://localhost:8080/healthz
Both should return “OK”.
Connectivity Tests
# Check agent registration in broker
kubectl logs deploy/brokkr-broker | grep "agent registered"
# Check agent connection to broker
kubectl logs deploy/brokkr-agent | grep "connected to broker"
# List registered agents via API
kubectl port-forward svc/brokkr-broker 3000:3000 &
curl http://localhost:3000/api/v1/agents
Test Deployment
Create a test namespace to verify end-to-end functionality:
# Port forward to broker
kubectl port-forward svc/brokkr-broker 3000:3000 &
# Create a stack
STACK_ID=$(curl -s -X POST http://localhost:3000/api/v1/stacks \
-H "Content-Type: application/json" \
-d '{"name": "test-stack", "description": "Test stack"}' \
| jq -r '.id')
# Deploy a test namespace
curl -X POST http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects \
-H "Content-Type: application/json" \
-d '{
"yaml_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: brokkr-test",
"is_deletion_marker": false
}'
# Verify namespace was created
kubectl get namespace brokkr-test
# Clean up
curl -X POST http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects \
-H "Content-Type: application/json" \
-d '{
"yaml_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: brokkr-test",
"is_deletion_marker": true
}'
kubectl get namespace brokkr-test # Should show Terminating/NotFound
Configuration Reference
Broker Values
Key configuration options for the broker chart:
| Parameter | Description | Default |
|---|---|---|
postgresql.enabled | Enable bundled PostgreSQL | true |
postgresql.auth.password | PostgreSQL password (bundled) | brokkr |
postgresql.external.host | External database host | "" |
postgresql.external.port | External database port | 5432 |
postgresql.external.database | Database name | brokkr |
postgresql.external.username | Database username | brokkr |
postgresql.external.password | Database password | brokkr |
postgresql.external.schema | PostgreSQL schema (multi-tenant) | "" |
replicaCount | Number of broker replicas | 1 |
image.tag | Image tag to use | latest |
broker.logLevel | Log level | info |
resources.limits.cpu | CPU limit | 500m |
resources.limits.memory | Memory limit | 512Mi |
tls.enabled | Enable TLS | false |
Agent Values
Key configuration options for the agent chart:
| Parameter | Description | Default |
|---|---|---|
broker.url | Broker URL | Required |
broker.pak | Agent PAK (Prefixed API Key) | Required |
broker.agentName | Human-readable agent name | "" |
broker.clusterName | Name of the managed cluster | "" |
agent.pollingInterval | Seconds between broker polls | 30 |
agent.deploymentHealth.enabled | Enable deployment health checks | true |
agent.deploymentHealth.intervalSeconds | Health check interval | 60 |
rbac.create | Create RBAC resources | true |
rbac.clusterWide | Cluster-wide RBAC (vs namespaced) | true |
rbac.secretAccess.enabled | Enable secret access | false |
resources.limits.cpu | CPU limit | 200m |
resources.limits.memory | Memory limit | 256Mi |
image.tag | Image tag to use | latest |
For complete configuration options, see the chart values files:
Next Steps
- Follow our Quick Start Guide to deploy your first application
- Learn about Basic Concepts in Brokkr
- Explore Configuration Guide
Troubleshooting
Common Issues
Broker pod not starting:
# Check pod status
kubectl describe pod -l app.kubernetes.io/name=brokkr-broker
# Check logs
kubectl logs -l app.kubernetes.io/name=brokkr-broker
Agent not connecting to broker:
# Verify broker URL is accessible from agent
kubectl exec deploy/brokkr-agent -- wget -qO- http://brokkr-broker:3000/healthz
# Check agent logs for connection errors
kubectl logs -l app.kubernetes.io/name=brokkr-agent
Database connection errors:
# Check PostgreSQL is running
kubectl get pods -l app.kubernetes.io/name=postgresql
# Check database credentials
kubectl get secret brokkr-broker-postgresql -o yaml
PAK authentication failures:
- Verify the PAK is correct and not expired
- Check that the agent name matches the registration
- Ensure the broker URL is accessible
Getting Help
- Check our GitHub Issues
- Check our GitHub Issues for known issues and solutions
Building from Source
For contributors or advanced users who want to build Brokkr from source:
Prerequisites
- Rust toolchain (1.8+)
- PostgreSQL database (v12+)
- Kubernetes cluster
- Docker (for building images)
Build Instructions
# Clone the repository
git clone https://github.com/colliery-io/brokkr.git
cd brokkr
# Build using Cargo
cargo build --release
# The binaries will be available in target/release/
# - brokkr-broker: The central management service
# - brokkr-agent: The Kubernetes cluster agent
Running Locally
# Set up database
export BROKKR__DATABASE__URL="postgres://brokkr:brokkr@localhost:5432/brokkr"
# Run broker
./target/release/brokkr-broker serve
# Run agent (in another terminal)
export BROKKR__AGENT__PAK="<your-pak>"
export BROKKR__AGENT__BROKER_URL="http://localhost:3000"
./target/release/brokkr-agent start
Development Environment
For active development:
# Install Angreal (development task runner)
pip install angreal
# Start the development environment
angreal local up
# Rebuild specific services
angreal local rebuild broker
angreal local rebuild agent
For more details on contributing, see the project README.
Quick Start Guide
This guide walks through deploying your first application using Brokkr. By the end, you will understand the core deployment workflow: creating stacks, associating them with agents, deploying Kubernetes resources, and cleaning up when you’re done.
Prerequisites
Before starting, ensure you have completed the Installation Guide with both the broker and at least one agent running. You will need the admin PAK (Prefixed API Key) from the broker setup and kubectl configured to access your target cluster. The examples below assume the broker is accessible at http://localhost:3000 via port-forward.
Understanding the Deployment Model
Brokkr organizes deployments around three interconnected concepts. A stack represents a logical grouping of Kubernetes resources that belong together—an application, a service, or any collection of related resources. An agent runs in a Kubernetes cluster and handles the actual application of resources. Targeting connects stacks to agents, telling Brokkr which agents should receive which stacks.
The deployment flow works as follows: when you create a deployment object in a stack, Brokkr stores it in the broker’s database. Agents that target that stack poll the broker on a regular interval and retrieve pending deployment objects. Each agent then applies the resources to its cluster using Kubernetes server-side apply, ensuring resources are created or updated to match the desired state.
Step 1: Create a Stack
Stacks serve as containers for related Kubernetes resources. Every deployment in Brokkr belongs to a stack, so you’ll start by creating one for your quick-start application.
# Set your admin PAK for convenience
export ADMIN_PAK="brokkr_BR..." # Replace with your actual PAK
# Create a stack
curl -s -X POST http://localhost:3000/api/v1/stacks \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_PAK" \
-d '{
"name": "quick-start-app",
"description": "My first Brokkr deployment"
}' | jq .
The response includes a stack ID that you’ll use in subsequent commands:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "quick-start-app",
"description": "My first Brokkr deployment",
"created_at": "2024-01-15T10:30:00Z"
}
# Save the stack ID for later use
export STACK_ID="550e8400-e29b-41d4-a716-446655440000" # Use your actual ID
Step 2: Target the Stack to Your Agent
Agents only receive deployment objects from stacks they’re targeting. This targeting relationship must be created explicitly, giving you control over which agents manage which resources.
First, find your agent’s ID by listing registered agents:
# List agents to find the ID
curl -s http://localhost:3000/api/v1/agents \
-H "Authorization: Bearer $ADMIN_PAK" | jq '.[].id'
Then create the targeting relationship that connects your agent to the stack:
# Save the agent ID
export AGENT_ID="your-agent-id-here"
# Create the targeting relationship
curl -s -X POST http://localhost:3000/api/v1/agents/$AGENT_ID/targets \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_PAK" \
-d "{
\"agent_id\": \"$AGENT_ID\",
\"stack_id\": \"$STACK_ID\"
}" | jq .
With the targeting in place, the agent will poll for deployment objects from this stack on its next polling cycle.
Step 3: Deploy the Application
Now you’ll create a deployment object containing Kubernetes resources. This example deploys a simple application with a namespace, a ConfigMap for configuration, and a Deployment that reads from that ConfigMap.
Create a YAML file with the resources:
cat > quick-start.yaml << 'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: quick-start
---
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: quick-start
data:
message: "Hello from Brokkr!"
environment: development
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: quick-start-app
namespace: quick-start
spec:
replicas: 1
selector:
matchLabels:
app: quick-start-app
template:
metadata:
labels:
app: quick-start-app
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "while true; do echo $MESSAGE; sleep 30; done"]
env:
- name: MESSAGE
valueFrom:
configMapKeyRef:
name: app-config
key: message
EOF
Submit this YAML to Brokkr as a deployment object. The jq command properly encodes the YAML content for the JSON request:
curl -s -X POST "http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_PAK" \
-d "$(jq -n --arg yaml "$(cat quick-start.yaml)" '{yaml_content: $yaml, is_deletion_marker: false}')" | jq .
The broker stores this deployment object and marks it pending. On the agent’s next polling cycle (default: every 30 seconds), it retrieves the object and applies the resources to the cluster.
Step 4: Verify the Deployment
After waiting a few seconds for the agent to poll and apply the resources, verify they were created correctly in your cluster:
# Check the namespace was created
kubectl get namespace quick-start
# Verify the ConfigMap contains your data
kubectl get configmap app-config -n quick-start -o yaml
# Check the Deployment and its pods
kubectl get deployment quick-start-app -n quick-start
kubectl get pods -n quick-start
# View the application logs to confirm it's working
kubectl logs -n quick-start -l app=quick-start-app
You should see the pod running and logging “Hello from Brokkr!” every 30 seconds.
You can also check the deployment status through the Brokkr API to see how the broker tracked this deployment:
# View deployment objects in the stack
curl -s "http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects" \
-H "Authorization: Bearer $ADMIN_PAK" | jq '.[] | {id, sequence_id, status, created_at}'
Step 5: Update the Application
Updating resources in Brokkr works by submitting a new deployment object with the complete desired state. Brokkr uses full-state deployments rather than partial updates—each deployment object represents all the resources that should exist.
Create an updated version of the application with more replicas and a changed message:
cat > quick-start-updated.yaml << 'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: quick-start
---
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: quick-start
data:
message: "Updated: Brokkr is working!"
environment: production
debug: "false"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: quick-start-app
namespace: quick-start
spec:
replicas: 2
selector:
matchLabels:
app: quick-start-app
template:
metadata:
labels:
app: quick-start-app
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "while true; do echo $MESSAGE; sleep 30; done"]
env:
- name: MESSAGE
valueFrom:
configMapKeyRef:
name: app-config
key: message
EOF
Deploy the updated resources through Brokkr:
curl -s -X POST "http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_PAK" \
-d "$(jq -n --arg yaml "$(cat quick-start-updated.yaml)" '{yaml_content: $yaml, is_deletion_marker: false}')" | jq .
After the agent polls and applies the update, verify the changes took effect:
# Check that replicas scaled to 2
kubectl get pods -n quick-start
# Verify the ConfigMap was updated
kubectl get configmap app-config -n quick-start -o jsonpath='{.data.message}'
Step 6: Clean Up
To delete resources through Brokkr, submit a deployment object with is_deletion_marker: true. This signals the agent to remove the specified resources rather than apply them.
curl -s -X POST "http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_PAK" \
-d "$(jq -n --arg yaml "$(cat quick-start-updated.yaml)" '{yaml_content: $yaml, is_deletion_marker: true}')" | jq .
The YAML content is required even for deletions so the agent knows exactly which resources to remove. After the agent processes this deletion marker, verify the resources are gone:
# The namespace should be terminating or gone
kubectl get namespace quick-start
Finally, clean up the Brokkr resources themselves if you no longer need them:
# Delete the targeting relationship
curl -s -X DELETE "http://localhost:3000/api/v1/agents/$AGENT_ID/targets/$STACK_ID" \
-H "Authorization: Bearer $ADMIN_PAK"
# Delete the stack
curl -s -X DELETE "http://localhost:3000/api/v1/stacks/$STACK_ID" \
-H "Authorization: Bearer $ADMIN_PAK"
Next Steps
You have now completed the core Brokkr workflow: creating stacks, targeting them to agents, deploying resources, updating them, and cleaning up. This pattern—declarative state stored in the broker, pulled and applied by agents—scales to managing deployments across many clusters.
From here, explore more advanced capabilities:
- Read about Core Concepts to understand Brokkr’s design philosophy
- Learn about the Data Model to understand how entities relate
- Explore the Configuration Guide for production deployment options
- Check the Work Orders reference for container builds and other transient operations
Configuration Guide
Brokkr uses a layered configuration system that allows settings to be defined through default values, configuration files, and environment variables. This guide provides a comprehensive reference for all configuration options and explains how to configure both the broker and agent components.
Configuration Sources
Configuration values are loaded from multiple sources, with later sources taking precedence over earlier ones:
- Default values embedded in the application from
default.toml - Configuration file (optional) specified at startup
- Environment variables prefixed with
BROKKR__
This layering enables a flexible deployment model where defaults work out of the box, configuration files provide environment-specific settings, and environment variables allow runtime overrides without modifying files.
Environment Variable Naming
All environment variables use the BROKKR__ prefix with double underscores (__) as separators for nested configuration. The naming convention converts configuration paths to uppercase with underscores.
For example, the configuration path broker.webhook_delivery_interval_seconds becomes the environment variable BROKKR__BROKER__WEBHOOK_DELIVERY_INTERVAL_SECONDS.
Database Configuration
The database configuration controls the connection to PostgreSQL.
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__DATABASE__URL | string | postgres://brokkr:brokkr@localhost:5433/brokkr | PostgreSQL connection URL |
BROKKR__DATABASE__SCHEMA | string | None | Schema name for multi-tenant isolation |
The schema setting enables multi-tenant deployments where each tenant’s data is isolated in a separate PostgreSQL schema. When configured, all queries automatically set search_path to the specified schema.
# Standard single-tenant configuration
BROKKR__DATABASE__URL=postgres://user:password@db.example.com:5432/brokkr
# Multi-tenant configuration with schema isolation
BROKKR__DATABASE__URL=postgres://user:password@db.example.com:5432/brokkr
BROKKR__DATABASE__SCHEMA=tenant_acme
Logging Configuration
Logging settings control the verbosity and format of application logs.
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__LOG__LEVEL | string | debug | Log level: trace, debug, info, warn, error |
BROKKR__LOG__FORMAT | string | text | Log format: text (human-readable) or json (structured) |
The log level is hot-reloadable—changes take effect without restarting the application. Use json format in production environments for easier log aggregation and parsing.
# Production logging configuration
BROKKR__LOG__LEVEL=info
BROKKR__LOG__FORMAT=json
Broker Configuration
The broker configuration controls the central management service’s behavior.
Core Settings
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__BROKER__PAK_HASH | string | None | Pre-computed PAK hash for admin authentication |
Webhook Settings
Webhooks deliver event notifications to external systems. These settings control the delivery worker’s behavior.
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__BROKER__WEBHOOK_ENCRYPTION_KEY | string | Random | 64-character hex string for AES-256 encryption |
BROKKR__BROKER__WEBHOOK_DELIVERY_INTERVAL_SECONDS | integer | 5 | Polling interval for pending webhook deliveries |
BROKKR__BROKER__WEBHOOK_DELIVERY_BATCH_SIZE | integer | 50 | Maximum deliveries processed per batch |
BROKKR__BROKER__WEBHOOK_CLEANUP_RETENTION_DAYS | integer | 7 | Days to retain completed webhook deliveries |
The encryption key protects webhook URLs and authentication headers at rest. If not configured, the broker generates a random key at startup and logs a warning. This means encrypted data will become unreadable if the broker restarts. For production deployments, always configure an explicit encryption key.
# Production webhook configuration
BROKKR__BROKER__WEBHOOK_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
BROKKR__BROKER__WEBHOOK_DELIVERY_INTERVAL_SECONDS=5
BROKKR__BROKER__WEBHOOK_DELIVERY_BATCH_SIZE=100
BROKKR__BROKER__WEBHOOK_CLEANUP_RETENTION_DAYS=30
Diagnostic Settings
Diagnostics are temporary operations that agents execute for debugging purposes.
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__BROKER__DIAGNOSTIC_CLEANUP_INTERVAL_SECONDS | integer | 900 | Cleanup task interval (15 minutes) |
BROKKR__BROKER__DIAGNOSTIC_MAX_AGE_HOURS | integer | 1 | Maximum age for diagnostic results |
Audit Log Settings
Audit logs record all significant actions for security and compliance.
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__BROKER__AUDIT_LOG_RETENTION_DAYS | integer | 90 | Days to retain audit log entries |
# Extended audit retention for compliance
BROKKR__BROKER__AUDIT_LOG_RETENTION_DAYS=365
Auth Cache Settings
The broker caches PAK authentication results in memory to reduce database queries per request. Each authenticated request would otherwise require 2-3 database lookups (admin, agent, generator tables). The cache is automatically invalidated when PAKs are rotated or entities are deleted.
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__BROKER__AUTH_CACHE_TTL_SECONDS | integer | 60 | TTL for cached auth results (0 to disable) |
# Increase cache TTL for high-throughput deployments
BROKKR__BROKER__AUTH_CACHE_TTL_SECONDS=120
# Disable auth caching entirely
BROKKR__BROKER__AUTH_CACHE_TTL_SECONDS=0
CORS Configuration
Cross-Origin Resource Sharing (CORS) settings control which origins can access the broker API.
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__CORS__ALLOWED_ORIGINS | list | ["http://localhost:3001"] | Allowed origins (use * for all) |
BROKKR__CORS__ALLOWED_METHODS | list | ["GET", "POST", "PUT", "DELETE", "OPTIONS"] | Allowed HTTP methods |
BROKKR__CORS__ALLOWED_HEADERS | list | ["Content-Type", "Authorization"] | Allowed request headers |
BROKKR__CORS__MAX_AGE_SECONDS | integer | 3600 | Preflight cache duration |
CORS settings are hot-reloadable. In production, restrict allowed_origins to specific domains rather than using *.
Agent Configuration
The agent configuration controls the Kubernetes cluster agent’s behavior.
Required Settings
| Variable | Type | Required | Description |
|---|---|---|---|
BROKKR__AGENT__BROKER_URL | string | Yes | Broker API URL |
BROKKR__AGENT__PAK | string | Yes | Prefixed API Key for broker communication |
BROKKR__AGENT__AGENT_NAME | string | Yes | Human-readable agent name |
BROKKR__AGENT__CLUSTER_NAME | string | Yes | Name of the managed Kubernetes cluster |
Polling Settings
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__AGENT__POLLING_INTERVAL | integer | 10 | Seconds between broker polls |
BROKKR__AGENT__MAX_RETRIES | integer | 60 | Maximum operation retry attempts |
BROKKR__AGENT__MAX_EVENT_MESSAGE_RETRIES | integer | 2 | Maximum event reporting retry attempts |
BROKKR__AGENT__EVENT_MESSAGE_RETRY_DELAY | integer | 5 | Seconds between event retry attempts |
Health and Monitoring
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__AGENT__HEALTH_PORT | integer | 8080 | HTTP port for health endpoints |
BROKKR__AGENT__DEPLOYMENT_HEALTH_ENABLED | boolean | true | Enable deployment health monitoring |
BROKKR__AGENT__DEPLOYMENT_HEALTH_INTERVAL | integer | 60 | Seconds between health checks |
Kubernetes Settings
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__AGENT__KUBECONFIG_PATH | string | None | Path to kubeconfig file (uses in-cluster config if not set) |
# Complete agent configuration
BROKKR__AGENT__BROKER_URL=https://broker.example.com:3000
BROKKR__AGENT__PAK=brokkr_BRabc123_xyzSecretTokenHere
BROKKR__AGENT__AGENT_NAME=production-east
BROKKR__AGENT__CLUSTER_NAME=prod-us-east-1
BROKKR__AGENT__POLLING_INTERVAL=10
BROKKR__AGENT__HEALTH_PORT=8080
BROKKR__AGENT__DEPLOYMENT_HEALTH_ENABLED=true
BROKKR__AGENT__DEPLOYMENT_HEALTH_INTERVAL=60
Telemetry Configuration
Telemetry settings control OpenTelemetry trace export for distributed tracing.
Base Settings
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__TELEMETRY__ENABLED | boolean | false | Enable telemetry export |
BROKKR__TELEMETRY__OTLP_ENDPOINT | string | http://localhost:4317 | OTLP collector endpoint (gRPC) |
BROKKR__TELEMETRY__SERVICE_NAME | string | brokkr | Service name for traces |
BROKKR__TELEMETRY__SAMPLING_RATE | float | 0.1 | Sampling rate (0.0 to 1.0) |
Component Overrides
The broker and agent can have independent telemetry configurations that override the base settings.
| Variable | Description |
|---|---|
BROKKR__TELEMETRY__BROKER__ENABLED | Override enabled for broker |
BROKKR__TELEMETRY__BROKER__OTLP_ENDPOINT | Override endpoint for broker |
BROKKR__TELEMETRY__BROKER__SERVICE_NAME | Override service name for broker |
BROKKR__TELEMETRY__BROKER__SAMPLING_RATE | Override sampling rate for broker |
BROKKR__TELEMETRY__AGENT__ENABLED | Override enabled for agent |
BROKKR__TELEMETRY__AGENT__OTLP_ENDPOINT | Override endpoint for agent |
BROKKR__TELEMETRY__AGENT__SERVICE_NAME | Override service name for agent |
BROKKR__TELEMETRY__AGENT__SAMPLING_RATE | Override sampling rate for agent |
# Enable telemetry with different sampling for broker and agent
BROKKR__TELEMETRY__ENABLED=true
BROKKR__TELEMETRY__OTLP_ENDPOINT=http://otel-collector:4317
BROKKR__TELEMETRY__SERVICE_NAME=brokkr
BROKKR__TELEMETRY__SAMPLING_RATE=0.1
BROKKR__TELEMETRY__BROKER__SERVICE_NAME=brokkr-broker
BROKKR__TELEMETRY__BROKER__SAMPLING_RATE=0.5
BROKKR__TELEMETRY__AGENT__SERVICE_NAME=brokkr-agent
BROKKR__TELEMETRY__AGENT__SAMPLING_RATE=0.1
PAK Configuration
Prefixed API Key (PAK) settings control token generation characteristics.
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__PAK__PREFIX | string | brokkr | PAK string prefix |
BROKKR__PAK__SHORT_TOKEN_PREFIX | string | BR | Short token prefix |
BROKKR__PAK__SHORT_TOKEN_LENGTH | integer | 8 | Short token character count |
BROKKR__PAK__LONG_TOKEN_LENGTH | integer | 24 | Long token character count |
BROKKR__PAK__RNG | string | osrng | Random number generator type |
BROKKR__PAK__DIGEST | integer | 8 | Digest algorithm identifier |
These settings are typically left at their defaults. Changing them affects only newly generated PAKs—existing PAKs remain valid.
Configuration File Format
Configuration files use TOML format. All settings can be specified in a configuration file as an alternative to environment variables.
[database]
url = "postgres://user:password@localhost:5432/brokkr"
schema = "tenant_acme"
[log]
level = "info"
format = "json"
[broker]
webhook_encryption_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
webhook_delivery_interval_seconds = 5
webhook_delivery_batch_size = 50
webhook_cleanup_retention_days = 7
diagnostic_cleanup_interval_seconds = 900
diagnostic_max_age_hours = 1
audit_log_retention_days = 90
auth_cache_ttl_seconds = 60
[cors]
allowed_origins = ["https://admin.example.com"]
allowed_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allowed_headers = ["Content-Type", "Authorization"]
max_age_seconds = 3600
[agent]
broker_url = "https://broker.example.com:3000"
pak = "brokkr_BRabc123_xyzSecretTokenHere"
agent_name = "production-east"
cluster_name = "prod-us-east-1"
polling_interval = 10
max_retries = 60
health_port = 8080
deployment_health_enabled = true
deployment_health_interval = 60
[telemetry]
enabled = true
otlp_endpoint = "http://otel-collector:4317"
service_name = "brokkr"
sampling_rate = 0.1
[telemetry.broker]
service_name = "brokkr-broker"
sampling_rate = 0.5
[telemetry.agent]
service_name = "brokkr-agent"
sampling_rate = 0.1
[pak]
prefix = "brokkr"
short_token_prefix = "BR"
short_token_length = 8
long_token_length = 24
Hot-Reload Configuration
The broker supports dynamic configuration reloading for certain settings without requiring a restart.
Hot-Reloadable Settings
These settings can be changed at runtime:
log.level- Log verbositycors.allowed_origins- CORS originscors.max_age_seconds- CORS preflight cachebroker.diagnostic_cleanup_interval_seconds- Diagnostic cleanup intervalbroker.diagnostic_max_age_hours- Diagnostic retentionbroker.webhook_delivery_interval_seconds- Webhook delivery intervalbroker.webhook_delivery_batch_size- Webhook batch sizebroker.webhook_cleanup_retention_days- Webhook retention
Static Settings (Require Restart)
These settings require an application restart to change:
database.url- Database connectiondatabase.schema- Database schemabroker.webhook_encryption_key- Encryption keybroker.pak_hash- Admin PAK hashbroker.auth_cache_ttl_seconds- Auth cache TTLtelemetry.*- All telemetry settingspak.*- All PAK generation settings
Triggering a Reload
Reload configuration via the admin API:
curl -X POST https://broker.example.com/api/v1/admin/config/reload \
-H "Authorization: Bearer $ADMIN_PAK"
When a configuration file is specified via BROKKR_CONFIG_FILE, the broker automatically watches it for filesystem changes with a 5-second debounce period.
Troubleshooting
Common Configuration Issues
Database connection failures typically indicate incorrect credentials or network issues. Verify the database URL is correct, the database server is running, and network connectivity exists between the broker and database.
# Test database connectivity
psql "postgres://user:password@localhost:5432/brokkr" -c "SELECT 1"
Agent authentication failures usually result from an invalid PAK. Verify the PAK was copied correctly without extra whitespace and that the agent record hasn’t been deleted from the broker.
Kubernetes access issues in agents may indicate missing or invalid credentials. When running outside a cluster, ensure BROKKR__AGENT__KUBECONFIG_PATH points to a valid kubeconfig file. When running inside a cluster, verify the service account has appropriate RBAC permissions.
Debugging Configuration
Enable trace-level logging to see configuration loading details:
BROKKR__LOG__LEVEL=trace brokkr-broker
The broker logs configuration values at startup (with sensitive values redacted), making it easy to verify which settings were applied.
Getting Help
If you encounter configuration issues:
- Check the logs for detailed error messages
- Verify all required configuration values are set
- Test connectivity to external dependencies (database, Kubernetes API)
- Consult the GitHub Issues for known issues
Tutorials
Step-by-step tutorials for learning Brokkr. Each tutorial walks you through a complete workflow from start to finish, building practical skills along the way.
These tutorials assume you have a working Brokkr development environment. If you haven’t set one up yet, follow the Installation Guide first.
| Tutorial | What You’ll Learn |
|---|---|
| Deploy Your First Application | Create a stack, add a deployment object, register an agent, and watch Kubernetes resources get applied |
| Multi-Cluster Targeting | Use labels and annotations to direct deployments to specific agents |
| CI/CD with Generators | Create a generator and use it from a CI/CD pipeline to push deployments |
| Standardized Deployments with Templates | Create reusable templates with JSON Schema validation and instantiate them across stacks |
Tutorial: Deploy Your First Application
In this tutorial, you’ll deploy an nginx web server to a Kubernetes cluster through Brokkr. You’ll learn the core workflow: creating a stack, adding Kubernetes manifests as deployment objects, and watching an agent apply them to a cluster.
What you’ll learn:
- How stacks organize Kubernetes resources
- How deployment objects carry YAML manifests
- How agents poll for and apply resources
- How to verify deployments succeeded
Prerequisites:
- A running Brokkr development environment (see Installation Guide —
angreal local upstarts the full stack) - The admin PAK (Pre-Authentication Key) printed during first startup (check broker logs if you missed it)
curlandjqinstalled
Step 1: Verify the Environment
First, confirm the broker is running and healthy:
curl -s http://localhost:3000/healthz
You should see:
OK
Check that at least one agent is registered:
curl -s http://localhost:3000/api/v1/agents \
-H "Authorization: <your-admin-pak>" | jq '.[].name'
You should see the default agent name (typically "DEFAULT"). Note the agent’s id field — you’ll need it later.
Tip: Throughout this tutorial, replace
<your-admin-pak>with the actual admin PAK from your broker startup logs. It looks likebrokkr_BR3rVsDa_GK3QN7CDUzYc6iKgMkJ98M2WSimM5t6U8.
Step 2: Create a Stack
A stack is a named container that groups related Kubernetes resources. Think of it as a logical application — everything needed to run your service lives inside one stack.
Stacks are always owned by a generator (the entity that manages deployments, typically a CI/CD pipeline). As an admin, you can create a stack on behalf of any generator. For this tutorial, we’ll use a nil generator ID to indicate the stack is admin-managed:
STACK_ID=$(curl -s -X POST http://localhost:3000/api/v1/stacks \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{
"name": "tutorial-nginx",
"description": "Tutorial: a simple nginx deployment",
"generator_id": "00000000-0000-0000-0000-000000000000"
}' | jq -r '.id')
echo "Stack ID: $STACK_ID"
The response contains the new stack with its ID. The generator_id field ties the stack to its owning generator — we’ll explore generators in a later tutorial.
Step 3: Target the Agent to the Stack
Agents don’t automatically receive every stack’s resources. You need to explicitly target an agent to a stack, telling it “you are responsible for deploying this stack’s resources.”
First, get the agent ID:
AGENT_ID=$(curl -s http://localhost:3000/api/v1/agents \
-H "Authorization: <your-admin-pak>" | jq -r '.[0].id')
echo "Agent ID: $AGENT_ID"
Now target the agent to your stack:
curl -s -X POST "http://localhost:3000/api/v1/agents/${AGENT_ID}/targets" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d "{\"stack_id\": \"${STACK_ID}\"}" | jq .
The agent will now receive deployment objects from this stack on its next poll cycle.
Step 4: Create a Deployment Object
A deployment object contains the actual Kubernetes YAML that the agent will apply to its cluster. You can include multiple Kubernetes resources in a single deployment object using multi-document YAML (separated by ---).
Create a deployment object with an nginx namespace, deployment, and service:
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{
"yaml_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: tutorial-nginx\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: nginx\n namespace: tutorial-nginx\n labels:\n app: nginx\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: nginx\n template:\n metadata:\n labels:\n app: nginx\n spec:\n containers:\n - name: nginx\n image: nginx:1.27\n ports:\n - containerPort: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: nginx\n namespace: tutorial-nginx\nspec:\n selector:\n app: nginx\n ports:\n - port: 80\n targetPort: 80"
}' | jq .
The response includes a sequence_id — an auto-incrementing number that orders deployment objects within a stack. The agent uses this to know which version is latest.
Step 5: Watch the Agent Apply Resources
The agent polls the broker at its configured interval (default: 10 seconds). Within a few seconds, you should see the resources appear in your Kubernetes cluster.
Check the agent events to confirm the deployment was applied:
curl -s "http://localhost:3000/api/v1/agents/${AGENT_ID}/events" \
-H "Authorization: <your-admin-pak>" | jq '.[] | {event_type, status, message, created_at}'
You should see a SUCCESS event:
{
"event_type": "DEPLOYMENT",
"status": "SUCCESS",
"message": "Successfully applied deployment object",
"created_at": "2025-01-15T10:01:30Z"
}
If you have kubectl configured to talk to your development cluster (k3s), verify the resources directly:
kubectl get all -n tutorial-nginx
Expected output:
NAME READY STATUS RESTARTS AGE
pod/nginx-7c5ddbdf54-abc12 1/1 Running 0 30s
pod/nginx-7c5ddbdf54-def34 1/1 Running 0 30s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginx ClusterIP 10.43.120.50 <none> 80/TCP 30s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nginx 2/2 2 2 30s
Step 6: Update the Deployment
To update a deployment, create a new deployment object in the same stack. The agent detects the new sequence_id and applies the updated manifests, reconciling the cluster state.
Scale nginx to 3 replicas:
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{
"yaml_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: tutorial-nginx\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: nginx\n namespace: tutorial-nginx\n labels:\n app: nginx\nspec:\n replicas: 3\n selector:\n matchLabels:\n app: nginx\n template:\n metadata:\n labels:\n app: nginx\n spec:\n containers:\n - name: nginx\n image: nginx:1.27\n ports:\n - containerPort: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: nginx\n namespace: tutorial-nginx\nspec:\n selector:\n app: nginx\n ports:\n - port: 80\n targetPort: 80"
}' | jq .
After the next poll cycle, verify the update:
kubectl get deployment nginx -n tutorial-nginx
You should see 3/3 in the READY column.
Step 7: Clean Up
To remove the deployed resources, create a deletion marker — a special deployment object with is_deletion_marker: true. This tells the agent to delete all resources previously applied for this stack from the cluster (not just the resources listed in the YAML content):
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{
"yaml_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: tutorial-nginx",
"is_deletion_marker": true
}' | jq .
The agent will remove the Kubernetes resources on its next poll. Verify:
kubectl get namespace tutorial-nginx
After a few seconds, the namespace and all its contents will be gone.
Optionally, remove the agent target and delete the stack:
# Remove the target
curl -s -X DELETE "http://localhost:3000/api/v1/agents/${AGENT_ID}/targets/${STACK_ID}" \
-H "Authorization: <your-admin-pak>"
# Delete the stack (soft delete — marks as deleted but preserves the record)
curl -s -X DELETE "http://localhost:3000/api/v1/stacks/${STACK_ID}" \
-H "Authorization: <your-admin-pak>"
Note: Deletion in Brokkr is a “soft delete” — the record is marked with a
deleted_attimestamp but not removed from the database. See Soft Deletion for details.
What You’ve Learned
- Stacks group related Kubernetes resources under a single name
- Deployment objects carry the YAML manifests inside a stack
- Agent targets connect agents to stacks, controlling which clusters receive which resources
- Sequence IDs let the agent know when a newer version is available
- Deletion markers trigger resource cleanup on the cluster
- Agents use a pull-based model — they poll the broker, so clusters behind firewalls work without inbound connections
Next Steps
- Multi-Cluster Targeting — direct deployments to specific clusters using labels
- CI/CD with Generators — automate deployment pushes from a CI pipeline
- Managing Stacks — deeper guide on stack lifecycle management
- Configuration Guide — tune polling intervals, database settings, and more
Tutorial: Multi-Cluster Targeting
In this tutorial, you’ll deploy different configurations to different clusters using Brokkr’s label and annotation targeting system. You’ll register two agents representing two environments (staging and production), then direct deployments to each one selectively.
What you’ll learn:
- How labels categorize agents and stacks
- How annotations attach key-value metadata
- How targeting rules match deployments to specific agents
- How one stack can reach multiple clusters while another targets only one
Prerequisites:
- A running Brokkr development environment (
angreal local up) - Your admin PAK
- Completed the Deploy Your First Application tutorial
Step 1: Create Two Agents
In a real deployment, each agent runs in a different Kubernetes cluster. For this tutorial, we’ll create two agent records in the broker to simulate a multi-cluster setup.
# Create a staging agent
brokkr-broker create agent --name staging-agent --cluster_name staging-cluster
Note the PAK printed for the staging agent. Then create the production agent:
# Create a production agent
brokkr-broker create agent --name prod-agent --cluster_name prod-cluster
If you’re using the API instead of the CLI:
STAGING=$(curl -s -X POST http://localhost:3000/api/v1/agents \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{"name": "staging-agent", "cluster_name": "staging-cluster"}' | jq -r '.agent.id')
PROD=$(curl -s -X POST http://localhost:3000/api/v1/agents \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{"name": "prod-agent", "cluster_name": "prod-cluster"}' | jq -r '.agent.id')
echo "Staging agent: $STAGING"
echo "Production agent: $PROD"
Step 2: Label the Agents
Labels are simple tags that categorize agents (e.g., env:staging, tier:web). Annotations are key-value metadata pairs (e.g., region=us-east-1). Both are used for organizing and filtering agents. Labels are typically used for broad categories, while annotations carry specific configuration values.
Add environment labels to each agent:
# Label the staging agent
curl -s -X POST "http://localhost:3000/api/v1/agents/${STAGING}/labels" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '"env:staging"' | jq .
# Label the production agent
curl -s -X POST "http://localhost:3000/api/v1/agents/${PROD}/labels" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '"env:production"' | jq .
Add a region annotation to the production agent:
curl -s -X POST "http://localhost:3000/api/v1/agents/${PROD}/annotations" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{"key": "region", "value": "us-east-1"}' | jq .
Verify the labels are set:
curl -s "http://localhost:3000/api/v1/agents/${STAGING}/labels" \
-H "Authorization: <your-admin-pak>" | jq '.[].label'
curl -s "http://localhost:3000/api/v1/agents/${PROD}/labels" \
-H "Authorization: <your-admin-pak>" | jq '.[].label'
Step 3: Create Stacks with Matching Labels
Now create two stacks — one for staging, one for production — and label them to match the agents.
Important: Labels on agents and stacks are metadata for organization and filtering. The actual connection between an agent and a stack is created by agent targets (Step 4). Labels help you categorize resources; targets tell the agent “deploy this stack’s resources.”
# Create staging stack
STAGING_STACK=$(curl -s -X POST http://localhost:3000/api/v1/stacks \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{"name": "myapp-staging", "description": "My app - staging environment", "generator_id": "00000000-0000-0000-0000-000000000000"}' \
| jq -r '.id')
# Add label to staging stack
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STAGING_STACK}/labels" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '"env:staging"' | jq .
# Create production stack
PROD_STACK=$(curl -s -X POST http://localhost:3000/api/v1/stacks \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{"name": "myapp-production", "description": "My app - production environment", "generator_id": "00000000-0000-0000-0000-000000000000"}' \
| jq -r '.id')
# Add label to production stack
curl -s -X POST "http://localhost:3000/api/v1/stacks/${PROD_STACK}/labels" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '"env:production"' | jq .
Step 4: Target Agents to Stacks
Connect each agent to its corresponding stack:
# Staging agent targets staging stack
curl -s -X POST "http://localhost:3000/api/v1/agents/${STAGING}/targets" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d "{\"stack_id\": \"${STAGING_STACK}\"}" | jq .
# Production agent targets production stack
curl -s -X POST "http://localhost:3000/api/v1/agents/${PROD}/targets" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d "{\"stack_id\": \"${PROD_STACK}\"}" | jq .
Step 5: Deploy to Staging Only
Push a deployment to the staging stack. Only the staging agent will receive it:
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STAGING_STACK}/deployment-objects" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{
"yaml_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: myapp-staging\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: myapp\n namespace: myapp-staging\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: myapp\n template:\n metadata:\n labels:\n app: myapp\n spec:\n containers:\n - name: myapp\n image: nginx:1.27\n env:\n - name: ENVIRONMENT\n value: staging"
}' | jq .
Check agent events — only the staging agent should have a deployment event:
# Staging agent should have an event
curl -s "http://localhost:3000/api/v1/agents/${STAGING}/events" \
-H "Authorization: <your-admin-pak>" | jq 'length'
# Production agent should have no new events
curl -s "http://localhost:3000/api/v1/agents/${PROD}/events" \
-H "Authorization: <your-admin-pak>" | jq 'length'
Step 6: Deploy to Production
Now push to production with a different replica count:
curl -s -X POST "http://localhost:3000/api/v1/stacks/${PROD_STACK}/deployment-objects" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{
"yaml_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: myapp-production\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: myapp\n namespace: myapp-production\nspec:\n replicas: 3\n selector:\n matchLabels:\n app: myapp\n template:\n metadata:\n labels:\n app: myapp\n spec:\n containers:\n - name: myapp\n image: nginx:1.27\n env:\n - name: ENVIRONMENT\n value: production"
}' | jq .
The production agent receives 3 replicas while staging has 1 — each environment gets exactly what it needs.
Step 7: Create a Shared Stack
What if you have infrastructure (like monitoring) that should go to both environments? Create a stack that both agents target:
# Create shared stack
SHARED_STACK=$(curl -s -X POST http://localhost:3000/api/v1/stacks \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{"name": "monitoring-shared", "description": "Monitoring stack for all clusters", "generator_id": "00000000-0000-0000-0000-000000000000"}' \
| jq -r '.id')
# Both agents target the shared stack
curl -s -X POST "http://localhost:3000/api/v1/agents/${STAGING}/targets" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d "{\"stack_id\": \"${SHARED_STACK}\"}" | jq .
curl -s -X POST "http://localhost:3000/api/v1/agents/${PROD}/targets" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d "{\"stack_id\": \"${SHARED_STACK}\"}" | jq .
Now any deployment object pushed to monitoring-shared will be applied by both agents.
Step 8: Verify Targeting
Review each agent’s full target list:
echo "=== Staging Agent Targets ==="
curl -s "http://localhost:3000/api/v1/agents/${STAGING}/targets" \
-H "Authorization: <your-admin-pak>" | jq '.[].stack_id'
echo "=== Production Agent Targets ==="
curl -s "http://localhost:3000/api/v1/agents/${PROD}/targets" \
-H "Authorization: <your-admin-pak>" | jq '.[].stack_id'
Staging should show two stacks (its own + shared), and production should also show two (its own + shared).
Clean Up
Remove the test resources:
# Delete stacks (soft delete)
curl -s -X DELETE "http://localhost:3000/api/v1/stacks/${STAGING_STACK}" \
-H "Authorization: <your-admin-pak>"
curl -s -X DELETE "http://localhost:3000/api/v1/stacks/${PROD_STACK}" \
-H "Authorization: <your-admin-pak>"
curl -s -X DELETE "http://localhost:3000/api/v1/stacks/${SHARED_STACK}" \
-H "Authorization: <your-admin-pak>"
# Delete agents (soft delete)
curl -s -X DELETE "http://localhost:3000/api/v1/agents/${STAGING}" \
-H "Authorization: <your-admin-pak>"
curl -s -X DELETE "http://localhost:3000/api/v1/agents/${PROD}" \
-H "Authorization: <your-admin-pak>"
What You’ve Learned
- Labels categorize agents and stacks with simple tags like
env:staging - Annotations attach key-value metadata like
region=us-east-1 - Agent targets create explicit bindings between agents and stacks
- An agent only receives deployment objects from stacks it targets
- Multiple agents can target the same stack for shared infrastructure
- One agent can target multiple stacks for layered deployments
Next Steps
- CI/CD with Generators — automate deployments from pipelines
- Standardized Deployments with Templates — use templates to reduce YAML duplication across environments
- Core Concepts — deeper understanding of the targeting model
Tutorial: CI/CD with Generators
In this tutorial, you’ll set up a generator — Brokkr’s mechanism for CI/CD integration — and use it to push deployments from a simulated pipeline. Generators are non-admin identities with scoped permissions, designed for automation.
What you’ll learn:
- What generators are and why they exist
- How to create a generator and manage its PAK
- How generators create and manage stacks
- How to push deployment objects from a CI/CD pipeline
- Access control differences between admin and generator roles
Prerequisites:
- A running Brokkr development environment (
angreal local up) - Your admin PAK
- Completed the Deploy Your First Application tutorial
Step 1: Create a Generator
Generators represent automated systems (CI/CD pipelines, GitOps controllers, deployment scripts) that push resources to Brokkr. They have their own PAK and can only manage resources they own.
Create a generator using the CLI:
brokkr-broker create generator --name "github-actions" --description "GitHub Actions deployment pipeline"
This prints the generator’s ID and PAK:
Generator created successfully:
ID: f8e7d6c5-b4a3-...
Name: github-actions
Initial PAK: brokkr_BRx9y2Kq_A1B2C3D4E5F6G7H8I9J0K1L2
Save this PAK immediately — it’s only shown once. You’ll store it as a CI secret.
Alternatively, use the API:
GENERATOR_RESPONSE=$(curl -s -X POST http://localhost:3000/api/v1/generators \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{"name": "github-actions", "description": "GitHub Actions deployment pipeline"}')
GENERATOR_ID=$(echo "$GENERATOR_RESPONSE" | jq -r '.generator.id')
GENERATOR_PAK=$(echo "$GENERATOR_RESPONSE" | jq -r '.pak')
echo "Generator ID: $GENERATOR_ID"
echo "Generator PAK: $GENERATOR_PAK"
Step 2: Create a Stack as the Generator
Switch to using the generator’s PAK. The generator can create stacks, and those stacks become “owned” by the generator.
GENERATOR_PAK="brokkr_BRx9y2Kq_A1B2C3D4E5F6G7H8I9J0K1L2" # your actual PAK
STACK_ID=$(curl -s -X POST http://localhost:3000/api/v1/stacks \
-H "Authorization: ${GENERATOR_PAK}" \
-H "Content-Type: application/json" \
-d '{"name": "myapp-v2", "description": "My application deployed via CI/CD"}' \
| jq -r '.id')
echo "Stack ID: $STACK_ID"
The stack’s generator_id field is now set to your generator’s ID. This ownership means:
- The generator can update and delete this stack
- The generator can push deployment objects to this stack
- Other generators cannot modify this stack
- Admins can still manage any stack
Step 3: See Generator Permissions in Action
Generators have scoped access — they can only manage resources they own. Let’s see this in practice:
# Generator can list its own stacks
curl -s http://localhost:3000/api/v1/stacks \
-H "Authorization: ${GENERATOR_PAK}" | jq '.[].name'
# Generator CANNOT list agents (admin-only)
curl -s http://localhost:3000/api/v1/agents \
-H "Authorization: ${GENERATOR_PAK}"
# Returns: 403 Forbidden
The key rule: generators can create, update, and delete their own stacks and push deployment objects to them, but they cannot manage agents, targets, or other generators’ resources. See the Security Model for the complete access control matrix.
Step 4: Target an Agent (So Deployments Reach a Cluster)
Before pushing deployment objects, an agent must be targeted to the stack. Otherwise the deployment exists in the broker but no agent will apply it. As an admin, target the default agent:
AGENT_ID=$(curl -s http://localhost:3000/api/v1/agents \
-H "Authorization: <your-admin-pak>" | jq -r '.[0].id')
curl -s -X POST "http://localhost:3000/api/v1/agents/${AGENT_ID}/targets" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d "{\"stack_id\": \"${STACK_ID}\"}" | jq .
Note: Generators cannot manage agents or targets — that requires admin access. In production, an admin sets up the targeting once and the generator just pushes deployments.
Step 5: Push a Deployment (Simulating CI/CD)
Now simulate what a CI/CD pipeline would do — push a deployment object after building an image:
# This is what your CI/CD pipeline would run after building
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \
-H "Authorization: ${GENERATOR_PAK}" \
-H "Content-Type: application/json" \
-d '{
"yaml_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: myapp\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: myapp\n namespace: myapp\n labels:\n app: myapp\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: myapp\n template:\n metadata:\n labels:\n app: myapp\n spec:\n containers:\n - name: myapp\n image: myregistry.example.com/myapp:v1.2.3\n ports:\n - containerPort: 8080\n env:\n - name: VERSION\n value: v1.2.3"
}' | jq '{id, sequence_id, yaml_checksum}'
Each push creates a new deployment object with an incrementing sequence_id. The agent sees the new sequence and applies the latest version.
Step 6: Simulate a Deployment Update
Push a new version (as a CI/CD pipeline would on the next merge):
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \
-H "Authorization: ${GENERATOR_PAK}" \
-H "Content-Type: application/json" \
-d '{
"yaml_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: myapp\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: myapp\n namespace: myapp\n labels:\n app: myapp\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: myapp\n template:\n metadata:\n labels:\n app: myapp\n spec:\n containers:\n - name: myapp\n image: myregistry.example.com/myapp:v1.3.0\n ports:\n - containerPort: 8080\n env:\n - name: VERSION\n value: v1.3.0"
}' | jq '{id, sequence_id, yaml_checksum}'
Notice the sequence_id incremented. The agent will apply this new version.
Step 7: A Real GitHub Actions Workflow
Here’s how you’d integrate Brokkr into a real GitHub Actions pipeline:
# .github/workflows/deploy.yml
name: Deploy to Brokkr
on:
push:
branches: [main]
env:
BROKKR_URL: https://brokkr.example.com
STACK_ID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push image
run: |
docker build -t myregistry.example.com/myapp:${{ github.sha }} .
docker push myregistry.example.com/myapp:${{ github.sha }}
- name: Generate deployment YAML
run: |
cat > deployment.yaml << 'YAML'
apiVersion: v1
kind: Namespace
metadata:
name: myapp
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: myapp
spec:
replicas: 2
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myregistry.example.com/myapp:${{ github.sha }}
ports:
- containerPort: 8080
YAML
- name: Push to Brokkr
run: |
YAML_CONTENT=$(cat deployment.yaml | jq -Rs .)
curl -sf -X POST "${BROKKR_URL}/api/v1/stacks/${STACK_ID}/deployment-objects" \
-H "Authorization: ${{ secrets.BROKKR_GENERATOR_PAK }}" \
-H "Content-Type: application/json" \
-d "{\"yaml_content\": ${YAML_CONTENT}}"
Store the generator PAK as BROKKR_GENERATOR_PAK in your repository’s GitHub Actions secrets.
Step 8: Rotate the Generator PAK
PAKs should be rotated periodically. You can rotate via CLI or API:
# Via CLI (requires admin access to the broker host)
brokkr-broker rotate generator --uuid <generator-uuid>
# Via API
curl -s -X POST "http://localhost:3000/api/v1/generators/${GENERATOR_ID}/rotate-pak" \
-H "Authorization: ${GENERATOR_PAK}" | jq .
The response contains the new PAK. Update your CI secrets immediately — the old PAK stops working.
Clean Up
# Delete the stack (as generator)
curl -s -X DELETE "http://localhost:3000/api/v1/stacks/${STACK_ID}" \
-H "Authorization: ${GENERATOR_PAK}"
# Delete the generator (requires admin)
curl -s -X DELETE "http://localhost:3000/api/v1/generators/${GENERATOR_ID}" \
-H "Authorization: <your-admin-pak>"
What You’ve Learned
- Generators are scoped identities for CI/CD pipeline integration
- Each generator gets its own PAK for authentication
- Generators own the stacks they create — other generators can’t modify them
- Pushing deployment objects is as simple as a
curlPOST with YAML content - Sequence IDs ensure agents always apply the latest version
- Generator PAKs should be stored as CI secrets and rotated periodically
Next Steps
- Standardized Deployments with Templates — reduce YAML duplication with templates
- Working with Generators — detailed generator management guide
- Generators Reference — complete API reference
- Security Model — understand the full authorization model
Tutorial: Standardized Deployments with Templates
In this tutorial, you’ll create a reusable deployment template with parameterized values and JSON Schema validation, then instantiate it across multiple stacks. Templates eliminate YAML duplication and enforce consistency.
What you’ll learn:
- How to create a template with Tera syntax
- How to define a parameter schema using JSON Schema
- How to instantiate a template into a stack
- How template versioning works
- How template targeting restricts which stacks can use a template
Prerequisites:
- A running Brokkr development environment (
angreal local up) - Your admin PAK
- Completed the Deploy Your First Application tutorial
Step 1: Understand the Template Concept
A Brokkr template has two parts:
- Template content — Kubernetes YAML with Tera placeholders (e.g.,
{{ replicas }},{{ image_tag }}) - Parameters schema — a JSON Schema that defines which parameters exist, their types, defaults, and constraints
When you instantiate a template into a stack, you provide parameter values. Brokkr validates them against the schema, renders the Tera template, and creates a deployment object with the resulting YAML.
Step 2: Create a Template
Create a template for a standard web service deployment:
curl -s -X POST http://localhost:3000/api/v1/templates \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{
"name": "web-service",
"description": "Standard web service with configurable replicas, image, and resource limits",
"template_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: {{ namespace }}\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: {{ service_name }}\n namespace: {{ namespace }}\n labels:\n app: {{ service_name }}\n managed-by: brokkr\nspec:\n replicas: {{ replicas }}\n selector:\n matchLabels:\n app: {{ service_name }}\n template:\n metadata:\n labels:\n app: {{ service_name }}\n spec:\n containers:\n - name: {{ service_name }}\n image: {{ image_repository }}:{{ image_tag }}\n ports:\n - containerPort: {{ container_port }}\n resources:\n requests:\n cpu: {{ cpu_request }}\n memory: {{ memory_request }}\n limits:\n cpu: {{ cpu_limit }}\n memory: {{ memory_limit }}\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: {{ service_name }}\n namespace: {{ namespace }}\nspec:\n selector:\n app: {{ service_name }}\n ports:\n - port: 80\n targetPort: {{ container_port }}",
"parameters_schema": "{\"type\": \"object\", \"required\": [\"service_name\", \"namespace\", \"image_repository\", \"image_tag\"], \"properties\": {\"service_name\": {\"type\": \"string\", \"description\": \"Name of the service\", \"minLength\": 1, \"maxLength\": 63}, \"namespace\": {\"type\": \"string\", \"description\": \"Kubernetes namespace\", \"minLength\": 1}, \"image_repository\": {\"type\": \"string\", \"description\": \"Container image repository\"}, \"image_tag\": {\"type\": \"string\", \"description\": \"Container image tag\", \"default\": \"latest\"}, \"replicas\": {\"type\": \"integer\", \"description\": \"Number of replicas\", \"default\": 2, \"minimum\": 1, \"maximum\": 20}, \"container_port\": {\"type\": \"integer\", \"description\": \"Container port\", \"default\": 8080}, \"cpu_request\": {\"type\": \"string\", \"description\": \"CPU request\", \"default\": \"100m\"}, \"memory_request\": {\"type\": \"string\", \"description\": \"Memory request\", \"default\": \"128Mi\"}, \"cpu_limit\": {\"type\": \"string\", \"description\": \"CPU limit\", \"default\": \"500m\"}, \"memory_limit\": {\"type\": \"string\", \"description\": \"Memory limit\", \"default\": \"256Mi\"}}}"
}' | jq '{id, name, version, checksum}'
The response shows the template was created with version: 1:
{
"id": "t1234567-...",
"name": "web-service",
"version": 1,
"checksum": "abc123..."
}
Save the template ID:
TEMPLATE_ID="t1234567-..." # use the actual ID from the response
Step 3: Understand the Parameters Schema
The JSON Schema you provided defines 10 parameters: four required (service_name, namespace, image_repository, image_tag) and six optional with defaults (replicas defaults to 2, container_port to 8080, etc.). The schema enforces constraints — for example, replicas must be between 1 and 20, and service_name must be 1-63 characters.
The schema ensures that callers provide the required values and that constraints are enforced at instantiation time, before any YAML is rendered. See the Templates Reference for the full JSON Schema syntax guide.
Step 4: Create a Stack and Instantiate the Template
Create a stack, then instantiate the template into it:
# Create the stack
STACK_ID=$(curl -s -X POST http://localhost:3000/api/v1/stacks \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{"name": "frontend-app", "description": "Frontend web application", "generator_id": "00000000-0000-0000-0000-000000000000"}' \
| jq -r '.id')
# Instantiate the template
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects/from-template" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d "{
\"template_id\": \"${TEMPLATE_ID}\",
\"parameters\": {
\"service_name\": \"frontend\",
\"namespace\": \"frontend-app\",
\"image_repository\": \"myregistry.example.com/frontend\",
\"image_tag\": \"v2.1.0\",
\"replicas\": 3,
\"container_port\": 3000,
\"memory_limit\": \"512Mi\"
}
}" | jq '.[0] | {id, sequence_id, yaml_checksum}'
Brokkr validated the parameters, rendered the Tera template, and created a deployment object. The resulting YAML has all placeholders replaced with actual values.
Step 5: Verify the Rendered Output
Fetch the deployment object to see the rendered YAML:
DO_ID=$(curl -s "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \
-H "Authorization: <your-admin-pak>" | jq -r '.[0].id')
curl -s "http://localhost:3000/api/v1/deployment-objects/${DO_ID}" \
-H "Authorization: <your-admin-pak>" | jq -r '.yaml_content'
You’ll see fully-rendered Kubernetes YAML with all template variables replaced:
apiVersion: v1
kind: Namespace
metadata:
name: frontend-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: frontend-app
labels:
app: frontend
managed-by: brokkr
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: myregistry.example.com/frontend:v2.1.0
ports:
- containerPort: 3000
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: frontend
namespace: frontend-app
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 3000
Step 6: Re-use the Template for Another Service
The same template works for a different service by changing the parameters:
# Create a backend stack
BACKEND_STACK=$(curl -s -X POST http://localhost:3000/api/v1/stacks \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{"name": "backend-api", "description": "Backend API service", "generator_id": "00000000-0000-0000-0000-000000000000"}' \
| jq -r '.id')
# Instantiate with different parameters
curl -s -X POST "http://localhost:3000/api/v1/stacks/${BACKEND_STACK}/deployment-objects/from-template" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d "{
\"template_id\": \"${TEMPLATE_ID}\",
\"parameters\": {
\"service_name\": \"api\",
\"namespace\": \"backend-api\",
\"image_repository\": \"myregistry.example.com/api\",
\"image_tag\": \"v3.0.1\",
\"replicas\": 5,
\"container_port\": 8080,
\"cpu_limit\": \"1000m\",
\"memory_limit\": \"1Gi\"
}
}" | jq '.[0] | {id, sequence_id}'
One template, multiple services, each with appropriate configuration.
Step 7: Update the Template (Versioning)
Templates are versioned. Updating a template creates a new version while preserving the old one:
curl -s -X PUT "http://localhost:3000/api/v1/templates/${TEMPLATE_ID}" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d '{
"description": "Standard web service v2 - adds liveness probe",
"template_content": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: {{ namespace }}\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: {{ service_name }}\n namespace: {{ namespace }}\n labels:\n app: {{ service_name }}\n managed-by: brokkr\nspec:\n replicas: {{ replicas }}\n selector:\n matchLabels:\n app: {{ service_name }}\n template:\n metadata:\n labels:\n app: {{ service_name }}\n spec:\n containers:\n - name: {{ service_name }}\n image: {{ image_repository }}:{{ image_tag }}\n ports:\n - containerPort: {{ container_port }}\n livenessProbe:\n httpGet:\n path: /healthz\n port: {{ container_port }}\n initialDelaySeconds: 10\n periodSeconds: 30\n resources:\n requests:\n cpu: {{ cpu_request }}\n memory: {{ memory_request }}\n limits:\n cpu: {{ cpu_limit }}\n memory: {{ memory_limit }}\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: {{ service_name }}\n namespace: {{ namespace }}\nspec:\n selector:\n app: {{ service_name }}\n ports:\n - port: 80\n targetPort: {{ container_port }}",
"parameters_schema": "{\"type\": \"object\", \"required\": [\"service_name\", \"namespace\", \"image_repository\", \"image_tag\"], \"properties\": {\"service_name\": {\"type\": \"string\", \"minLength\": 1, \"maxLength\": 63}, \"namespace\": {\"type\": \"string\", \"minLength\": 1}, \"image_repository\": {\"type\": \"string\"}, \"image_tag\": {\"type\": \"string\", \"default\": \"latest\"}, \"replicas\": {\"type\": \"integer\", \"default\": 2, \"minimum\": 1, \"maximum\": 20}, \"container_port\": {\"type\": \"integer\", \"default\": 8080}, \"cpu_request\": {\"type\": \"string\", \"default\": \"100m\"}, \"memory_request\": {\"type\": \"string\", \"default\": \"128Mi\"}, \"cpu_limit\": {\"type\": \"string\", \"default\": \"500m\"}, \"memory_limit\": {\"type\": \"string\", \"default\": \"256Mi\"}}}"
}' | jq '{id, name, version}'
The response shows version: 2. Existing deployment objects rendered from version 1 are unaffected. New instantiations will use the latest version.
Step 8: Schema Validation in Action
Try instantiating with invalid parameters to see validation:
# Missing required field (service_name)
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects/from-template" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d "{
\"template_id\": \"${TEMPLATE_ID}\",
\"parameters\": {
\"namespace\": \"test\",
\"image_repository\": \"nginx\",
\"image_tag\": \"latest\"
}
}" | jq .
# Replicas out of range (max is 20)
curl -s -X POST "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects/from-template" \
-H "Authorization: <your-admin-pak>" \
-H "Content-Type: application/json" \
-d "{
\"template_id\": \"${TEMPLATE_ID}\",
\"parameters\": {
\"service_name\": \"test\",
\"namespace\": \"test\",
\"image_repository\": \"nginx\",
\"image_tag\": \"latest\",
\"replicas\": 100
}
}" | jq .
Both requests return validation errors, preventing invalid YAML from reaching your clusters.
Clean Up
curl -s -X DELETE "http://localhost:3000/api/v1/stacks/${STACK_ID}" \
-H "Authorization: <your-admin-pak>"
curl -s -X DELETE "http://localhost:3000/api/v1/stacks/${BACKEND_STACK}" \
-H "Authorization: <your-admin-pak>"
curl -s -X DELETE "http://localhost:3000/api/v1/templates/${TEMPLATE_ID}" \
-H "Authorization: <your-admin-pak>"
What You’ve Learned
- Templates combine Tera-syntax YAML with JSON Schema parameter validation
- Instantiation validates parameters, renders the template, and creates a deployment object
- Versioning preserves old template versions while allowing updates
- JSON Schema enforces types, required fields, ranges, and string constraints
- Templates reduce duplication — one template serves many stacks with different parameters
For the complete Tera template syntax (conditionals, loops, filters) and JSON Schema reference, see the Templates Reference.
Next Steps
- Using Stack Templates — detailed how-to guide for template workflows
- Templates Reference — complete API reference for templates
- Core Concepts — how templates fit into the Brokkr architecture
How-To Guides
Practical guides for accomplishing specific tasks with Brokkr. These are goal-oriented — each guide walks you through a particular operation or workflow.
Container Builds with Shipwright
Brokkr integrates with Shipwright Build to provide native container image building capabilities. This guide covers installation, configuration, and usage of Shipwright builds through Brokkr’s work order system.
Overview
Shipwright Build is a CNCF Sandbox project that provides a framework for building container images on Kubernetes. Brokkr uses Shipwright as the execution engine for build work orders, allowing you to:
- Build container images from Git repositories
- Push images to container registries
- Leverage production-ready build strategies (buildah, kaniko)
- Manage builds through Brokkr’s work order API
Prerequisites
Kubernetes Version
Shipwright integration requires Kubernetes 1.29 or later due to dependencies on newer API features.
# Verify your Kubernetes version
kubectl version --short
Cluster Requirements
- Sufficient resources for build pods (recommended: 4GB memory, 2 CPU cores available)
- Network access to container registries you’ll push to
- Network access to Git repositories you’ll build from
Installation Options
Option 1: Bundled Installation (Recommended)
The brokkr-agent Helm chart includes Shipwright Build and Tekton Pipelines as vendored dependencies. This is enabled by default.
# Install agent with bundled Shipwright (default)
helm install brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--set broker.url=http://brokkr-broker:3000 \
--set broker.pak="<YOUR_PAK>" \
--wait
This installs:
- Tekton Pipelines (v0.37.2) - Task execution engine
- Shipwright Build (v0.10.0) - Build orchestration
- buildah ClusterBuildStrategy - Default build strategy
Option 2: Bring Your Own Shipwright
If you already have Shipwright and Tekton installed, or need specific versions:
# Disable bundled Shipwright
helm install brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--set broker.url=http://brokkr-broker:3000 \
--set broker.pak="<YOUR_PAK>" \
--set shipwright.enabled=false \
--wait
Manual Shipwright Installation
If installing Shipwright manually, ensure you have compatible versions:
# Install Tekton Pipelines (v0.59.0 or later recommended)
kubectl apply -f https://storage.googleapis.com/tekton-releases/pipeline/previous/v0.59.0/release.yaml
# Wait for Tekton to be ready
kubectl wait --for=condition=Ready pods -l app=tekton-pipelines-controller -n tekton-pipelines --timeout=300s
# Install Shipwright Build (v0.13.0 or later recommended)
kubectl apply -f https://github.com/shipwright-io/build/releases/download/v0.13.0/release.yaml
# Wait for Shipwright to be ready
kubectl wait --for=condition=Ready pods -l app=shipwright-build-controller -n shipwright-build --timeout=300s
# Install sample build strategies
kubectl apply -f https://github.com/shipwright-io/build/releases/download/v0.13.0/sample-strategies.yaml
Verifying Installation
Check Components
# Verify Tekton Pipelines
kubectl get pods -n tekton-pipelines
# Expected: tekton-pipelines-controller and tekton-pipelines-webhook Running
# Verify Shipwright Build
kubectl get pods -n shipwright-build
# Expected: shipwright-build-controller Running
# Verify ClusterBuildStrategies
kubectl get clusterbuildstrategies
# Expected: buildah (and others if sample strategies installed)
Test Build Capability
Create a simple test build to verify the installation:
# test-build.yaml
apiVersion: shipwright.io/v1beta1
kind: Build
metadata:
name: test-build
spec:
source:
type: Git
git:
url: https://github.com/shipwright-io/sample-go
strategy:
name: buildah
kind: ClusterBuildStrategy
output:
image: ttl.sh/brokkr-test-$(date +%s):1h
---
apiVersion: shipwright.io/v1beta1
kind: BuildRun
metadata:
generateName: test-build-run-
spec:
build:
name: test-build
# Apply the test build
kubectl apply -f test-build.yaml
# Watch the build progress
kubectl get buildruns -w
# Check build logs
kubectl logs -l buildrun.shipwright.io/name=test-build-run-xxxxx -c step-build
Configuration
Build Strategies
The bundled installation includes a buildah ClusterBuildStrategy. You can install additional strategies:
# Install all sample strategies (buildah, kaniko, buildpacks, etc.)
helm upgrade brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--reuse-values \
--set shipwright.installSampleStrategies=true
Disable Sample Strategies
If you only want Shipwright without sample strategies:
helm install brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--set broker.url=http://brokkr-broker:3000 \
--set broker.pak="<YOUR_PAK>" \
--set shipwright.enabled=true \
--set shipwright.installSampleStrategies=false
RBAC Configuration
The brokkr-agent automatically includes RBAC rules for Shipwright when enabled:
# Automatically included when shipwright.enabled=true
- apiGroups: ["shipwright.io"]
resources: ["builds", "buildruns"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["shipwright.io"]
resources: ["buildstrategies", "clusterbuildstrategies"]
verbs: ["get", "list", "watch"]
Registry Authentication
To push images to private registries, create a Kubernetes secret:
# Create registry credentials secret
kubectl create secret docker-registry registry-creds \
--docker-server=ghcr.io \
--docker-username=<username> \
--docker-password=<token> \
--docker-email=<email>
Reference this secret in your Build spec:
apiVersion: shipwright.io/v1beta1
kind: Build
metadata:
name: my-build
spec:
source:
type: Git
git:
url: https://github.com/org/repo
strategy:
name: buildah
kind: ClusterBuildStrategy
output:
image: ghcr.io/org/my-image:latest
pushSecret: registry-creds # Reference your secret here
Git Authentication
For private Git repositories:
# Create Git credentials secret (HTTPS)
kubectl create secret generic git-creds \
--from-literal=username=<username> \
--from-literal=password=<token>
# Or for SSH
kubectl create secret generic git-ssh-creds \
--from-file=ssh-privatekey=/path/to/id_rsa
Reference in your Build:
spec:
source:
type: Git
git:
url: https://github.com/org/private-repo
cloneSecret: git-creds
Troubleshooting
Build Pods Not Starting
# Check for pending pods
kubectl get pods -l build.shipwright.io/name
# Check events
kubectl get events --sort-by='.lastTimestamp'
# Verify ServiceAccount has required permissions
kubectl auth can-i create pods --as=system:serviceaccount:default:default
Shipwright Controller Not Ready
# Check controller logs
kubectl logs -n shipwright-build -l app=shipwright-build-controller
# Check for CRD installation
kubectl get crd builds.shipwright.io buildruns.shipwright.io
Tekton Pipeline Failures
# Check Tekton controller logs
kubectl logs -n tekton-pipelines -l app=tekton-pipelines-controller
# Check TaskRun status
kubectl get taskruns
kubectl describe taskrun <taskrun-name>
Next Steps
- Learn about Work Orders for managing builds through Brokkr
- Configure Monitoring for build metrics
Configuring Webhooks
Brokkr’s webhook system enables external systems to receive real-time notifications when events occur. This guide covers creating webhook subscriptions, configuring delivery options, and integrating with external services.
Overview
Webhooks provide HTTP callbacks for events such as:
- Deployment applied or failed
- Work order completed or failed
- Agent registered or deregistered
- Stack created or deleted
Brokkr supports two delivery modes:
- Broker delivery (default): The broker sends webhooks directly
- Agent delivery: An agent in the target cluster delivers webhooks, enabling access to in-cluster services
Prerequisites
- Admin PAK for creating webhook subscriptions
- Target endpoint accessible from the broker or agent (depending on delivery mode)
- HTTPS recommended for production endpoints
Creating a Webhook Subscription
Basic Webhook (Broker Delivery)
Create a webhook subscription using the API:
curl -X POST "http://broker:3000/api/v1/webhooks" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "Deployment Notifications",
"url": "https://my-service.example.com/webhooks/brokkr",
"event_types": ["deployment.applied", "deployment.failed"],
"auth_header": "Bearer my-webhook-secret"
}'
Response:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Deployment Notifications",
"has_url": true,
"has_auth_header": true,
"event_types": ["deployment.applied", "deployment.failed"],
"enabled": true,
"max_retries": 5,
"timeout_seconds": 30,
"created_at": "2025-01-02T10:00:00Z"
}
Webhook with Agent Delivery
For in-cluster targets that the broker cannot reach, configure agent delivery using target_labels:
curl -X POST "http://broker:3000/api/v1/webhooks" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "In-Cluster Alerts",
"url": "http://alertmanager.monitoring.svc.cluster.local:9093/api/v2/alerts",
"event_types": ["deployment.failed", "workorder.failed"],
"target_labels": ["env:production"]
}'
When target_labels is set:
- Deliveries are queued for agents matching ALL specified labels
- The matching agent fetches pending deliveries during its polling loop
- The agent delivers the webhook from inside the cluster
- The agent reports success/failure back to the broker
Wildcard Event Types
Subscribe to multiple events using wildcards:
curl -X POST "http://broker:3000/api/v1/webhooks" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "All Deployment Events",
"url": "https://webhook.example.com/deployments",
"event_types": ["deployment.*"]
}'
Supported wildcards:
deployment.*- All deployment eventsworkorder.*- All work order eventsagent.*- All agent eventsstack.*- All stack events*- All events
Configuring Delivery Options
Retry Settings
Configure retry behavior for failed deliveries:
curl -X POST "http://broker:3000/api/v1/webhooks" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "Critical Alerts",
"url": "https://pagerduty.example.com/webhook",
"event_types": ["deployment.failed"],
"max_retries": 10,
"timeout_seconds": 60
}'
Retry behavior:
- Failed deliveries use exponential backoff: 2, 4, 8, 16… seconds
- After
max_retriesfailures, deliveries are marked as “dead” - Delivery timeouts count as failures
Filters
Filter events by specific agents or stacks:
curl -X POST "http://broker:3000/api/v1/webhooks" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Stack Alerts",
"url": "https://slack.example.com/webhook",
"event_types": ["deployment.*"],
"filters": {
"labels": {"env": "production"}
}
}'
Managing Webhooks
List All Webhooks
curl "http://broker:3000/api/v1/webhooks" \
-H "Authorization: Bearer $ADMIN_PAK"
Get Webhook Details
curl "http://broker:3000/api/v1/webhooks/{webhook_id}" \
-H "Authorization: Bearer $ADMIN_PAK"
Update a Webhook
curl -X PUT "http://broker:3000/api/v1/webhooks/{webhook_id}" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"enabled": false
}'
Delete a Webhook
curl -X DELETE "http://broker:3000/api/v1/webhooks/{webhook_id}" \
-H "Authorization: Bearer $ADMIN_PAK"
Test a Webhook
Send a test event to verify connectivity:
curl -X POST "http://broker:3000/api/v1/webhooks/{webhook_id}/test" \
-H "Authorization: Bearer $ADMIN_PAK"
Viewing Delivery Status
List Deliveries for a Subscription
curl "http://broker:3000/api/v1/webhooks/{webhook_id}/deliveries" \
-H "Authorization: Bearer $ADMIN_PAK"
Filter by Status
# Show only failed deliveries
curl "http://broker:3000/api/v1/webhooks/{webhook_id}/deliveries?status=failed" \
-H "Authorization: Bearer $ADMIN_PAK"
# Show only dead (max retries exceeded)
curl "http://broker:3000/api/v1/webhooks/{webhook_id}/deliveries?status=dead" \
-H "Authorization: Bearer $ADMIN_PAK"
Delivery statuses:
pending- Waiting to be deliveredacquired- Claimed by broker or agent, delivery in progresssuccess- Successfully deliveredfailed- Delivery failed, will retrydead- Max retries exceeded
Webhook Payload Format
All webhook deliveries include these headers:
Content-Type: application/json
X-Brokkr-Event-Type: deployment.applied
X-Brokkr-Delivery-Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Authorization: <your-configured-auth-header>
Payload structure:
{
"id": "event-uuid",
"event_type": "deployment.applied",
"timestamp": "2025-01-02T10:00:00Z",
"data": {
"deployment_object_id": "...",
"agent_id": "...",
"status": "SUCCESS"
}
}
Common Patterns
Slack Integration
curl -X POST "http://broker:3000/api/v1/webhooks" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "Slack Deployment Alerts",
"url": "https://hooks.slack.com/services/T00/B00/XXX",
"event_types": ["deployment.applied", "deployment.failed"]
}'
PagerDuty Integration
curl -X POST "http://broker:3000/api/v1/webhooks" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "PagerDuty Critical Alerts",
"url": "https://events.pagerduty.com/v2/enqueue",
"event_types": ["deployment.failed", "workorder.failed"],
"auth_header": "Token token=your-pagerduty-token",
"max_retries": 10
}'
In-Cluster Alertmanager
curl -X POST "http://broker:3000/api/v1/webhooks" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "Alertmanager Notifications",
"url": "http://alertmanager.monitoring.svc.cluster.local:9093/api/v2/alerts",
"event_types": ["deployment.failed"],
"target_labels": ["role:monitoring"]
}'
Troubleshooting
Webhooks Not Being Delivered
-
Check if the subscription is enabled:
curl "http://broker:3000/api/v1/webhooks/{id}" \ -H "Authorization: Bearer $ADMIN_PAK" -
Check delivery status for failures:
curl "http://broker:3000/api/v1/webhooks/{id}/deliveries?status=failed" \ -H "Authorization: Bearer $ADMIN_PAK" -
Verify endpoint is reachable from broker/agent
Agent-Delivered Webhooks Failing
-
Verify agent has matching labels:
curl "http://broker:3000/api/v1/agents/{agent_id}" \ -H "Authorization: Bearer $ADMIN_PAK" -
Check agent logs for delivery errors:
kubectl logs -l app=brokkr-agent -c agent -
Ensure the agent is ACTIVE and polling
Deliveries Stuck in “Acquired” State
Deliveries have a 60-second TTL. If they remain acquired longer, they’ll be released back to pending. This can happen if:
- The delivering agent/broker crashed mid-delivery
- Network issues prevented result reporting
The system automatically recovers these deliveries.
Related Documentation
- Webhooks Reference - Complete API reference
- Event Types - List of all event types
- Architecture - How webhooks fit into Brokkr
Managing Stacks
Stacks are the fundamental organizational unit in Brokkr, representing collections of related Kubernetes resources that belong together. This guide covers creating stacks, configuring them with labels and annotations for targeting, managing their lifecycle, and understanding how they connect to agents and deployment objects.
Understanding Stacks
A stack serves as a container for deployment objects—the versioned snapshots of Kubernetes resources you want to deploy. When you create a stack, you establish a logical boundary for a set of resources, whether that’s an application, a service, or any collection of related Kubernetes manifests. Agents are then assigned to stacks through targeting relationships, enabling you to control which clusters receive which resources.
Each stack maintains a history of deployment objects, providing an immutable audit trail of every configuration change. This history enables rollback capabilities and satisfies compliance requirements for tracking what was deployed and when.
Creating Stacks
Basic Stack Creation
To create a stack, send a POST request to the stacks endpoint with a name and optional description:
curl -X POST http://localhost:3000/api/v1/stacks \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "payment-service",
"description": "Payment processing microservice and dependencies"
}'
The response includes the stack’s UUID which you’ll use for all subsequent operations:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "payment-service",
"description": "Payment processing microservice and dependencies",
"created_at": "2025-01-02T10:00:00Z",
"updated_at": "2025-01-02T10:00:00Z",
"generator_id": "00000000-0000-0000-0000-000000000000"
}
Stack Naming Conventions
Stack names must be non-empty strings up to 255 characters. While Brokkr doesn’t enforce naming conventions, consistent patterns make your infrastructure easier to navigate. Consider including:
- The application or service name
- The environment (if not using labels)
- A version or variant indicator for parallel deployments
Examples: frontend-app, database-cluster, monitoring-stack, api-gateway-v2
Configuring Labels and Annotations
Labels and annotations provide metadata for stacks that enables dynamic targeting and integration with external systems. Labels are simple string values used for categorization and selection. Annotations are key-value pairs for richer metadata.
Adding Labels
Labels enable pattern-based targeting where agents with matching labels automatically receive stacks. Add a label with a POST request:
curl -X POST http://localhost:3000/api/v1/stacks/$STACK_ID/labels \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '"production"'
Labels must be non-empty strings up to 64 characters with no whitespace. Common labeling patterns include:
| Purpose | Example Labels |
|---|---|
| Environment | production, staging, development |
| Region | us-east, eu-west, apac |
| Tier | frontend, backend, data |
| Criticality | critical, standard |
Listing Labels
View all labels for a stack:
curl http://localhost:3000/api/v1/stacks/$STACK_ID/labels \
-H "Authorization: Bearer $ADMIN_PAK"
Removing Labels
Remove a label by its value:
curl -X DELETE http://localhost:3000/api/v1/stacks/$STACK_ID/labels/production \
-H "Authorization: Bearer $ADMIN_PAK"
Adding Annotations
Annotations carry key-value metadata for integration with external systems or configuration hints:
curl -X POST http://localhost:3000/api/v1/stacks/$STACK_ID/annotations \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"stack_id": "550e8400-e29b-41d4-a716-446655440000",
"key": "cost-center",
"value": "engineering-team-a"
}'
Both keys and values must be non-empty strings up to 64 characters with no whitespace. Annotations are useful for:
| Purpose | Example |
|---|---|
| Cost allocation | cost-center=team-alpha |
| Owner tracking | owner=platform-team |
| SLA classification | sla-tier=gold |
| External references | jira-project=PLAT |
Listing Annotations
View all annotations for a stack:
curl http://localhost:3000/api/v1/stacks/$STACK_ID/annotations \
-H "Authorization: Bearer $ADMIN_PAK"
Removing Annotations
Remove an annotation by its key:
curl -X DELETE http://localhost:3000/api/v1/stacks/$STACK_ID/annotations/cost-center \
-H "Authorization: Bearer $ADMIN_PAK"
Targeting Stacks to Agents
Targeting establishes the relationship between stacks and the agents that should manage them. Without targeting, an agent won’t receive deployment objects from a stack regardless of other configuration.
Direct Assignment
Create a targeting relationship by associating an agent with a specific stack:
curl -X POST http://localhost:3000/api/v1/agents/$AGENT_ID/targets \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d "{
\"agent_id\": \"$AGENT_ID\",
\"stack_id\": \"$STACK_ID\"
}"
Direct assignment is appropriate when you have explicit control over which agents manage which stacks and the relationship is stable.
Label-Based Targeting
When both agents and stacks carry matching labels, you can configure agents to automatically target all stacks with those labels. This enables patterns like “all production agents receive all production stacks” without maintaining explicit per-pair associations.
To set up label-based targeting:
- Add labels to your stacks that represent their characteristics
- Add corresponding labels to agents that should manage those stacks
- Configure the targeting policy (see agent configuration)
The agent polls for stacks matching its label configuration and creates targeting relationships automatically.
Multi-Cluster Deployments
A single stack can be targeted by multiple agents, enabling multi-cluster deployment scenarios. Each agent independently polls for the stack’s deployment objects and applies them to its cluster. This is useful for:
- High availability across regions
- Disaster recovery setups
- Consistent infrastructure across environments
Working with Deployment Objects
Once a stack exists and is targeted to agents, you populate it with deployment objects containing Kubernetes resources.
Creating Deployment Objects
Submit Kubernetes YAML as a deployment object:
curl -X POST "http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg yaml "$(cat resources.yaml)" '{yaml_content: $yaml, is_deletion_marker: false}')"
Each deployment object receives a sequence ID that guarantees ordering. Agents process deployment objects in sequence order, ensuring resources are applied in the intended order.
Listing Deployment Objects
View all deployment objects in a stack:
curl "http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects" \
-H "Authorization: Bearer $ADMIN_PAK"
Using Templates
For standardized deployments, instantiate a template into a deployment object:
curl -X POST "http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects/from-template" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"template_id": "template-uuid-here",
"parameters": {
"replicas": 3,
"image_tag": "v1.2.3"
}
}'
The template’s parameters are validated against its JSON Schema before rendering. If the template has labels, they must match the stack’s labels for instantiation to succeed.
Stack Lifecycle
Updating Stacks
Modify a stack’s name or description:
curl -X PUT http://localhost:3000/api/v1/stacks/$STACK_ID \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "payment-service-v2",
"description": "Updated payment service with new features"
}'
Deleting Stacks
Brokkr uses soft deletion for stacks. When you delete a stack, it’s marked with a deleted_at timestamp rather than being removed from the database:
curl -X DELETE http://localhost:3000/api/v1/stacks/$STACK_ID \
-H "Authorization: Bearer $ADMIN_PAK"
Soft deletion marks the stack with a deleted_at timestamp:
- The stack stops appearing in list queries
- The underlying data remains intact for audit purposes and potential recovery
Note that soft-deleting a stack does not automatically cascade to its deployment objects or create deletion markers. To ensure agents remove resources from clusters, create a deletion marker deployment object before deleting the stack.
Understanding Deletion Markers
To clean up cluster resources, create a deployment object with is_deletion_marker: true for the stack before deleting it. Agents receiving this marker understand they should delete the stack’s resources rather than apply them. This ensures resources are cleaned up from clusters.
Generator Integration
Generators are external systems like CI/CD pipelines that create stacks programmatically. When a generator creates a stack, that stack’s generator_id links it to the creating generator, establishing ownership and access control.
Generators can only access stacks they created. This scoping ensures pipelines can’t accidentally modify resources belonging to other systems. See the Generators Guide for details on integrating CI/CD systems.
Best Practices
Organize by responsibility: Group resources into stacks based on what changes together. A stack should contain resources that are deployed, updated, and scaled as a unit.
Use labels for targeting: Rather than creating explicit targeting relationships for each stack-agent pair, use labels to establish patterns. This reduces configuration overhead as your infrastructure grows.
Keep stacks focused: While you can put any resources in a stack, keeping stacks focused on specific applications or services makes management clearer and reduces blast radius for changes.
Document with annotations: Use annotations to record metadata that helps teams understand the stack’s purpose, ownership, and relationship to business systems.
Plan for deletion: Remember that deleting a stack triggers resource deletion on targeted clusters. Ensure you understand which clusters are affected before deleting.
Related Documentation
- Quick Start Guide - First deployment walkthrough
- Core Concepts - Understanding Brokkr’s architecture
- Generators - CI/CD integration
- Templates - Standardized deployments
Working with Generators
Generators are identity principals that enable external systems to interact with Brokkr. They provide a way for CI/CD pipelines, automation tools, and other services to authenticate and manage resources within defined boundaries. This guide covers creating generators, integrating them with CI/CD systems, and managing their lifecycle.
Understanding Generators
A generator represents an external system that creates and manages Brokkr resources. Each generator receives a Pre-Authentication Key (PAK) that grants it permission to create stacks, templates, and deployment objects. Resources created by a generator are scoped to that generator, providing natural isolation between different automation pipelines or teams.
Generators differ from the admin PAK in important ways. The admin PAK has full access to all resources and administrative functions. Generator PAKs can only access resources they created and cannot perform administrative operations like creating other generators or managing agents.
Prerequisites
- Admin PAK for creating and managing generators
- Access to the Brokkr broker API
- CI/CD system or automation tool to configure
Creating a Generator
Step 1: Create the Generator
Create a new generator using the admin PAK:
curl -X POST "http://broker:3000/api/v1/generators" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "github-actions-prod",
"description": "Production deployment pipeline"
}'
The response includes the generator details and its PAK:
{
"generator": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "github-actions-prod",
"description": "Production deployment pipeline",
"created_at": "2025-01-02T10:00:00Z",
"updated_at": "2025-01-02T10:00:00Z"
},
"pak": "brk_gen_abc123...xyz789"
}
Step 2: Store the PAK Securely
The PAK is only returned once at creation time. Store it immediately in your secret management system:
- GitHub Actions: Add as a repository or organization secret
- GitLab CI: Add as a protected variable
- Jenkins: Store in credentials manager
- Vault/AWS Secrets Manager: Store with appropriate access policies
If you lose the PAK, you’ll need to rotate it (see PAK Rotation below).
CI/CD Integration
GitHub Actions Example
Configure your workflow to deploy through Brokkr:
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create Stack
env:
BROKKR_PAK: ${{ secrets.BROKKR_GENERATOR_PAK }}
BROKKR_URL: ${{ vars.BROKKR_URL }}
run: |
curl -X POST "$BROKKR_URL/api/v1/stacks" \
-H "Authorization: Bearer $BROKKR_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "my-app-${{ github.sha }}",
"description": "Deployed from commit ${{ github.sha }}"
}'
- name: Add Deployment Objects
env:
BROKKR_PAK: ${{ secrets.BROKKR_GENERATOR_PAK }}
BROKKR_URL: ${{ vars.BROKKR_URL }}
run: |
STACK_ID=$(cat stack-response.json | jq -r '.id')
curl -X POST "$BROKKR_URL/api/v1/stacks/$STACK_ID/deployment-objects" \
-H "Authorization: Bearer $BROKKR_PAK" \
-H "Content-Type: application/json" \
-d @deployment.json
GitLab CI Example
deploy:
stage: deploy
script:
- |
curl -X POST "$BROKKR_URL/api/v1/stacks" \
-H "Authorization: Bearer $BROKKR_GENERATOR_PAK" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"my-app-$CI_COMMIT_SHA\",
\"description\": \"Pipeline $CI_PIPELINE_ID\"
}"
only:
- main
Using Templates
Generators can create and use stack templates for consistent deployments:
# Create a template (using generator PAK)
curl -X POST "http://broker:3000/api/v1/templates" \
-H "Authorization: Bearer $GENERATOR_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "web-service",
"description": "Standard web service deployment",
"template_yaml": "...",
"schema_json": "..."
}'
# Create stack from template
curl -X POST "http://broker:3000/api/v1/stacks" \
-H "Authorization: Bearer $GENERATOR_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "my-web-service",
"template_name": "web-service",
"parameters": {
"replicas": 3,
"image": "myapp:v1.2.3"
}
}'
Managing Generators
List Generators
View all generators (admin only):
curl "http://broker:3000/api/v1/generators" \
-H "Authorization: Bearer $ADMIN_PAK"
Get Generator Details
A generator can view its own details:
curl "http://broker:3000/api/v1/generators/$GENERATOR_ID" \
-H "Authorization: Bearer $GENERATOR_PAK"
Update Generator
Update the generator’s metadata:
curl -X PUT "http://broker:3000/api/v1/generators/$GENERATOR_ID" \
-H "Authorization: Bearer $GENERATOR_PAK" \
-H "Content-Type: application/json" \
-d '{
"description": "Updated description"
}'
Delete Generator
Soft-delete a generator (admin or generator itself):
curl -X DELETE "http://broker:3000/api/v1/generators/$GENERATOR_ID" \
-H "Authorization: Bearer $ADMIN_PAK"
Deleting a generator cascades the soft-delete to all stacks owned by the generator and their deployment objects. This is handled by a database trigger, so the cascade is atomic.
PAK Rotation
Rotate the generator’s PAK for security best practices or if the current PAK is compromised:
curl -X POST "http://broker:3000/api/v1/generators/$GENERATOR_ID/rotate-pak" \
-H "Authorization: Bearer $GENERATOR_PAK"
The response contains the new PAK:
{
"generator": {
"id": "a1b2c3d4-...",
"name": "github-actions-prod",
...
},
"pak": "brk_gen_new123...newxyz"
}
After rotation:
- The old PAK is immediately invalidated
- Update all CI/CD systems with the new PAK
- Verify deployments work with the new credentials
Consider rotating PAKs:
- On a regular schedule (quarterly, annually)
- When team members with access leave
- If the PAK may have been exposed
- After security incidents
Access Control
Generators operate under a scoped permission model:
| Operation | Admin PAK | Generator PAK |
|---|---|---|
| Create generators | Yes | No |
| List all generators | Yes | No |
| View own generator | Yes | Yes |
| Update own generator | Yes | Yes |
| Delete own generator | Yes | Yes |
| Rotate own PAK | Yes | Yes |
| Create stacks | Yes | Yes |
| View own stacks | Yes | Yes |
| View other generators’ stacks | Yes | No |
| Manage agents | Yes | No |
| Manage webhooks | Yes | No |
Best Practices
One Generator Per Pipeline
Create separate generators for each deployment pipeline or team. This provides:
- Clear ownership of resources
- Independent PAK rotation
- Easier auditing and troubleshooting
- Isolation between environments
Naming Conventions
Use descriptive names that identify the purpose and scope:
github-actions-prod- Production pipeline in GitHub Actionsgitlab-ci-staging- Staging pipeline in GitLab CIjenkins-nightly-builds- Nightly build automationteam-platform-prod- Platform team’s production deployments
Secret Management
Never store PAKs in:
- Source code repositories
- Unencrypted configuration files
- Logs or console output
Always use:
- CI/CD secret management (GitHub Secrets, GitLab Variables)
- Secret management systems (Vault, AWS Secrets Manager)
- Encrypted environment variables
Troubleshooting
Authentication Failures
If API calls fail with 401 or 403:
- Verify the PAK is correct and not expired
- Check if the PAK was rotated
- Ensure you’re using the generator PAK, not the admin PAK (for generator-scoped operations)
Cannot See Resources
If a generator cannot see expected stacks or templates:
- Verify the resources were created with this generator’s PAK
- Resources created by other generators are not visible
- Use admin PAK to view all resources across generators
PAK Lost
If you’ve lost a generator’s PAK:
- Use the admin PAK to rotate:
POST /api/v1/generators/{id}/rotate-pak - Store the new PAK securely
- Update all systems using the old PAK
Related Documentation
- Generators API Reference - Complete API documentation
- Stack Templates - Using templates with generators
- Authentication - Understanding Brokkr authentication
Monitoring Deployment Health
Brokkr agents continuously monitor the health of deployed Kubernetes resources and report status back to the broker. This provides centralized visibility into deployment health across all clusters without requiring direct cluster access. This guide covers configuring health monitoring, interpreting health status, and troubleshooting common issues.
How Health Monitoring Works
When an agent applies deployment objects to a Kubernetes cluster, it tracks those resources and periodically checks their health. The agent examines pod status, container states, and Kubernetes conditions to determine overall health. This information is reported to the broker, where it can be viewed through the API or UI.
Health monitoring runs as a background process on each agent. On each check interval, the agent queries the Kubernetes API for pods associated with each deployment object, analyzes their status, and sends a consolidated health report to the broker.
Health Status Values
The health monitoring system reports one of four status values:
| Status | Description |
|---|---|
healthy | All pods are ready and running without issues |
degraded | Some pods have issues but the deployment is partially functional |
failing | The deployment has failed or all pods are in error states |
unknown | Health cannot be determined (no pods found or API errors) |
Detected Conditions
The agent detects and reports these problematic conditions:
Container Issues:
ImagePullBackOff- Unable to pull container imageErrImagePull- Error pulling container imageCrashLoopBackOff- Container repeatedly crashingCreateContainerConfigError- Invalid container configurationInvalidImageName- Malformed image referenceRunContainerError- Error starting containerContainerCannotRun- Container failed to run
Resource Issues:
OOMKilled- Container killed due to memory limitsError- Container exited with error
Pod Issues:
PodFailed- Pod entered failed phase
Configuring Health Monitoring
Enabling Health Monitoring
Health monitoring is enabled by default. Configure it through environment variables:
# Helm values for agent
agent:
config:
deploymentHealthEnabled: true
deploymentHealthInterval: 60
Or set environment variables directly:
BROKKR__AGENT__DEPLOYMENT_HEALTH_ENABLED=true
BROKKR__AGENT__DEPLOYMENT_HEALTH_INTERVAL=60
Adjusting Check Interval
The check interval determines how frequently the agent evaluates deployment health. The default is 60 seconds, which balances responsiveness with API load.
For environments where rapid detection is critical:
agent:
config:
deploymentHealthInterval: 30 # Check every 30 seconds
For large clusters with many deployments, increase the interval to reduce API load:
agent:
config:
deploymentHealthInterval: 120 # Check every 2 minutes
Disabling Health Monitoring
To disable health monitoring entirely:
agent:
config:
deploymentHealthEnabled: false
Note that disabling health monitoring means the broker will not have visibility into deployment status.
Viewing Health Status
Via API
Query health status for a specific deployment object:
curl "http://broker:3000/api/v1/deployment-objects/{id}/health" \
-H "Authorization: Bearer $ADMIN_PAK"
Response:
{
"deployment_object_id": "a1b2c3d4-...",
"overall_status": "healthy",
"health_records": [
{
"agent_id": "e5f6g7h8-...",
"status": "healthy",
"summary": {
"pods_ready": 3,
"pods_total": 3,
"conditions": []
},
"checked_at": "2025-01-02T10:00:00Z"
}
]
}
Understanding the Summary
The health summary provides details about pod status:
{
"pods_ready": 2,
"pods_total": 3,
"conditions": ["ImagePullBackOff"],
"resources": [
{
"kind": "Pod",
"name": "my-app-abc123",
"namespace": "production",
"ready": false,
"message": "Back-off pulling image \"myapp:invalid\""
}
]
}
| Field | Description |
|---|---|
pods_ready | Number of pods in Ready state |
pods_total | Total number of pods found |
conditions | List of detected problematic conditions |
resources | Per-resource details (optional) |
Common Scenarios
ImagePullBackOff
When the agent reports ImagePullBackOff:
- Verify the image name and tag are correct
- Check that the image exists in the registry
- Verify the cluster has network access to the registry
- Check image pull secrets are configured correctly
# Check pod events for details
kubectl describe pod <pod-name> -n <namespace>
# Check image pull secrets
kubectl get secrets -n <namespace>
CrashLoopBackOff
When containers repeatedly crash:
-
Check container logs for error messages:
kubectl logs <pod-name> -n <namespace> --previous -
Verify the application configuration is correct
-
Check resource limits aren’t too restrictive
-
Ensure required environment variables and secrets are present
OOMKilled
When containers are killed for memory:
-
Increase memory limits:
resources: limits: memory: "512Mi" # Increase as needed -
Investigate application memory usage
-
Consider memory profiling to identify leaks
Unknown Status
When status shows as unknown:
- Verify pods exist for the deployment object
- Check the agent has RBAC permissions to list pods
- Check agent logs for API errors:
kubectl logs -l app=brokkr-agent -c agent
Multi-Agent Deployments
When a deployment object is targeted to multiple agents, each agent reports its own health status. The broker stores health per agent, reflecting that the same deployment may have different health on different clusters.
The health endpoint always returns records from all reporting agents in the health_records array, along with an overall_status that reflects the aggregate state:
curl "http://broker:3000/api/v1/deployment-objects/{id}/health" \
-H "Authorization: Bearer $ADMIN_PAK"
Webhook Integration
Configure webhooks to receive notifications when deployment health changes:
curl -X POST "http://broker:3000/api/v1/webhooks" \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "Health Alerts",
"url": "https://alerts.example.com/webhook",
"event_types": ["deployment.failed"]
}'
The deployment.failed event fires when a deployment transitions to failing status.
Troubleshooting
Health Not Updating
If health status isn’t updating:
-
Check the agent is running and connected:
kubectl get pods -l app=brokkr-agent -
Verify health monitoring is enabled:
kubectl get configmap brokkr-agent-config -o yaml -
Check agent logs for health check errors:
kubectl logs -l app=brokkr-agent -c agent | grep -i health
Incorrect Health Status
If reported health doesn’t match actual pod status:
- Verify pods have the correct deployment object ID label
- Check the health check interval - status may be stale
- Confirm the agent has permission to list pods across namespaces
High API Load
If health monitoring causes excessive Kubernetes API load:
- Increase the check interval
- Consider reducing the number of deployment objects per agent
- Monitor agent metrics for API call rates
Related Documentation
- Configuration Reference - Agent configuration options
- Architecture - How agents monitor health
- Webhooks - Alert on health changes
Understanding Reconciliation
Brokkr agents continuously reconcile the desired state defined in deployment objects with the actual state in Kubernetes clusters. This guide explains how the reconciliation loop works, how sequence IDs ensure correct ordering, how the system handles pre-existing resources, and what to do when things go wrong.
The Reconciliation Model
Brokkr uses a pull-based reconciliation model where agents periodically fetch their target state from the broker and ensure their cluster matches. This differs from push-based systems where a central controller actively pushes changes—instead, each agent owns the reconciliation responsibility for its cluster.
The core loop runs at a configurable polling interval (default: 10 seconds). On each cycle, the agent fetches deployment objects from the broker, validates the resources, applies them using Kubernetes server-side apply, and reports the result back to the broker as an event. This cycle continues indefinitely, ensuring clusters stay aligned with the desired state even if drift occurs.
Polling and Target State
When an agent polls for its target state, it receives deployment objects from all stacks it’s targeting. The broker returns objects ordered by sequence ID, ensuring the agent processes them in the order they were created.
Agent -> Broker: GET /api/v1/agents/{id}/target-state
Broker -> Agent: [DeploymentObject, DeploymentObject, ...]
Each deployment object contains the complete YAML content to be applied and a checksum calculated from that content. The checksum serves as a version identifier—resources in the cluster are annotated with the checksum so the agent can identify which version they belong to.
Sequence IDs and Ordering
Every deployment object receives a monotonically increasing sequence ID when created. This provides a global ordering that’s essential for correct reconciliation:
- The agent processes objects in sequence order
- Later objects supersede earlier ones for the same stack
- The most recent (highest sequence ID) deployment object represents current desired state
When multiple deployment objects exist for a stack, the agent only needs to apply the most recent one. Earlier objects are historical records useful for audit purposes but don’t affect the current cluster state.
The Apply Process
For each deployment object, the reconciliation proceeds through several stages:
-
Priority Resource Application: Namespaces and CustomResourceDefinitions are applied first. These resources must exist before namespaced resources can be validated or created.
-
Validation: Remaining resources are validated using a Kubernetes dry-run apply. This catches schema errors, missing fields, and other issues before attempting the real apply.
-
Server-Side Apply: Resources are applied using Kubernetes server-side apply with force enabled. This approach allows Brokkr to take ownership of fields it manages while preserving fields managed by other controllers.
-
Annotation Injection: Each applied resource receives annotations identifying its stack and the deployment object checksum:
k8s.brokkr.io/stack: Links the resource to its stack (label)k8s.brokkr.io/deployment-checksum: Identifies which deployment object version created it (annotation)
-
Pruning: After applying desired resources, the agent queries the cluster for all resources with the stack annotation and deletes any that don’t match the current checksum. This removes resources that were part of previous deployments but aren’t in the current desired state.
Handling Pre-Existing Resources
When Brokkr encounters resources that already exist in the cluster, several scenarios arise:
Resources without Brokkr annotations: Server-side apply succeeds, and Brokkr adds its annotations. The resource becomes managed by Brokkr going forward. Any fields not specified in the deployment object remain unchanged.
Resources with matching annotations: The apply is idempotent—the resource is updated to match the desired state. If the content hasn’t changed, this is effectively a no-op.
Resources with different checksum: The resource was created by a previous deployment object. It gets updated with the new content and checksum. If the resource is no longer in the desired state, it gets pruned during the cleanup phase.
Resources with owner references: During pruning, Brokkr skips resources that have owner references. These resources are managed by Kubernetes controllers and will be garbage collected when their owner is deleted.
Rollback on Failure
If reconciliation fails partway through applying resources, the agent attempts to roll back changes to maintain cluster consistency:
Namespace rollback: If the agent created new namespaces as part of this reconciliation and a later step fails, those namespaces are deleted. This prevents orphaned namespaces from accumulating after failed deployments.
No resource rollback: Individual resources that were successfully applied are not rolled back. This means partial applies can occur. The next reconciliation cycle will attempt the full apply again.
Error reporting: Failed reconciliation generates a failure event that’s sent to the broker. The event includes the error message, enabling visibility into what went wrong through the broker’s API or webhooks.
Deletion Markers
When a stack is deleted, Brokkr creates a special deployment object with is_deletion_marker: true. When the agent receives this:
- The agent identifies all cluster resources belonging to that stack
- Each resource is deleted from the cluster
- A success event is reported to the broker
Deletion markers ensure resources are cleaned up even if the agent was offline when the stack was deleted. The agent will process the deletion marker on its next poll and remove the resources.
Configuration Options
Polling Interval
Control how frequently the agent checks for changes:
agent:
polling_interval: 10 # seconds
Shorter intervals mean faster propagation of changes but higher API load on both the broker and Kubernetes API server. For production deployments, 10-60 seconds is typically appropriate.
Retry Behavior
The agent implements exponential backoff for transient Kubernetes API errors:
- Initial retry interval: 1 second
- Maximum retry interval: 60 seconds
- Maximum elapsed time: 5 minutes
- Backoff multiplier: 2.0
Retryable errors include:
- HTTP 429 (Too Many Requests)
- HTTP 500 (Internal Server Error)
- HTTP 503 (Service Unavailable)
- HTTP 504 (Gateway Timeout)
Non-retryable errors fail immediately and are reported to the broker.
Troubleshooting Reconciliation Issues
Resources Not Being Applied
If resources aren’t appearing in your cluster:
-
Check agent status: Verify the agent is running and in ACTIVE status. Inactive agents skip deployment object requests.
-
Check targeting: Confirm the stack is targeted to the agent via
GET /api/v1/agents/{id}/targets. -
Check agent logs: Look for validation errors or API failures in the agent container logs.
-
Check events: Query the broker for events related to the deployment object to see if failures were reported.
Resources Not Being Deleted
If resources persist after stack deletion:
-
Verify deletion marker: Check that a deletion marker deployment object was created for the stack.
-
Check labels: Verify the resources have the
k8s.brokkr.io/stacklabel. Resources without this label aren’t managed by Brokkr. -
Check owner references: Resources with owner references are skipped during pruning. They’ll be cleaned up when their owner is deleted.
Validation Failures
If deployments fail validation:
-
Check YAML syntax: Ensure the YAML in your deployment object is valid.
-
Check API versions: Verify the apiVersion and kind are correct for your Kubernetes version.
-
Check namespaces: If referencing a namespace, ensure it’s either included in the deployment object or already exists in the cluster.
-
Check CRDs: If using custom resources, ensure the CRD is either included in the deployment object or already installed.
Drift Detection
Brokkr doesn’t continuously monitor for drift—it only reconciles during polling cycles. If resources are modified outside of Brokkr:
- The next deployment object apply will restore the Brokkr-managed state
- Fields not managed by Brokkr (not in the deployment object) will be preserved
- To force a reconcile, submit the same deployment object content again (it gets a new sequence ID)
Related Documentation
- Core Concepts - Understanding the pull-based model
- Deployment Health - Monitoring applied resources
- Quick Start Guide - First deployment walkthrough
- Managing Stacks - Stack lifecycle and deletion
Using Stack Templates
Stack templates allow you to define reusable Kubernetes manifests with parameterized values. Templates use Tera for templating and JSON Schema for parameter validation.
Concepts
What Templates Provide
- Reusability: Define common patterns once, instantiate many times
- Validation: Parameters are validated against JSON Schema before rendering
- Safety: Template syntax is validated at creation time
- Versioning: Updates create new versions, preserving history
- Access Control: System templates (admin) vs generator-owned templates
Template Matching
Templates can be constrained to specific stacks using labels and annotations:
- No labels/annotations: Template can be used with any stack
- With labels: ALL template labels must exist on the target stack
- With annotations: ALL template annotation key-value pairs must exist on the target stack
Creating a Template
Basic Template Structure
A template consists of:
- Name: Identifier for the template
- Template Content: Tera-templated YAML
- Parameters Schema: JSON Schema defining valid parameters
Example: Nginx Deployment Template
curl -X POST http://localhost:3000/api/v1/templates \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "nginx-deployment",
"description": "Simple nginx deployment with configurable replicas and image",
"template_content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: {{ name }}\n namespace: {{ namespace | default(value=\"default\") }}\nspec:\n replicas: {{ replicas | default(value=1) }}\n selector:\n matchLabels:\n app: {{ name }}\n template:\n metadata:\n labels:\n app: {{ name }}\n spec:\n containers:\n - name: nginx\n image: nginx:{{ version | default(value=\"latest\") }}\n ports:\n - containerPort: 80",
"parameters_schema": "{\"type\": \"object\", \"required\": [\"name\"], \"properties\": {\"name\": {\"type\": \"string\", \"minLength\": 1, \"description\": \"Deployment name\"}, \"namespace\": {\"type\": \"string\", \"description\": \"Target namespace\"}, \"replicas\": {\"type\": \"integer\", \"minimum\": 1, \"maximum\": 10, \"description\": \"Number of replicas\"}, \"version\": {\"type\": \"string\", \"description\": \"Nginx image tag\"}}}"
}'
Tera Templating
Variable Substitution
Use {{ variable }} syntax for simple substitution:
metadata:
name: {{ name }}
namespace: {{ namespace }}
Default Values
Use the default filter for optional parameters:
spec:
replicas: {{ replicas | default(value=1) }}
Conditionals
Use {% if %} blocks for conditional content:
{% if enable_hpa %}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ name }}-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ name }}
minReplicas: {{ min_replicas | default(value=1) }}
maxReplicas: {{ max_replicas | default(value=10) }}
{% endif %}
Loops
Use {% for %} to iterate over arrays:
spec:
containers:
{% for container in containers %}
- name: {{ container.name }}
image: {{ container.image }}
ports:
{% for port in container.ports %}
- containerPort: {{ port }}
{% endfor %}
{% endfor %}
Filters
Tera provides many built-in filters:
metadata:
name: {{ name | lower }} # lowercase
labels:
version: "{{ version | upper }}" # uppercase
slug: {{ name | slugify }} # URL-safe slug
See the Tera documentation for all available filters.
JSON Schema Validation
Basic Schema
Define required and optional parameters:
{
"type": "object",
"required": ["name", "image"],
"properties": {
"name": {
"type": "string",
"minLength": 1,
"description": "Resource name"
},
"image": {
"type": "string",
"pattern": "^[a-z0-9./-]+:[a-zA-Z0-9.-]+$"
},
"replicas": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 1
}
}
}
Validation Constraints
Common JSON Schema constraints:
| Constraint | Type | Description |
|---|---|---|
minLength, maxLength | string | String length limits |
pattern | string | Regex pattern |
minimum, maximum | number | Numeric bounds |
enum | any | Allowed values |
minItems, maxItems | array | Array length limits |
Nested Objects
{
"type": "object",
"properties": {
"resources": {
"type": "object",
"properties": {
"cpu": {"type": "string", "pattern": "^[0-9]+m?$"},
"memory": {"type": "string", "pattern": "^[0-9]+[GMK]i$"}
}
}
}
}
Instantiating Templates
Once a template is created, instantiate it to create deployment objects:
curl -X POST http://localhost:3000/api/v1/stacks/$STACK_ID/deployment-objects/from-template \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"template_id": "'"$TEMPLATE_ID"'",
"parameters": {
"name": "my-nginx",
"namespace": "production",
"replicas": 3,
"version": "1.25"
}
}'
The broker will:
- Validate template labels match the stack
- Validate parameters against the JSON Schema
- Render the template with Tera
- Create a deployment object in the stack
Template Labels and Annotations
Restricting Template Usage
Add labels to restrict which stacks can use a template:
# Add label to template
curl -X POST http://localhost:3000/api/v1/templates/$TEMPLATE_ID/labels \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '"env=production"'
# Add annotation to template
curl -X POST http://localhost:3000/api/v1/templates/$TEMPLATE_ID/annotations \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{"key": "tier", "value": "1"}'
Matching Rules
| Template | Stack | Result |
|---|---|---|
| No labels | Any labels | Matches |
env=prod | env=prod, team=platform | Matches |
env=prod | env=staging | No match |
env=prod, tier=1 | env=prod | No match (missing tier) |
When instantiation fails due to label mismatch, you’ll receive a 422 response with details:
{
"error": "Template labels do not match stack",
"missing_labels": ["tier=1"],
"missing_annotations": []
}
Template Versioning
Templates are immutable. Updates create new versions:
# Update template (creates version 2)
curl -X PUT http://localhost:3000/api/v1/templates/$TEMPLATE_ID \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"description": "Updated nginx template with HPA support",
"template_content": "...",
"parameters_schema": "..."
}'
Each version has a unique ID. Deployment objects reference the specific template version used.
Generator-Owned Templates
Generators can create and manage their own templates:
# Generator creates template
curl -X POST http://localhost:3000/api/v1/templates \
-H "Authorization: Bearer $GENERATOR_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "my-app-template",
"template_content": "...",
"parameters_schema": "..."
}'
Generators can only:
- View system templates (no generator_id) and their own templates
- Modify/delete only their own templates
- Instantiate templates into stacks they own
Complete Example: PostgreSQL Database
1. Create the Template
curl -X POST http://localhost:3000/api/v1/templates \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"name": "postgresql-database",
"description": "PostgreSQL StatefulSet with PVC",
"template_content": "apiVersion: v1\nkind: Secret\nmetadata:\n name: {{ name }}-credentials\n namespace: {{ namespace }}\nstringData:\n POSTGRES_USER: {{ username | default(value=\"postgres\") }}\n POSTGRES_PASSWORD: {{ password }}\n POSTGRES_DB: {{ database }}\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: {{ name }}\n namespace: {{ namespace }}\nspec:\n ports:\n - port: 5432\n clusterIP: None\n selector:\n app: {{ name }}\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: {{ name }}\n namespace: {{ namespace }}\nspec:\n serviceName: {{ name }}\n replicas: {{ replicas | default(value=1) }}\n selector:\n matchLabels:\n app: {{ name }}\n template:\n metadata:\n labels:\n app: {{ name }}\n spec:\n containers:\n - name: postgres\n image: postgres:{{ version | default(value=\"15\") }}\n ports:\n - containerPort: 5432\n envFrom:\n - secretRef:\n name: {{ name }}-credentials\n volumeMounts:\n - name: data\n mountPath: /var/lib/postgresql/data\n volumeClaimTemplates:\n - metadata:\n name: data\n spec:\n accessModes: [\"ReadWriteOnce\"]\n resources:\n requests:\n storage: {{ storage_size }}",
"parameters_schema": "{\"type\": \"object\", \"required\": [\"name\", \"namespace\", \"database\", \"password\", \"storage_size\"], \"properties\": {\"name\": {\"type\": \"string\", \"minLength\": 1}, \"namespace\": {\"type\": \"string\", \"minLength\": 1}, \"database\": {\"type\": \"string\", \"minLength\": 1}, \"username\": {\"type\": \"string\"}, \"password\": {\"type\": \"string\", \"minLength\": 8}, \"version\": {\"type\": \"string\"}, \"replicas\": {\"type\": \"integer\", \"minimum\": 1}, \"storage_size\": {\"type\": \"string\", \"pattern\": \"^[0-9]+[GMK]i$\"}}}"
}'
2. Add Production Label
curl -X POST http://localhost:3000/api/v1/templates/$TEMPLATE_ID/labels \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '"database=postgresql"'
3. Instantiate for Production
curl -X POST http://localhost:3000/api/v1/stacks/$PROD_STACK_ID/deployment-objects/from-template \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"template_id": "'"$TEMPLATE_ID"'",
"parameters": {
"name": "orders-db",
"namespace": "production",
"database": "orders",
"password": "secure-password-here",
"version": "15",
"replicas": 3,
"storage_size": "100Gi"
}
}'
Troubleshooting
Invalid Tera Syntax
Template creation fails with syntax errors:
{
"error": "Invalid Tera syntax: ..."
}
Check for:
- Unclosed
{{ }}or{% %}blocks - Missing
{% endif %}or{% endfor %} - Invalid filter names
Invalid JSON Schema
{
"error": "Invalid JSON Schema: ..."
}
Validate your schema at jsonschemavalidator.net.
Parameter Validation Failed
{
"error": "Invalid parameters",
"validation_errors": [
"/replicas: 0 is less than the minimum of 1"
]
}
Check that parameters match the schema constraints.
Template Rendering Failed
{
"error": "Template rendering failed: Variable `name` not found"
}
Ensure all required template variables are provided in parameters, or use | default(value=...) for optional ones.
How-To: Running On-Demand Diagnostics
This guide shows how to collect pod statuses, Kubernetes events, and container logs from a remote cluster when a deployment is misbehaving. Brokkr’s diagnostic system lets you request this data through the broker API without direct kubectl access to the target cluster.
When to Use Diagnostics
Use on-demand diagnostics when:
- A deployment shows
degradedorfailinghealth status - You need to see pod conditions, restart counts, or OOMKill events
- You want container logs from a remote cluster you can’t directly access
- You’re troubleshooting why a deployment object failed to apply
Prerequisites
- Admin PAK for the broker
- The
deployment_object_idof the resource you want to diagnose - The
agent_idof the agent running in the target cluster - The agent must be connected and sending heartbeats
Step 1: Identify the Deployment Object
If you know the stack, list its deployment objects:
curl -s "http://localhost:3000/api/v1/stacks/${STACK_ID}/deployment-objects" \
-H "Authorization: <admin-pak>" | jq '.[] | {id, sequence_id, created_at}'
Check the health status to confirm something is wrong:
curl -s "http://localhost:3000/api/v1/deployment-objects/${DO_ID}/health" \
-H "Authorization: <admin-pak>" | jq .
Step 2: Find the Target Agent
List agents that target this stack:
curl -s "http://localhost:3000/api/v1/agents" \
-H "Authorization: <admin-pak>" | jq '.[] | {id, name, cluster_name, last_heartbeat}'
Verify the agent has a recent heartbeat (within the last few minutes).
Step 3: Request Diagnostics
Create a diagnostic request:
curl -s -X POST "http://localhost:3000/api/v1/deployment-objects/${DO_ID}/diagnostics" \
-H "Authorization: <admin-pak>" \
-H "Content-Type: application/json" \
-d "{
\"agent_id\": \"${AGENT_ID}\",
\"requested_by\": \"oncall-engineer\",
\"retention_minutes\": 120
}" | jq .
Save the diagnostic request ID from the response:
DIAG_ID="..."
The retention_minutes field controls how long the request stays active before expiring. Default is 60 minutes, maximum is 1440 (24 hours).
Step 4: Wait for Results
The agent picks up the diagnostic request on its next poll cycle. Poll the diagnostic status:
curl -s "http://localhost:3000/api/v1/diagnostics/${DIAG_ID}" \
-H "Authorization: <admin-pak>" | jq '.request.status'
Status progression: pending → claimed → completed
Step 5: Read the Results
Once the status is completed, the full results are available:
# Pod statuses
curl -s "http://localhost:3000/api/v1/diagnostics/${DIAG_ID}" \
-H "Authorization: <admin-pak>" | jq -r '.result.pod_statuses' | jq .
# Kubernetes events
curl -s "http://localhost:3000/api/v1/diagnostics/${DIAG_ID}" \
-H "Authorization: <admin-pak>" | jq -r '.result.events' | jq .
# Container logs
curl -s "http://localhost:3000/api/v1/diagnostics/${DIAG_ID}" \
-H "Authorization: <admin-pak>" | jq -r '.result.log_tails' | jq .
Reading Pod Statuses
Look for:
- Phase:
PendingorFailedindicates problems - Conditions: Check
Ready=Falsewith the reason - Containers: Look for
restart_count > 0,state=waitingwith reasons likeCrashLoopBackOff, orstate=terminatedwith reasonOOMKilled
Reading Events
Filter for warnings:
curl -s "http://localhost:3000/api/v1/diagnostics/${DIAG_ID}" \
-H "Authorization: <admin-pak>" \
| jq -r '.result.events' \
| jq '.[] | select(.event_type == "Warning")'
Common warning events: FailedScheduling, Unhealthy, BackOff, FailedMount.
Reading Logs
Log tails are keyed by pod-name/container-name:
curl -s "http://localhost:3000/api/v1/diagnostics/${DIAG_ID}" \
-H "Authorization: <admin-pak>" \
| jq -r '.result.log_tails' \
| jq 'to_entries[] | "\(.key):\n\(.value)\n---"' -r
Each container’s last 100 log lines are included.
Troubleshooting
Diagnostic stays in pending state:
- Check the agent’s heartbeat — it may be disconnected
- Verify the agent is targeting the stack that contains the deployment object
- Check the agent logs for errors
Diagnostic moves to expired:
- The retention period elapsed before the agent could claim it
- Increase
retention_minutesand try again - Check if the agent is running and polling
Diagnostic moves to failed:
- The agent encountered an error collecting data
- Check the agent logs for Kubernetes API errors
- Verify the agent has RBAC permissions to read pods, events, and logs
Cleanup
Diagnostics are automatically cleaned up by the broker’s background task based on broker.diagnostic_cleanup_interval_seconds (default: 15 minutes) and broker.diagnostic_max_age_hours (default: 1 hour).
Related Documentation
- Diagnostics Reference — complete API and data model reference
- Monitoring Deployment Health — continuous health monitoring
- Health Endpoints — health check configuration
How-To: Managing PAKs (Key Rotation)
Pre-Authentication Keys (PAKs) are the authentication credentials for all Brokkr entities — admins, agents, and generators. This guide covers creating, rotating, and managing PAKs.
Overview
PAKs look like brokkr_BR3rVsDa_GK3QN7CDUzYc6iKgMkJ98M2WSimM5t6U8. Brokkr stores only the hash — once a PAK is displayed at creation, it cannot be recovered, only rotated. See the Environment Variables Reference for PAK configuration details.
Rotating the Admin PAK
Via CLI (recommended)
Run on the broker host:
brokkr-broker rotate admin
The new PAK is printed to stdout. The old PAK immediately stops working.
Via API
Not available — admin PAK rotation requires CLI access to prevent an attacker with a compromised admin PAK from locking out the real admin.
When to Rotate
- After personnel changes (someone with admin access leaves)
- If the PAK may have been exposed in logs, version control, or screenshots
- As part of a regular rotation schedule (e.g., quarterly)
Rotating Agent PAKs
Via CLI
brokkr-broker rotate agent --uuid <agent-uuid>
Via API
An agent can rotate its own PAK, or an admin can rotate any agent’s PAK:
# As admin
curl -s -X POST "http://localhost:3000/api/v1/agents/${AGENT_ID}/rotate-pak" \
-H "Authorization: <admin-pak>" | jq .
# As the agent itself
curl -s -X POST "http://localhost:3000/api/v1/agents/${AGENT_ID}/rotate-pak" \
-H "Authorization: <agent-pak>" | jq .
Response:
{
"agent": { "id": "...", "name": "prod-1", ... },
"pak": "brokkr_BRnewKey_NewLongTokenValue1234567890"
}
Updating the Agent After Rotation
After rotating, update the agent’s configuration with the new PAK:
Helm deployment:
helm upgrade brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--set broker.pak="brokkr_BRnewKey_NewLongTokenValue1234567890"
Environment variable:
BROKKR__AGENT__PAK=brokkr_BRnewKey_NewLongTokenValue1234567890
Kubernetes secret:
kubectl create secret generic brokkr-agent-pak \
--from-literal=pak="brokkr_BRnewKey_NewLongTokenValue1234567890" \
--dry-run=client -o yaml | kubectl apply -f -
Warning: The agent will fail to authenticate with the old PAK immediately after rotation. Ensure you update the agent configuration before the next poll cycle, or the agent will lose connectivity until updated.
Rotating Generator PAKs
Via CLI
brokkr-broker rotate generator --uuid <generator-uuid>
Via API
# As admin
curl -s -X POST "http://localhost:3000/api/v1/generators/${GEN_ID}/rotate-pak" \
-H "Authorization: <admin-pak>" | jq .
# As the generator itself
curl -s -X POST "http://localhost:3000/api/v1/generators/${GEN_ID}/rotate-pak" \
-H "Authorization: <generator-pak>" | jq .
Updating CI/CD After Rotation
Update the stored secret in your CI/CD system:
GitHub Actions:
- Go to Settings → Secrets and variables → Actions
- Update the
BROKKR_GENERATOR_PAKsecret with the new value
GitLab CI:
- Go to Settings → CI/CD → Variables
- Update the
BROKKR_GENERATOR_PAKvariable
Cache Considerations After CLI Rotation
API-based rotation automatically invalidates the auth cache. CLI-based rotation operates directly on the database, so the old PAK may still work for up to 60 seconds (the default broker.auth_cache_ttl_seconds). To force immediate invalidation after a CLI rotation:
curl -s -X POST "http://localhost:3000/api/v1/admin/config/reload" \
-H "Authorization: <admin-pak>"
Verifying Rotation via Audit Logs
All PAK operations are recorded. Query PAK-related audit events:
curl -s "http://localhost:3000/api/v1/admin/audit-logs?action=pak.*" \
-H "Authorization: <admin-pak>" | jq .
See the Audit Logs Reference for the full list of audit event types.
Related Documentation
- Security Model — authentication and authorization architecture
- Audit Logs Reference — audit event format and querying
- Configuration Guide — PAK and auth cache settings
How-To: Setting Up Multi-Tenant Isolation
This guide walks through configuring Brokkr for multi-tenant operation using PostgreSQL schema isolation. Each tenant gets a fully isolated dataset while sharing a single database server.
Goal
Set up two tenants (acme and globex) on a shared PostgreSQL instance, each with their own broker instance and complete data isolation.
Prerequisites
- A PostgreSQL server accessible to both broker instances
- The
brokkruser with permission to create schemas - Helm (for Kubernetes deployment) or direct access to run broker binaries
Step 1: Prepare the Database
Create the shared database if it doesn’t exist:
CREATE DATABASE brokkr;
CREATE USER brokkr WITH PASSWORD 'your-secure-password';
GRANT ALL PRIVILEGES ON DATABASE brokkr TO brokkr;
-- Grant schema creation permission
GRANT CREATE ON DATABASE brokkr TO brokkr;
You don’t need to create the schemas manually — Brokkr creates them on first startup.
Step 2: Deploy Tenant Broker Instances
Option A: Helm (Kubernetes)
Deploy each tenant as a separate Helm release:
# Tenant: Acme
helm install brokkr-acme oci://ghcr.io/colliery-io/charts/brokkr-broker \
--namespace brokkr-acme --create-namespace \
--set postgresql.enabled=false \
--set postgresql.external.host=postgres.example.com \
--set postgresql.external.port=5432 \
--set postgresql.external.database=brokkr \
--set postgresql.external.username=brokkr \
--set postgresql.external.password=your-secure-password \
--set postgresql.external.schema=tenant_acme
# Tenant: Globex
helm install brokkr-globex oci://ghcr.io/colliery-io/charts/brokkr-broker \
--namespace brokkr-globex --create-namespace \
--set postgresql.enabled=false \
--set postgresql.external.host=postgres.example.com \
--set postgresql.external.port=5432 \
--set postgresql.external.database=brokkr \
--set postgresql.external.username=brokkr \
--set postgresql.external.password=your-secure-password \
--set postgresql.external.schema=tenant_globex
Option B: Environment Variables (Direct)
Run each broker with different schema settings:
# Terminal 1: Acme broker (port 3000)
BROKKR__DATABASE__URL=postgres://brokkr:password@postgres.example.com:5432/brokkr \
BROKKR__DATABASE__SCHEMA=tenant_acme \
BROKKR__LOG__LEVEL=info \
brokkr-broker serve
# Terminal 2: Globex broker (port 3001 - change bind port in config)
BROKKR__DATABASE__URL=postgres://brokkr:password@postgres.example.com:5432/brokkr \
BROKKR__DATABASE__SCHEMA=tenant_globex \
BROKKR__LOG__LEVEL=info \
brokkr-broker serve
Option C: Configuration Files
Create a config file per tenant:
# /etc/brokkr/acme.toml
[database]
url = "postgres://brokkr:password@postgres.example.com:5432/brokkr"
schema = "tenant_acme"
[log]
level = "info"
format = "json"
# /etc/brokkr/globex.toml
[database]
url = "postgres://brokkr:password@postgres.example.com:5432/brokkr"
schema = "tenant_globex"
[log]
level = "info"
format = "json"
BROKKR_CONFIG_FILE=/etc/brokkr/acme.toml brokkr-broker serve
BROKKR_CONFIG_FILE=/etc/brokkr/globex.toml brokkr-broker serve
Step 3: First Startup
On first startup, each broker instance:
- Creates the schema (
CREATE SCHEMA IF NOT EXISTS tenant_acme) - Runs all database migrations within the schema
- Creates the admin role and generates an admin PAK
- Logs the admin PAK to stdout
Capture the admin PAK for each tenant — it’s only shown once:
# Look for this line in the logs
# INFO Admin PAK: brokkr_BR...
Step 4: Register Agents Per Tenant
Each tenant’s agents connect to their tenant’s broker instance:
# Create agent for Acme tenant
curl -s -X POST http://acme-broker:3000/api/v1/agents \
-H "Authorization: <acme-admin-pak>" \
-H "Content-Type: application/json" \
-d '{"name": "acme-prod", "cluster_name": "us-east-1"}'
# Create agent for Globex tenant
curl -s -X POST http://globex-broker:3000/api/v1/agents \
-H "Authorization: <globex-admin-pak>" \
-H "Content-Type: application/json" \
-d '{"name": "globex-prod", "cluster_name": "eu-west-1"}'
Step 5: Deploy Tenant Agents
Point each agent at the correct tenant broker:
# Acme agent
helm install brokkr-agent-acme oci://ghcr.io/colliery-io/charts/brokkr-agent \
--namespace brokkr-acme \
--set broker.url=http://brokkr-acme-brokkr-broker:3000 \
--set broker.pak="<acme-agent-pak>" \
--set broker.agentName=acme-prod \
--set broker.clusterName=us-east-1
# Globex agent
helm install brokkr-agent-globex oci://ghcr.io/colliery-io/charts/brokkr-agent \
--namespace brokkr-globex \
--set broker.url=http://brokkr-globex-brokkr-broker:3000 \
--set broker.pak="<globex-agent-pak>" \
--set broker.agentName=globex-prod \
--set broker.clusterName=eu-west-1
Step 6: Verify Isolation
Confirm that each tenant only sees its own data:
# Acme sees only Acme agents
curl -s http://acme-broker:3000/api/v1/agents \
-H "Authorization: <acme-admin-pak>" | jq '.[].name'
# Output: "acme-prod"
# Globex sees only Globex agents
curl -s http://globex-broker:3000/api/v1/agents \
-H "Authorization: <globex-admin-pak>" | jq '.[].name'
# Output: "globex-prod"
Acme’s admin PAK does not work against Globex’s broker, and vice versa.
Connection Pool Sizing
Each broker instance uses a connection pool (default: 50 connections). With multiple tenants on one database, the total connections across all broker instances must stay under PostgreSQL’s max_connections (default: 100). Increase it or reduce per-tenant pool sizes for many tenants. See Multi-Tenancy Reference for detailed capacity planning.
Schema Naming
Use a consistent pattern like tenant_{name} (e.g., tenant_acme). Schema names allow only alphanumeric characters and underscores, max 63 characters. See Multi-Tenancy Reference for full constraints.
Related Documentation
- Multi-Tenancy Reference — data isolation details and limitations
- Configuration Guide — database configuration
- Installation Guide — deployment options
How-To: Querying Audit Logs
Brokkr maintains an immutable audit trail of all significant operations. This guide shows how to query audit logs to investigate events, track changes, and monitor security.
Audit Log API
All audit log queries go through the admin API:
GET /api/v1/admin/audit-logs
Auth: Admin only.
Basic Query
List the most recent audit events:
curl -s "http://localhost:3000/api/v1/admin/audit-logs" \
-H "Authorization: <admin-pak>" | jq .
Default: returns the 100 most recent entries, ordered by timestamp (newest first). The response is a JSON object with a logs array, plus total, count, limit, and offset fields for pagination.
Filtering
By Actor Type
See all actions performed by agents:
curl -s "http://localhost:3000/api/v1/admin/audit-logs?actor_type=agent" \
-H "Authorization: <admin-pak>" | jq '.logs[] | {action, resource_type, timestamp}'
Valid actor types: admin, agent, generator, system
By Action
Find all agent creation events:
curl -s "http://localhost:3000/api/v1/admin/audit-logs?action=agent.created" \
-H "Authorization: <admin-pak>" | jq .
Actions support wildcard prefix matching. To see all agent-related events:
curl -s "http://localhost:3000/api/v1/admin/audit-logs?action=agent.*" \
-H "Authorization: <admin-pak>" | jq .
By Resource
Track all changes to a specific agent:
curl -s "http://localhost:3000/api/v1/admin/audit-logs?resource_type=agent&resource_id=${AGENT_ID}" \
-H "Authorization: <admin-pak>" | jq .
By Time Range
Query events within a specific window:
curl -s "http://localhost:3000/api/v1/admin/audit-logs?from=2025-01-15T00:00:00Z&to=2025-01-16T00:00:00Z" \
-H "Authorization: <admin-pak>" | jq .
By Actor ID
See everything a specific generator has done:
curl -s "http://localhost:3000/api/v1/admin/audit-logs?actor_type=generator&actor_id=${GEN_ID}" \
-H "Authorization: <admin-pak>" | jq .
Pagination
Use limit and offset for large result sets:
# First page
curl -s "http://localhost:3000/api/v1/admin/audit-logs?limit=50&offset=0" \
-H "Authorization: <admin-pak>" | jq .
# Second page
curl -s "http://localhost:3000/api/v1/admin/audit-logs?limit=50&offset=50" \
-H "Authorization: <admin-pak>" | jq .
Maximum limit is 1000.
Common Investigation Patterns
Who Changed This Agent?
curl -s "http://localhost:3000/api/v1/admin/audit-logs?resource_type=agent&resource_id=${AGENT_ID}" \
-H "Authorization: <admin-pak>" \
| jq '.logs[] | {actor_type, actor_id, action, timestamp, details}'
Failed Authentication Attempts
curl -s "http://localhost:3000/api/v1/admin/audit-logs?action=auth.failed" \
-H "Authorization: <admin-pak>" \
| jq '.logs[] | {timestamp, ip_address, user_agent, details}'
Recent PAK Rotations
curl -s "http://localhost:3000/api/v1/admin/audit-logs?action=pak.rotated" \
-H "Authorization: <admin-pak>" \
| jq '.logs[] | {actor_type, resource_type, resource_id, timestamp}'
All Admin Actions Today
TODAY=$(date -u +%Y-%m-%dT00:00:00Z)
curl -s "http://localhost:3000/api/v1/admin/audit-logs?actor_type=admin&from=${TODAY}" \
-H "Authorization: <admin-pak>" | jq .
Webhook Configuration Changes
curl -s "http://localhost:3000/api/v1/admin/audit-logs?action=webhook.*" \
-H "Authorization: <admin-pak>" \
| jq '.logs[] | {action, resource_id, timestamp, details}'
Stack Deletion History
curl -s "http://localhost:3000/api/v1/admin/audit-logs?action=stack.deleted" \
-H "Authorization: <admin-pak>" \
| jq '.logs[] | {actor_type, actor_id, resource_id, timestamp}'
Audit Event Types
Actions follow the pattern resource.verb (e.g., agent.created, pak.rotated, auth.failed). You can use wildcard queries like action=agent.* to match all events for a resource type.
For the complete list of all audit event types, see the Audit Logs Reference.
Retention
Audit logs are retained for 90 days by default (broker.audit_log_retention_days). The broker runs a daily cleanup task to purge older entries.
To change retention:
BROKKR__BROKER__AUDIT_LOG_RETENTION_DAYS=365
Related Documentation
- Audit Logs Reference — schema and data model details
- Security Model — authentication and authorization
- Managing PAKs — PAK rotation and security
Explanation
In-depth discussion of Brokkr’s architecture, design decisions, and internal workings. These documents help you understand why Brokkr works the way it does.
Core Concepts
What is Brokkr?
Brokkr is an environment-aware control plane for dynamically distributing Kubernetes objects. Think of it as a smart traffic controller for your Kubernetes resources—it knows not just what to deploy, but where and when to deploy it based on your environment’s specific needs and policies.
graph LR
subgraph "Control Plane"
UA[User/Admin] -->|Creates/Updates| BR[Broker]
end
subgraph "Agents"
AG[Agent]
end
subgraph "Kubernetes Clusters"
KC[K8s Cluster]
end
AG -- Fetches Target State --> BR
AG -- Reports Status --> BR
AG -- Applies --> KC
Note: This diagram shows a single agent and cluster for clarity. In real deployments, Brokkr supports multiple agents and clusters, each following the same pattern.
Key Components
The Broker: The Source of Truth
The Broker serves as the central source of truth for Brokkr. It records the desired state of your applications and environments while providing APIs for users and agents to interact with this state. Importantly, the broker does not directly control clusters or push deployments. Instead, it maintains the authoritative record of what should exist and lets agents pull that information on their own schedule.
When users create or update resources, the broker records these changes and makes them available via its REST API. It handles authentication and authorization for all requests, ensuring that agents and generators can only access resources they’re permitted to see. As agents report back their activities, the broker records these events to maintain a complete audit trail of what has happened across your infrastructure.
The Agent: The Executor
Agents are the workhorses that make Brokkr’s desired state a reality in your Kubernetes clusters. Each agent runs within a specific environment, typically a single Kubernetes cluster, and takes full responsibility for that environment’s alignment with the broker’s desired state.
On a regular polling interval, agents contact the broker to fetch their target state—the deployment objects they should apply. They then validate these resources locally, checking YAML syntax and ensuring the resources make sense for their environment. After validation, agents apply the resources to their local Kubernetes cluster and report the results back to the broker.
This pull-based model has important advantages. Agents in restricted networks or behind firewalls can still receive deployments by initiating outbound connections to the broker. The model also provides natural resilience; if an agent goes offline temporarily, it simply catches up on missed changes when it reconnects.
Internal Data Architecture
Brokkr’s data model tracks what should be deployed, where, and by whom, while maintaining a clear audit trail of what has actually occurred. Understanding these entities helps you work effectively with the system.
Stacks
A Stack is a collection of related Kubernetes objects managed as a unit. Stacks provide the organizational boundary for grouping resources that belong together—perhaps all the components of a microservice, or all the infrastructure for a particular application. Beyond this grouping, Brokkr imposes no particular structure or semantics on stacks.
Deployment Objects
A Deployment Object is a versioned snapshot of all Kubernetes resources in a Stack at a particular point in time. Each time you update a Stack, Brokkr creates a new Deployment Object capturing that desired state. These objects are immutable once created, providing a complete historical record of changes. This immutability means you can always see exactly what was deployed at any point in the past.
Agents
An Agent represents a Brokkr process running in a specific environment. Agents have unique identities, authentication credentials, and metadata describing their capabilities and characteristics. The broker tracks which agents are registered, their current status, and their assignment to various stacks.
Agent Targets
An Agent Target connects an Agent to a Stack, defining which agents are responsible for managing which stacks. This mapping layer allows Brokkr to distribute workloads across multiple clusters and environments. A single stack might be targeted by multiple agents (for multi-cluster deployments), and a single agent might be responsible for multiple stacks.
Agent Events
Agent Events record the outcome of each attempt to apply a Deployment Object. When an agent applies resources and reports back to the broker, that report becomes an event in the system’s history. Events capture both successes and failures, providing an audit trail that’s essential for troubleshooting and compliance requirements.
Targeting Mechanisms
Brokkr provides flexible mechanisms for associating agents with stacks, allowing you to model a variety of deployment scenarios.
Direct Assignment offers the simplest approach: explicitly associate an agent with a stack by their IDs. This works well when you have a clear one-to-one mapping between agents and the stacks they should manage.
Label-Based Targeting enables dynamic, scalable associations. Both agents and stacks can carry labels, and you can configure stacks to target all agents with matching labels. This supports patterns like “all production agents should receive all production stacks” without maintaining explicit associations for each pair.
Annotation-Based Targeting extends the label concept with key-value pairs that can encode more complex matching rules. Annotations are useful when targeting logic requires more nuance than simple label presence—for example, targeting agents in a specific region or with particular capabilities.
| Targeting Method | Example Use Case |
|---|---|
| Direct Assignment | Agent A manages Stack X specifically |
| Label-Based | All “prod” agents manage all “prod” stacks |
| Annotation-Based | Agents with region=us-east manage stacks with region=us-east |
How These Pieces Fit Together
The data entities connect to form a complete deployment workflow. Users create Stacks to group their Kubernetes resources. Each Stack accumulates Deployment Objects as its contents change over time. Agents register with the broker and are assigned responsibility for one or more Stacks via Agent Targets.
When an Agent polls the broker, it receives the latest Deployment Objects for its assigned Stacks. The Agent validates and applies these resources to its Kubernetes cluster, then reports the outcome as Agent Events. This cycle repeats continuously, keeping all clusters aligned with the desired state recorded in the broker.
erDiagram
STACK ||--o{ DEPLOYMENT_OBJECT : has
AGENT ||--o{ AGENT_TARGET : assigned_to
STACK ||--o{ AGENT_TARGET : targeted_by
DEPLOYMENT_OBJECT ||--o{ AGENT_EVENT : triggers
AGENT ||--o{ AGENT_EVENT : reports
This architecture provides a clear, auditable, and scalable foundation for managing Kubernetes resources across many environments.
The Deployment Journey
The deployment process follows a pull-based model where agents take responsibility for fetching, validating, and applying their assigned target state. The broker maintains the source of truth and records events but never pushes deployments or performs environment-specific validation.
The journey begins when a user creates or updates a stack, which results in a new deployment object being created in the broker. Each agent then polls the broker on its regular interval, receiving the latest deployment objects for its assigned stacks. The agent validates these locally—checking YAML syntax, resource constraints, and any environment-specific rules—before applying the resources to its Kubernetes cluster.
After applying resources, the agent reports the outcome back to the broker as an event. Whether the application succeeded or failed, this information becomes part of the permanent audit trail. Over time, the broker accumulates a complete history of every deployment attempt across all your environments.
sequenceDiagram
participant User
participant Broker
participant Agent
participant Cluster
User->>Broker: Create/Update Stack (creates Deployment Object)
loop Every polling interval
Agent->>Broker: Fetch Target State (Deployment Objects)
Broker-->>Agent: Return Deployment Objects
Agent->>Agent: Validate & Apply Resources
Agent->>Cluster: Apply Resources
Cluster-->>Agent: Result
Agent->>Broker: Report Event (Success/Failure)
end
Security Model
Brokkr uses API key authentication and role-based authorization for all API access. Every request must include a valid PAK (Prefixed API Key) in the Authorization header.
Authentication
The system supports three types of PAKs, each granting different levels of access. Admin PAKs provide full administrative access to all API endpoints and resources. Agent PAKs grant access only to endpoints and data relevant to a specific agent, such as fetching target state and reporting events. Generator PAKs allow external systems to create resources within their designated scope.
When a request arrives, the API middleware extracts the PAK from the Authorization header and verifies it against stored hashes. If the PAK matches a known admin, agent, or generator, the request proceeds with that identity and role attached. Invalid or missing PAKs result in authentication failures.
Authorization
Beyond authentication, Brokkr enforces role-based access control at every endpoint. Certain operations require admin privileges: creating agents, listing all resources, managing system configuration. Agent endpoints ensure that each agent can only access its own target state and report its own events. Generator endpoints similarly restrict access to each generator’s own resources.
The system also enforces row-based access control within endpoints. After authenticating a request, the API verifies that the requesting entity has permission to access each specific resource. An agent fetching deployment objects receives only those for stacks it’s assigned to. A generator creating a stack can only access stacks it created. This fine-grained control ensures that even authenticated entities can only see and modify what they’re supposed to.
sequenceDiagram
participant Client
participant API
participant DB
Client->>API: Request (with PAK)
API->>API: Authenticate PAK
API->>API: Determine role/identity
API->>DB: Query resource (with access check)
alt Access allowed
DB-->>API: Resource data
API-->>Client: Success/Resource
else Access denied
API-->>Client: Forbidden/Unauthorized
end
Key Management
PAKs are generated using secure random generation and stored as hashes in the database. The actual PAK value is shown only once at creation or rotation time, so it must be captured and stored securely at that moment. Both agents and generators can rotate their own PAKs, and administrators can rotate any PAK in the system.
Next Steps
With an understanding of Brokkr’s core concepts, you can explore further:
- Follow the Quick Start Guide to deploy your first application
- Study the Technical Architecture for implementation details
- Explore the Data Model to understand entity relationships
- Read the Security Model for comprehensive authentication and authorization details
Technical Architecture
Brokkr is a deployment orchestration platform designed to manage Kubernetes resources across multiple clusters from a central control plane. This document provides a comprehensive technical overview of Brokkr’s architecture, explaining how its components are implemented, how they interact, and the design decisions that shape its behavior.
System Overview
C4Context
title Brokkr System Context (C4 Level 1)
Person(engineer, "Platform Engineer", "Manages deployments and configuration")
System_Ext(cicd, "CI/CD Pipeline", "Automated deployment source (Generator)")
System(brokkr, "Brokkr", "Deployment orchestration platform for multi-cluster Kubernetes")
System_Ext(k8s, "Kubernetes Clusters", "Target clusters where resources are deployed")
SystemDb(pg, "PostgreSQL", "Persistent state storage")
System_Ext(webhooks, "External Systems", "Receive event notifications via webhooks")
Rel(engineer, brokkr, "Manages stacks and deployments", "HTTPS REST API")
Rel(cicd, brokkr, "Creates stacks and deployment objects", "HTTPS REST API")
Rel(brokkr, pg, "Stores state", "SQL")
Rel(brokkr, k8s, "Deploys resources via agents", "HTTPS")
Rel(brokkr, webhooks, "Sends event notifications", "HTTPS")
At its core, Brokkr follows a hub-and-spoke architecture where a central broker service coordinates deployments across multiple agent instances. The broker maintains the desired state of all deployments in a PostgreSQL database, while agents running in target Kubernetes clusters continuously poll the broker to fetch their assigned work and reconcile the actual cluster state to match the desired state.
C4Container
title Brokkr Container Diagram (C4 Level 2)
Person(engineer, "Platform Engineer", "Manages deployments")
System_Ext(cicd, "CI/CD Pipeline", "Generator identity")
Container_Boundary(brokkr, "Brokkr Platform") {
Container(broker, "Broker", "Rust, Axum", "REST API, background tasks, webhook dispatch")
Container(agent, "Agent", "Rust, kube-rs", "Polls broker, reconciles cluster state")
ContainerDb(db, "PostgreSQL", "PostgreSQL 15+", "Stacks, deployment objects, events, audit logs")
}
System_Ext(k8s, "Kubernetes API", "Target cluster")
System_Ext(webhooks, "Webhook Endpoints", "External notification receivers")
Rel(engineer, broker, "Manages", "HTTPS REST API")
Rel(cicd, broker, "Deploys", "HTTPS REST API")
Rel(agent, broker, "Polls for state, reports events", "HTTPS REST")
Rel(broker, db, "Reads/writes state", "SQL :5432")
Rel(agent, k8s, "Applies/deletes resources", "HTTPS :6443")
Rel(broker, webhooks, "Delivers events", "HTTPS")
This pull-based model was chosen deliberately over a push-based approach for several reasons. First, it simplifies network topology since agents only need outbound connectivity to the broker rather than requiring the broker to reach into potentially firewalled cluster networks. Second, it provides natural resilience since agents can continue operating with their cached state during temporary broker unavailability. Third, it allows agents to control their own reconciliation pace based on their cluster’s capabilities and load.
Broker Service Architecture
The broker is implemented as an asynchronous Rust service built on the Axum web framework with the Tokio runtime providing the async execution environment. When the broker starts, it initializes several interconnected subsystems that work together to provide the complete platform functionality.
Initialization Sequence
The broker follows a carefully ordered startup sequence to ensure all dependencies are properly initialized before accepting requests. First, the configuration is loaded from environment variables and configuration files, establishing database connection parameters, authentication settings, and operational parameters like polling intervals.
Next, a PostgreSQL connection pool is established using the r2d2 connection manager with Diesel as the ORM layer. The pool is configured with a default of 50 connections to accommodate background tasks, middleware, and concurrent request handling. If multi-tenant mode is enabled, the broker sets up the appropriate PostgreSQL schema to isolate tenant data.
After the database connection is established, Diesel migrations run automatically to ensure the schema is up to date. The broker then checks the app_initialization table to determine if this is a first-time startup. On first run, it creates the admin role and an associated admin generator, writing the initial admin PAK (Prefixed API Key) to a temporary file for retrieval.
With the database ready, the broker initializes its runtime subsystems in sequence: the Data Access Layer (DAL), the encryption subsystem for webhook secrets, the event emission system for webhook dispatch, the audit logger for compliance tracking, and finally the five background task workers. If a configuration file is specified, it also starts a filesystem watcher for hot-reload capability.
API Layer
The API layer exposes a RESTful interface under the /api/v1 prefix, with all endpoints documented via OpenAPI annotations that generate Swagger documentation. Every request to an API endpoint passes through authentication middleware that extracts and validates the PAK from the Authorization header.
The authentication middleware performs verification against three possible identity types in order: first checking the admin role table, then the agents table, and finally the generators table. This lookup is optimized with partial database indexes on the pak_hash column that exclude soft-deleted records, enabling O(1) lookup performance rather than full table scans.
Upon successful authentication, the middleware injects an AuthPayload structure into the request context containing the authenticated identity’s type and ID. Individual endpoint handlers then perform authorization checks against this payload to determine if the caller has permission for the requested operation.
C4Component
title Broker Request Processing (C4 Component)
Person(client, "API Client", "Admin, Agent, or Generator")
Component(auth, "Auth Middleware", "Axum Middleware", "Extracts PAK, verifies against DB, injects AuthPayload")
Component(handler, "Route Handler", "Axum Handler", "Authorization check and business logic")
Component(dal, "Data Access Layer", "Diesel ORM", "Type-safe database operations via accessor types")
ComponentDb(pool, "Connection Pool", "r2d2", "Manages PostgreSQL connections (default: 50)")
ComponentDb(db, "PostgreSQL", "Database", "Persistent storage")
Rel(client, auth, "HTTP Request", "Authorization: Bearer {PAK}")
Rel(auth, handler, "Authenticated request", "AuthPayload injected")
Rel(handler, dal, "Data operations", "Type-safe queries")
Rel(dal, pool, "Get connection", "")
Rel(pool, db, "SQL", "TCP :5432")
The API is organized into resource-specific modules: agents, generators, stacks, deployment objects, templates, work orders, webhooks, diagnostics, health status, and admin operations. Each module defines its routes, request/response structures, and authorization requirements.
Data Access Layer
The DAL provides a clean abstraction over database operations, exposing specialized accessor types for each entity in the system. Rather than having a monolithic data access object, the DAL struct provides factory methods that return purpose-built accessor types: dal.agents() returns an AgentsDAL, dal.stacks() returns a StacksDAL, and so on for all twenty-two entity types in the system.
Each accessor type implements the standard CRUD operations appropriate for its entity, along with any specialized queries needed. For example, the AgentsDAL provides not only basic create, get, update, and delete operations, but also get_by_pak_hash for authentication lookups and filtered listing methods that support complex queries combining label and annotation filters.
The DAL uses Diesel’s query builder for type-safe SQL generation, ensuring that queries are checked at compile time rather than failing at runtime with SQL syntax errors. All operations use the connection pool, automatically returning connections when operations complete.
Soft deletion is implemented throughout the system using a deleted_at timestamp column. When an entity is “deleted,” this timestamp is set rather than removing the row, preserving the audit trail and enabling potential recovery. Most queries automatically filter out soft-deleted records.
Event Emission
The event emission system provides a database-centric approach to webhook dispatch. Rather than using an in-memory pub/sub bus, events are directly matched against webhook subscriptions and inserted into the webhook_deliveries table for processing by the webhook delivery worker.
When an event occurs (such as a deployment being applied or a work order completing), the emit_event() function queries the database for all webhook subscriptions whose event type pattern matches the event. Pattern matching supports exact matches like workorder.completed, wildcard suffixes like deployment.* that match any deployment-related event, and a catch-all * pattern that matches everything. For each matching subscription, a webhook delivery record is created in PENDING status for the webhook delivery worker to process.
Background Tasks
The broker runs five concurrent background tasks that handle various maintenance and processing duties:
Diagnostic Cleanup Task runs on a configurable interval (default: every 15 minutes) to expire pending diagnostic requests that have exceeded their timeout and delete completed, expired, or failed diagnostic records older than the configured maximum age (default: 1 hour). This prevents the database from accumulating stale diagnostic data.
Work Order Maintenance Task runs frequently (default: every 10 seconds) to handle work order lifecycle transitions. It moves work orders from RETRY_PENDING status back to PENDING when their backoff period has elapsed, allowing them to be claimed again. It also reclaims work orders that were claimed but never completed within the timeout period, returning them to PENDING for another agent to attempt.
Webhook Delivery Worker runs on a short interval (default: every 5 seconds) to process pending webhook deliveries. It fetches a batch of pending deliveries (default: 50), retrieves and decrypts the webhook URL and authentication header for each subscription, performs an HTTP POST with a 30-second timeout, and records the result. Failed deliveries are scheduled for retry with exponential backoff until they exceed the maximum retry count, at which point they are marked as dead.
Webhook Cleanup Task runs less frequently (default: hourly) to delete completed and dead webhook deliveries older than the retention period (default: 7 days), preventing unbounded growth of delivery history.
Audit Log Cleanup Task runs daily to delete audit log entries older than the configured retention period (default: 90 days), balancing compliance requirements against storage costs.
Audit Logger
The audit logger provides asynchronous, batched recording of security-relevant events for compliance and forensic purposes. Like the event bus, it is implemented as a singleton with an internal mpsc channel and background writer task.
Rather than writing each audit entry immediately to the database, which would create significant write amplification, the logger buffers entries and writes them in batches. The writer task accumulates entries up to a configurable batch size (default: 100) or until a flush interval elapses (default: 1 second), whichever comes first. This approach dramatically reduces database write pressure while maintaining a nearly real-time audit trail.
Audit entries capture the actor type (admin, agent, generator, or system), the actor’s ID, the action performed, the resource type affected, the resource’s ID, and additional contextual details. This information enables security teams to reconstruct the sequence of events leading to any system state.
Configuration Hot-Reload
When a configuration file is specified via the BROKKR_CONFIG_FILE environment variable, the broker can automatically detect and apply configuration changes without requiring a pod restart. The config watcher uses the notify crate to monitor the configuration file’s parent directory for filesystem events, detecting modifications through OS-level file watching.
When a change is detected, the watcher applies a configurable debounce period (default: 5 seconds, configurable via BROKKR_CONFIG_WATCHER_DEBOUNCE_SECONDS) to coalesce rapid successive changes into a single reload operation. The watcher can be disabled via BROKKR_CONFIG_WATCHER_ENABLED=false. Only certain configuration values support hot-reload: log level, diagnostic cleanup intervals, webhook delivery settings, and CORS configuration. Settings that affect initialization, like database connection parameters or TLS configuration, require a pod restart.
The admin API also exposes a manual reload endpoint at POST /api/v1/admin/config/reload that triggers an immediate configuration reload, useful when changes need to take effect immediately.
Agent Service Architecture
The agent is a Kubernetes-native component that runs inside target clusters, responsible for applying deployment objects to the cluster and reporting status back to the broker. It is designed to be resilient, continuing to operate with cached state during broker unavailability.
Startup and Initialization
When the agent starts, it first loads its configuration, which must include the broker URL, the agent’s name and cluster name, and its PAK for authentication. It then enters a readiness loop, repeatedly checking the broker’s health endpoint until it receives a successful response or exceeds its retry limit.
Once the broker is reachable, the agent verifies its PAK by calling the authentication endpoint. A successful response confirms the agent is properly registered and authorized. The agent then fetches its full details from the broker, including its assigned labels and annotations that determine which deployments it will receive.
With authentication confirmed, the agent initializes its Kubernetes client using in-cluster configuration (when running as a pod) or a specified kubeconfig path (for development). It validates connectivity by fetching the cluster’s API server version.
Finally, the agent starts an HTTP server on port 8080 for health checks and metrics, then enters its main control loop.
Main Control Loop
The agent’s control loop is implemented using Tokio’s select! macro to concurrently await multiple timer-based tasks. This design allows multiple activities to proceed in parallel while ensuring orderly shutdown when a termination signal is received.
C4Component
title Agent Internal Architecture (C4 Component)
Container_Boundary(agent, "Brokkr Agent") {
Component(loop, "Control Loop", "Tokio select!", "Concurrent timer-based task dispatcher")
Component(heartbeat, "Heartbeat", "Timer Task", "Sends periodic heartbeat to broker")
Component(deployer, "Deployment Checker", "Timer Task", "Polls for deployment objects and reconciles")
Component(workorder, "Work Order Processor", "Timer Task", "Claims and executes work orders")
Component(healthcheck, "Health Monitor", "Timer Task", "Evaluates deployment health status")
Component(diagnostics, "Diagnostics Handler", "Timer Task", "Processes on-demand diagnostic requests")
Component(reconciler, "Reconciliation Engine", "kube-rs", "Server-side apply with ordering and ownership")
}
System_Ext(broker, "Broker API", "Central control plane")
System_Ext(k8s, "Kubernetes API", "Target cluster")
Rel(loop, heartbeat, "Triggers")
Rel(loop, deployer, "Triggers")
Rel(loop, workorder, "Triggers")
Rel(loop, healthcheck, "Triggers")
Rel(loop, diagnostics, "Triggers")
Rel(deployer, reconciler, "Delegates apply/delete")
Rel(heartbeat, broker, "POST heartbeat", "HTTPS")
Rel(deployer, broker, "GET target-state", "HTTPS")
Rel(reconciler, k8s, "Apply/Delete resources", "HTTPS :6443")
Rel(healthcheck, k8s, "Query pod status", "HTTPS :6443")
Heartbeat Timer fires at a configurable interval (derived from the polling interval) to send a heartbeat to the broker, maintaining the agent’s “alive” status. The response includes updated agent details, allowing the broker to push configuration changes like label updates.
Deployment Check Timer fires at the configured polling interval to fetch the agent’s target state from the broker. The agent compares this desired state against what it has previously applied and performs reconciliation to converge them.
Work Order Timer polls for pending work orders assigned to the agent. Work orders represent transient operations like container image builds that don’t fit the declarative deployment model.
Health Check Timer (when enabled) periodically evaluates the health of deployments the agent has applied, checking pod status, container states, and conditions to produce health summaries reported back to the broker.
Diagnostics Timer polls for on-demand diagnostic requests, which allow administrators to collect debugging information from specific deployments.
Reconciliation Engine
The reconciliation engine is the heart of the agent, responsible for applying Kubernetes resources to achieve the desired state while handling the complexities of resource dependencies, conflicts, and failures.
When the agent receives deployment objects from the broker, it first parses the YAML content into Kubernetes DynamicObject instances. The objects are then sorted to apply cluster-scoped resources (Namespaces and CustomResourceDefinitions) before namespace-scoped resources, ensuring dependencies exist before resources that need them.
Before applying each resource, the agent injects standard metadata labels that link the resource back to Brokkr: the stack ID, a checksum of the YAML content for change detection, the deployment object ID, and the agent’s ID as the owner. This metadata enables the agent to identify which resources it manages and detect changes.
Application uses Kubernetes server-side apply with field ownership, which handles merge conflicts more gracefully than client-side apply. Before applying in earnest, resources are validated with a dry-run request to catch schema errors or policy violations early.
When deployment objects are removed (indicated by deletion markers), the agent deletes the corresponding resources from the cluster. However, it only deletes resources it owns, verified by checking the owner ID label. This prevents accidental deletion of resources created by other systems.
The reconciliation engine implements exponential backoff retry logic for transient failures like rate limiting (HTTP 429) or temporary server errors (5xx). Non-retryable errors like authorization failures (403) or resource not found (404) fail immediately to avoid wasting retry attempts.
Work Order Processing
Work orders handle operations that don’t fit the declarative resource model, such as building container images. The agent polls for pending work orders, claims one for exclusive processing, executes it, and reports the result.
For build work orders, the agent parses the YAML to extract Shipwright Build resources, applies them to the cluster, creates a BuildRun to trigger execution, and monitors the BuildRun status until completion. Build progress is checked every 5 seconds with a 15-minute overall timeout. On success, the agent reports the resulting image digest; on failure, it reports the error reason.
The work order system includes retry logic: when a work order fails with a retryable error, it is scheduled for retry with exponential backoff. Non-retryable errors are marked as permanently failed.
Health Monitoring
When deployment health monitoring is enabled, the agent periodically evaluates the health of resources it has applied. For each tracked deployment object, it queries for pods matching the deployment object ID label and analyzes their status.
The health checker examines pod phase, conditions, and container states to produce a health assessment. It specifically looks for problematic conditions like ImagePullBackOff, CrashLoopBackOff, OOMKilled, and various container creation errors. Based on its analysis, it assigns one of four health statuses: healthy (all pods running and ready), degraded (issues detected but not failed), failing (pod in Failed phase), or unknown (unable to determine status).
Health summaries include the count of ready versus total pods, a list of detected issues, and detailed resource information. This data is reported to the broker, which stores it for display in management interfaces and can trigger webhook notifications based on health changes.
Component Interaction Patterns
Understanding how the broker and agents interact is essential for operating and troubleshooting Brokkr deployments.
Deployment Flow
The deployment lifecycle begins when an administrator or CI/CD system (acting as a generator) creates a stack and adds deployment objects to it. The stack serves as a logical grouping with labels that determine which agents will receive its deployments.
When a deployment object is created, the broker’s matching engine evaluates all registered agents to find those whose labels satisfy the stack’s targeting requirements. For each matching agent, it creates an agent target record linking the agent to the stack.
sequenceDiagram
participant Admin as Administrator
participant Broker as Broker
participant DB as PostgreSQL
participant Agent as Agent
participant K8s as Kubernetes
Admin->>Broker: Create Stack with Labels
Broker->>DB: Store Stack
Admin->>Broker: Create Deployment Object
Broker->>DB: Store Deployment Object
Broker->>Broker: Find Matching Agents
Broker->>DB: Create Agent Targets
loop Every Polling Interval
Agent->>Broker: Request Target State
Broker->>DB: Query Agent's Deployments
Broker-->>Agent: Deployment Objects
Agent->>Agent: Compare Desired vs Actual
Agent->>K8s: Apply Resources
K8s-->>Agent: Result
Agent->>Broker: Report Event
Broker->>DB: Store Event
Broker->>Broker: Dispatch Webhooks
end
On the agent side, the deployment check timer fires and requests the agent’s target state from the broker. The broker returns all deployment objects from stacks the agent is targeting, along with sequence IDs that enable incremental synchronization. The agent compares this desired state against what it has applied, performs the necessary create, update, or delete operations in Kubernetes, and reports events back to the broker.
Event and Webhook Flow
Events flow from agents through the broker to external systems via webhooks. When an agent reports an event (success, failure, health change), the broker stores it in the database and emits it through the event emission system.
The event emitter queries the database for matching webhook subscriptions. For each match, it creates a webhook delivery record. The webhook delivery worker picks up these records and attempts HTTP delivery to the configured endpoints.
This decoupled architecture ensures that webhook delivery proceeds asynchronously with its own retry logic, separate from the initial event recording.
Performance Characteristics
The broker is designed to handle substantial load with modest resources. The API layer uses Axum’s async request handling to efficiently multiplex many concurrent connections onto a small number of OS threads. Connection pooling minimizes database connection overhead.
API Performance: Under typical conditions, API requests complete in under 50 milliseconds at the 95th percentile, with the broker capable of handling over 1,000 requests per second on a single instance.
Database Performance: Query latency is typically under 20 milliseconds, with indexed lookups for authentication completing in single-digit milliseconds. The connection pool defaults to 50 connections.
Agent Performance: Resource application completes in under 100 milliseconds per resource, with the reconciliation loop typically completing in under 1 second. Event reporting has sub-50ms latency to the broker.
For larger deployments, the broker can be horizontally scaled behind a load balancer since it maintains no in-memory state beyond caches—all persistent state lives in PostgreSQL.
Scaling Patterns
Horizontal Broker Scaling
The broker’s stateless design enables straightforward horizontal scaling. Multiple broker instances can run behind a load balancer, all connecting to the same PostgreSQL database. Each instance runs its own background tasks, but these are designed to be safe for concurrent execution through database-level coordination (e.g., work order claiming uses atomic updates to prevent double-claiming).
C4Container
title Horizontal Scaling Pattern (C4 Container)
System_Ext(lb, "Load Balancer", "Distributes API traffic")
Container_Boundary(brokers, "Broker Instances") {
Container(b1, "Broker 1", "Rust/Axum", "Stateless API + background tasks")
Container(b2, "Broker 2", "Rust/Axum", "Stateless API + background tasks")
Container(b3, "Broker 3", "Rust/Axum", "Stateless API + background tasks")
}
ContainerDb(db, "PostgreSQL Primary", "PostgreSQL", "All persistent state")
ContainerDb(dbr, "PostgreSQL Replica", "PostgreSQL", "Read replicas (optional)")
Rel(lb, b1, "Routes requests", "HTTPS")
Rel(lb, b2, "Routes requests", "HTTPS")
Rel(lb, b3, "Routes requests", "HTTPS")
Rel(b1, db, "Read/Write", "SQL :5432")
Rel(b2, db, "Read/Write", "SQL :5432")
Rel(b3, db, "Read/Write", "SQL :5432")
Rel(db, dbr, "Replication", "Streaming")
Agent Deployment
Each Kubernetes cluster runs one agent instance. The agent handles all deployments for that cluster, with no need for multiple agents per cluster. Agents operate independently with no inter-agent communication, simplifying the operational model.
For very large clusters or high deployment volumes, the agent’s polling interval can be tuned to balance responsiveness against API load.
Resource Requirements
| Component | CPU Request | Memory Request | CPU Limit | Memory Limit |
|---|---|---|---|---|
| Broker | 100m | 256Mi | 500m | 512Mi |
| Agent | 50m | 128Mi | 200m | 256Mi |
| PostgreSQL | 250m | 256Mi | 500m | 512Mi |
These are conservative defaults suitable for small to medium deployments. Production deployments handling thousands of deployment objects or hundreds of agents should increase these limits based on observed resource utilization.
Multi-Tenancy
The broker supports multi-tenant deployments through PostgreSQL schema isolation. When configured with a schema name, the broker creates a dedicated schema and sets the connection’s search_path to use it for all queries. This provides data isolation between tenants sharing a PostgreSQL instance without requiring separate databases.
Schema names are validated to prevent SQL injection, accepting only alphanumeric characters and underscores, and requiring the name to start with a letter.
Component Implementation Details
This document provides detailed technical implementation information about each component in the Brokkr system. Understanding these implementation details helps operators debug issues, extend functionality, and optimize performance.
Broker Components
The broker is implemented in Rust using the Axum web framework with Tokio as the async runtime. It uses Diesel ORM with r2d2 connection pooling for PostgreSQL database access.
API Module
The API module implements the broker’s RESTful interface using Axum’s router and middleware patterns. Routes are organized hierarchically with authentication applied uniformly through middleware.
Route Organization
Routes are defined in submodules and merged into a unified router with authentication middleware applied:
#![allow(unused)]
fn main() {
pub fn routes(dal: DAL, cors_config: &Cors, reloadable_config: Option<ReloadableConfig>) -> Router<DAL> {
let cors = build_cors_layer(cors_config);
let api_routes = Router::new()
.merge(agent_events::routes())
.merge(agents::routes())
.merge(stacks::routes())
.merge(webhooks::routes())
.merge(work_orders::routes())
// Additional route modules...
.layer(from_fn_with_state(dal.clone(), middleware::auth_middleware))
.layer(cors);
Router::new().nest("/api/v1", api_routes)
}
}
The authentication middleware intercepts every request, extracts the PAK from the Authorization header, verifies it against the database, and attaches an AuthPayload to the request extensions. Handlers can then access the authenticated identity to make authorization decisions.
CORS Configuration
CORS is configured dynamically based on settings, supporting three modes: allow all origins when "*" is specified, restrict to specific origins otherwise, with configurable methods, headers, and preflight cache duration.
#![allow(unused)]
fn main() {
pub struct Cors {
pub allowed_origins: Vec<String>,
pub allowed_methods: Vec<String>,
pub allowed_headers: Vec<String>,
pub max_age_seconds: u64,
}
}
Debugging
Enable debug logging with environment variables:
RUST_LOG=debug- General debug outputRUST_LOG=brokkr_broker=trace- Detailed broker tracingRUST_LOG=tower_http=debug- HTTP layer debugging
DAL (Data Access Layer) Module
The Data Access Layer provides structured access to the PostgreSQL database using Diesel ORM with r2d2 connection pooling. Each entity type has a dedicated accessor class that encapsulates all database operations.
Implementation Architecture
The DAL uses Diesel’s compile-time query checking and type-safe schema definitions. Connection management uses r2d2’s pooling with automatic connection recycling:
#![allow(unused)]
fn main() {
pub struct DAL {
pool: Pool<ConnectionManager<PgConnection>>,
schema: Option<String>,
}
impl DAL {
pub fn agents(&self) -> AgentsDAL {
AgentsDAL::new(self.pool.clone(), self.schema.clone())
}
pub fn stacks(&self) -> StacksDAL {
StacksDAL::new(self.pool.clone(), self.schema.clone())
}
// Additional accessors...
}
}
Each accessor obtains a connection from the pool, optionally sets the PostgreSQL search path for multi-tenant schema isolation, executes the query, and returns the connection to the pool.
Multi-Tenant Schema Support
The DAL supports PostgreSQL schema isolation for multi-tenant deployments. When a schema is configured, every connection sets search_path before executing queries:
#![allow(unused)]
fn main() {
if let Some(schema) = &self.schema {
diesel::sql_query(format!("SET search_path TO {}", schema))
.execute(&mut conn)?;
}
}
Schema names are validated to prevent SQL injection before use.
Error Handling
Database errors are wrapped in a unified DalError type:
#![allow(unused)]
fn main() {
pub enum DalError {
ConnectionPool(r2d2::Error),
Query(diesel::result::Error),
NotFound,
}
}
This provides consistent error handling across all database operations while preserving the underlying error details for debugging.
CLI Module
The CLI module handles command-line argument parsing, configuration loading, and service initialization. It supports running database migrations, starting the broker server, and administrative operations.
Configuration is loaded from environment variables using the BROKKR__ prefix with double underscore separators for nesting:
BROKKR__DATABASE__URL=postgres://user:pass@localhost/brokkr
BROKKR__DATABASE__SCHEMA=tenant_a
BROKKR__LOG__LEVEL=info
BROKKR__BROKER__WEBHOOK_DELIVERY_INTERVAL_SECONDS=5
Background Tasks Module
The broker runs several background tasks for maintenance operations:
Diagnostic Cleanup runs every 15 minutes (configurable) to remove diagnostic results older than 1 hour (configurable).
Work Order Maintenance runs every 10 seconds to process retry scheduling and detect stale claims.
Webhook Delivery runs every 5 seconds (configurable) to process pending webhook deliveries in batches of 50 (configurable).
Webhook Cleanup runs hourly to remove delivery records older than 7 days (configurable).
Audit Log Cleanup runs daily to remove audit entries older than 90 days (configurable).
Utils Module
The utils module provides shared functionality:
Event Emission provides database-centric webhook dispatch by matching events against subscriptions and inserting delivery records directly.
Audit Logger provides non-blocking audit logging with batched database writes (100 entries or 1 second flush).
Encryption implements AES-256-GCM encryption for webhook secrets with versioned format for algorithm upgrades.
PAK Controller generates and verifies Prefixed API Keys using SHA-256 hashing with indexed lookups.
Agent Components
The agent is implemented in Rust using Tokio for async operations and kube-rs for Kubernetes API interaction. It runs a continuous control loop that polls the broker and reconciles cluster state.
Broker Communication Module
The broker module handles all communication with the Brokkr Broker service using REST API calls with PAK authentication. The agent polls the broker at configurable intervals rather than maintaining persistent connections.
Communication Pattern
All broker communication uses HTTP requests with Bearer token authentication:
#![allow(unused)]
fn main() {
pub async fn fetch_deployment_objects(
config: &Settings,
client: &Client,
agent: &Agent,
) -> Result<Vec<DeploymentObject>, Error> {
let url = format!(
"{}/api/v1/agents/{}/target-state",
config.agent.broker_url, agent.id
);
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.send()
.await?;
response.json().await
}
}
Key Endpoints
The agent communicates with these broker endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/api/v1/auth/pak | POST | Verify PAK and retrieve agent identity |
/api/v1/agents/{id}/target-state | GET | Fetch deployment objects to apply |
/api/v1/agents/{id}/events | POST | Report deployment outcomes |
/api/v1/agents/{id}/heartbeat | POST | Send periodic heartbeat |
/api/v1/agents/{id}/health-status | PATCH | Report deployment health |
/api/v1/agents/{id}/work-orders/pending | GET | Fetch claimable work orders |
/api/v1/agents/{id}/diagnostics/pending | GET | Fetch diagnostic requests |
Retry Logic
Failed broker requests use exponential backoff with configurable parameters:
#![allow(unused)]
fn main() {
pub struct Agent {
pub max_retries: u32,
pub event_message_retry_delay: u64,
// ...
}
}
Kubernetes Module
The Kubernetes module manages all interactions with the Kubernetes API using the kube-rs client library. It implements server-side apply for resource management with intelligent ordering and ownership tracking.
Server-Side Apply
Resources are applied using Kubernetes server-side apply, which provides declarative management with conflict detection:
#![allow(unused)]
fn main() {
pub async fn apply_k8s_objects(
k8s_objects: &[DynamicObject],
k8s_client: Client,
patch_params: PatchParams,
) -> Result<(), Error> {
for obj in k8s_objects {
let api = get_api_for_object(&k8s_client, obj)?;
let patch = Patch::Apply(obj);
api.patch(&obj.name_any(), &patch_params, &patch).await?;
}
Ok(())
}
}
Resource Ordering
Resources are applied in priority order to respect dependencies:
- Namespaces are applied first as other resources may depend on them
- CustomResourceDefinitions are applied second as custom resources require their definitions
- All other resources are applied after dependencies exist
This ordering prevents failures from missing dependencies during initial deployment.
Ownership Tracking
The agent tracks resource ownership using a combination of labels and annotations:
#![allow(unused)]
fn main() {
// Labels (used for selection and filtering)
pub static STACK_LABEL: &str = "k8s.brokkr.io/stack";
pub static DEPLOYMENT_OBJECT_ID_LABEL: &str = "brokkr.io/deployment-object-id";
// Annotations (used for metadata)
pub static CHECKSUM_ANNOTATION: &str = "k8s.brokkr.io/deployment-checksum";
pub static LAST_CONFIG_ANNOTATION: &str = "k8s.brokkr.io/last-config-applied";
pub static BROKKR_AGENT_OWNER_ANNOTATION: &str = "brokkr.io/owner-id";
}
Before deleting resources, the agent verifies ownership by checking the owner annotation to prevent removing resources managed by other systems. The checksum annotation enables detection of configuration drift.
Reconciliation
Full reconciliation applies the desired state and prunes resources that no longer belong:
#![allow(unused)]
fn main() {
pub async fn reconcile_target_state(
objects: &[DynamicObject],
client: Client,
stack_id: &str,
checksum: &str,
) -> Result<(), Error> {
// Apply priority objects first
apply_priority_objects(&objects, &client).await?;
// Validate remaining objects
validate_objects(&objects)?;
// Apply all resources
apply_all_objects(&objects, &client).await?;
// Prune old resources with mismatched checksums
prune_old_resources(&client, stack_id, checksum).await?;
Ok(())
}
}
Error Handling
Kubernetes operations use retry logic for transient failures:
#![allow(unused)]
fn main() {
// Retryable HTTP status codes
const RETRYABLE_CODES: [u16; 4] = [429, 500, 503, 504];
// Retryable error reasons
const RETRYABLE_REASONS: [&str; 3] = [
"ServiceUnavailable",
"InternalError",
"Timeout",
];
}
Exponential backoff prevents overwhelming a recovering API server.
Health Module
The health module exposes HTTP endpoints for Kubernetes probes and Prometheus metrics:
| Endpoint | Purpose |
|---|---|
/healthz | Liveness probe - returns 200 if process is alive |
/readyz | Readiness probe - returns 200 if agent can serve traffic |
/health | Detailed health status with JSON response |
/metrics | Prometheus metrics in text exposition format |
The health server runs on a configurable port (default: 8080) separately from the main control loop.
CLI Module
The agent CLI handles configuration loading and service initialization. Configuration uses the same environment variable pattern as the broker:
BROKKR__AGENT__BROKER_URL=https://broker.example.com:3000
BROKKR__AGENT__PAK=brokkr_BR...
BROKKR__AGENT__AGENT_NAME=production-cluster
BROKKR__AGENT__CLUSTER_NAME=prod-us-east-1
BROKKR__AGENT__POLLING_INTERVAL=10
BROKKR__AGENT__HEALTH_PORT=8080
Configuration Reference
Broker Configuration
#![allow(unused)]
fn main() {
pub struct Settings {
pub database: Database,
pub log: Log,
pub broker: Broker,
pub cors: Cors,
pub telemetry: Telemetry,
}
pub struct Database {
pub url: String,
pub schema: Option<String>,
}
pub struct Broker {
pub diagnostic_cleanup_interval_seconds: Option<u64>, // default: 900
pub diagnostic_max_age_hours: Option<i64>, // default: 1
pub webhook_encryption_key: Option<String>,
pub webhook_delivery_interval_seconds: Option<u64>, // default: 5
pub webhook_delivery_batch_size: Option<i64>, // default: 50
pub webhook_cleanup_retention_days: Option<i64>, // default: 7
pub audit_log_retention_days: Option<i64>, // default: 90
}
pub struct Cors {
pub allowed_origins: Vec<String>,
pub allowed_methods: Vec<String>,
pub allowed_headers: Vec<String>,
pub max_age_seconds: u64,
}
}
Agent Configuration
#![allow(unused)]
fn main() {
pub struct Settings {
pub agent: Agent,
pub log: Log,
pub telemetry: Telemetry,
}
pub struct Agent {
pub broker_url: String,
pub pak: String,
pub agent_name: String,
pub cluster_name: String,
pub polling_interval: u64, // default: 10
pub kubeconfig_path: Option<String>,
pub max_retries: u32,
pub max_event_message_retries: usize,
pub event_message_retry_delay: u64,
pub health_port: Option<u16>, // default: 8080
pub deployment_health_enabled: Option<bool>, // default: true
pub deployment_health_interval: Option<u64>, // default: 60
}
}
Hot-Reload Configuration
The broker supports dynamic configuration reloading for certain settings:
Hot-reloadable (apply without restart):
- Log level
- CORS settings (origins, methods, headers, max-age)
- Webhook delivery interval and batch size
- Diagnostic cleanup settings
Static (require restart):
- Database URL and schema
- Webhook encryption key
- PAK configuration
- Telemetry settings
Trigger a manual reload via the admin API:
curl -X POST https://broker/api/v1/admin/config/reload \
-H "Authorization: Bearer <admin-pak>"
When a configuration file is specified, the broker automatically watches it for filesystem changes with a 5-second debounce.
Component Lifecycle
Broker Startup Sequence
- Configuration Loading - Parse environment variables and configuration files
- Database Connection - Establish r2d2 connection pool to PostgreSQL
- Migration Check - Verify database schema is current
- Encryption Initialization - Load or generate webhook encryption key
- Event Bus Initialization - Start event dispatcher with mpsc channel
- Audit Logger Initialization - Start background writer with batching
- Background Tasks - Spawn diagnostic cleanup, work order, webhook, and audit tasks
- API Server - Bind to configured port and start accepting requests
Agent Startup Sequence
- Configuration Loading - Parse environment variables
- PAK Verification - Authenticate with broker and retrieve agent identity
- Kubernetes Client - Initialize kube-rs client with in-cluster or kubeconfig credentials
- Health Server - Start HTTP server for probes and metrics
- Control Loop - Enter main loop with polling, health checks, and work order processing
Graceful Shutdown
Both components handle SIGTERM and SIGINT for graceful shutdown:
- Stop accepting new requests
- Complete in-flight operations
- Flush pending data (audit logs, events)
- Close database connections
- Exit cleanly
Performance Considerations
Broker Optimization
Connection Pooling - r2d2 maintains a pool of database connections, avoiding connection establishment overhead for each request.
Batched Writes - Audit logs and webhook deliveries are batched to reduce database round trips.
Indexed Lookups - PAK verification uses indexed columns for O(1) authentication performance.
Async I/O - Tokio provides non-blocking I/O for high concurrency without thread-per-request overhead.
Agent Optimization
Incremental Fetching - Agents track processed sequence IDs to fetch only new deployment objects.
Parallel Apply - Independent resources can be applied concurrently within priority groups.
Connection Reuse - HTTP client maintains connection pools to the broker and Kubernetes API.
Efficient Diffing - Checksum-based change detection avoids unnecessary applies for unchanged resources.
Data Model Design
This document explains the design decisions and architectural philosophy behind Brokkr’s data model.
Entity Relationship Overview
classDiagram
class stacks
class agents
class deployment_objects
class agent_events
class agent_targets
class stack_labels
class stack_annotations
class agent_labels
class agent_annotations
class generators
class stack_templates
class work_orders
generators "1" -- "0..*" stacks : owns
stacks "1" -- "0..*" deployment_objects : contains
stacks "1" -- "0..*" agent_targets : targeted by
agents "1" -- "0..*" agent_events : reports
agents "1" -- "0..*" agent_targets : targets
deployment_objects "1" -- "0..*" agent_events : triggers
stacks "1" -- "0..*" stack_labels : has
stacks "1" -- "0..*" stack_annotations : has
agents "1" -- "0..*" agent_labels : has
agents "1" -- "0..*" agent_annotations : has
generators "1" -- "0..*" stack_templates : owns
stacks "1" -- "0..*" work_orders : targets
Design Philosophy
Immutability of Deployment Objects
Deployment objects are immutable after creation (except for soft deletion). This design decision ensures:
- Audit Trail: Every deployment can be traced back to its exact configuration
- Rollback Capability: Previous configurations are always available
- Consistency: No accidental modifications to deployed resources
Soft Deletion Strategy
All primary entities support soft deletion via deleted_at timestamps. This approach provides:
- Recovery: Accidentally deleted items can be restored
- Referential Integrity: Related data remains intact
- Historical Analysis: Past configurations and relationships are preserved
- Compliance: Audit requirements are met without data loss
Cascading Operations
The system implements intelligent cascading for both soft and hard deletes:
Soft Delete Cascades
- Generator → Stacks and Deployment Objects
- Stack → Deployment Objects (with deletion marker)
- Agent → Agent Events
Hard Delete Cascades
- Stack → Agent Targets, Agent Events, Deployment Objects
- Agent → Agent Targets, Agent Events
- Generator → (handled by foreign key constraints)
Key Architectural Decisions
Why Generators?
Generators represent external systems that create stacks and deployment objects. This abstraction:
- Provides authentication boundaries for automated systems
- Tracks which system created which resources
- Enables rate limiting and access control per generator
- Maintains audit trail of automated deployments
Why Agent Targets?
The many-to-many relationship between agents and stacks enables:
- Flexible deployment topologies
- Multi-cluster deployments
- Gradual rollouts
- Environment-specific targeting
Labels vs Annotations
Labels (single values):
- Used for selection and filtering
- Simple categorization
- Fast queries
Annotations (key-value pairs):
- Rich metadata
- Configuration hints
- Integration with external systems
Trigger Behavior
Stack Deletion Flow
sequenceDiagram
participant User
participant DB
Note over User,DB: Soft Delete
User->>DB: UPDATE stacks SET deleted_at = NOW()
DB->>DB: Soft delete all deployment objects
DB->>DB: Insert deletion marker object
Note over User,DB: Hard Delete
User->>DB: DELETE FROM stacks
DB->>DB: Delete agent_targets
DB->>DB: Delete agent_events
DB->>DB: Delete deployment_objects
Deployment Object Protection
Deployment objects cannot be modified except for:
- Setting
deleted_at(soft delete) - Updating deletion markers
This is enforced by database triggers to ensure immutability.
Performance Considerations
Indexing Strategy
Key indexes are created on:
- Foreign keys for join performance
deleted_atfor filtering active records- Unique constraints for data integrity
- Frequently queried fields (name, status)
Sequence IDs
Deployment objects use BIGSERIAL sequence_id for:
- Guaranteed ordering
- Efficient pagination
- Conflict-free concurrent inserts
Migration Strategy
The data model is managed through versioned SQL migrations in crates/brokkr-models/migrations/. Each migration:
- Is idempotent
- Includes both up and down scripts
- Is tested in CI/CD pipeline
For detailed field definitions and constraints, refer to the API documentation or the source code in crates/brokkr-models/.
Network Flows
Understanding the network architecture of a distributed system is essential for proper deployment, security hardening, and troubleshooting. This document provides a comprehensive analysis of the network traffic patterns between Brokkr components, including detailed port and protocol specifications, firewall requirements, and Kubernetes NetworkPolicy configurations.
Network Topology
Brokkr implements a hub-and-spoke network topology where the broker acts as the central coordination point. All agents initiate outbound connections to the broker—there are no inbound connections required to agents. This pull-based model simplifies firewall configuration and enables agents to operate behind NAT without special accommodations.
C4Context
title Brokkr Network Topology (C4 System Context)
Person(admin, "Platform Engineer", "Manages deployments and configuration")
System_Ext(generator, "Generator / CI Pipeline", "Automated deployment source")
System_Ext(webhook, "Webhook Endpoints", "External notification receivers")
System_Ext(otlp, "OTLP Collector", "Distributed tracing (optional)")
Enterprise_Boundary(broker_cluster, "Broker Cluster") {
System_Ext(ingress, "Ingress Controller", "TLS termination")
System(broker, "Broker Service", "Central API on :3000")
SystemDb(db, "PostgreSQL", "Persistent state on :5432")
}
Enterprise_Boundary(target_cluster, "Target Cluster(s)") {
System(agent, "Brokkr Agent", "Cluster-local operator")
System_Ext(k8sapi, "Kubernetes API", "Cluster API on :6443")
}
Rel(admin, ingress, "Manages", "HTTPS :443")
Rel(generator, ingress, "Deploys", "HTTPS :443")
Rel(ingress, broker, "Forwards", "HTTP :3000")
Rel(broker, db, "Queries", "TCP :5432")
Rel(broker, webhook, "Delivers events", "HTTPS :443")
Rel(broker, otlp, "Traces", "gRPC :4317")
Rel(agent, broker, "Polls & reports", "HTTPS :3000")
Rel(agent, k8sapi, "Applies resources", "HTTPS :6443")
Rel(agent, otlp, "Traces", "gRPC :4317")
The diagram above illustrates the three primary network zones in a typical Brokkr deployment. External traffic from administrators and generators enters through an ingress controller, which terminates TLS and forwards requests to the broker service. The broker maintains persistent connectivity to its PostgreSQL database and sends outbound webhook deliveries to configured external endpoints. Meanwhile, agents in target clusters poll the broker for deployment instructions and interact with their local Kubernetes API servers to apply resources.
Connection Specifications
Complete Connection Matrix
The following table enumerates every network connection in the Brokkr system, including the source and destination components, ports, protocols, and whether each connection is required for basic operation.
| Source | Destination | Port | Protocol | Direction | Required | Purpose |
|---|---|---|---|---|---|---|
| Admin/UI | Broker | 3000 | HTTPS | Inbound | Yes | API access, management operations |
| Generator | Broker | 3000 | HTTPS | Inbound | Yes | Stack and deployment object creation |
| Agent | Broker | 3000 | HTTPS | Outbound | Yes | Fetch deployments, report events |
| Broker | PostgreSQL | 5432 | TCP | Internal | Yes | Database operations |
| Agent | K8s API | 6443 | HTTPS | Local | Yes | Resource management |
| Broker | Webhook endpoints | 443 | HTTPS | Outbound | Optional | Event notifications |
| Prometheus | Broker | 3000 | HTTP | Inbound | Optional | Metrics scraping at /metrics |
| Prometheus | Agent | 8080 | HTTP | Inbound | Optional | Metrics scraping at /metrics |
| Broker | OTLP Collector | 4317 | gRPC | Outbound | Optional | Distributed tracing |
| Agent | OTLP Collector | 4317 | gRPC | Outbound | Optional | Distributed tracing |
Port Assignments
Brokkr uses a small number of well-defined ports. The broker service listens on port 3000 for all API traffic, including agent communication, administrator operations, and generator requests. This single-port design simplifies ingress configuration and firewall rules. Agents expose a health and metrics server on port 8080, which serves the /healthz, /readyz, /health, and /metrics endpoints used by Kubernetes liveness probes and Prometheus scraping.
The PostgreSQL database uses the standard port 5432. When deploying the bundled PostgreSQL instance via the Helm chart, this connection remains internal to the broker cluster. External PostgreSQL deployments may use different ports, which can be configured via the postgresql.external.port value.
OpenTelemetry tracing, when enabled, uses gRPC on port 4317 to communicate with OTLP collectors. This optional integration provides distributed tracing capabilities for debugging and performance analysis.
Broker Network Requirements
Inbound Traffic
The broker service accepts all inbound traffic on a single port, simplifying both service exposure and network policy configuration. The default configuration exposes port 3000, though this is rarely accessed directly in production. Instead, an ingress controller typically terminates TLS and forwards traffic to the broker.
The broker service supports three exposure methods through its Helm chart:
ClusterIP is the default service type, restricting access to within the Kubernetes cluster. This configuration is appropriate when agents run in the same cluster as the broker or when an ingress controller handles external access.
LoadBalancer creates a cloud provider load balancer that exposes the service directly to external traffic. While simpler to configure than ingress, this approach requires managing TLS termination separately and may incur additional cloud provider costs.
Ingress (recommended for production) delegates external access and TLS termination to a Kubernetes ingress controller. This approach integrates with cert-manager for automatic certificate management and provides flexible routing options.
Outbound Traffic
The broker initiates three types of outbound connections. Database connectivity to PostgreSQL is essential—the broker cannot operate without it. The Helm chart supports both bundled PostgreSQL (deployed as a subchart) and external PostgreSQL instances. For bundled deployments, the connection uses internal cluster DNS (brokkr-broker-postgresql:5432). External databases are configured via the postgresql.external values or by providing a complete connection URL through postgresql.existingSecret.
Webhook delivery represents the second outbound connection type. When webhooks are configured, the broker dispatches event notifications to external HTTP/HTTPS endpoints. The webhook delivery worker processes deliveries in batches, with the batch size and interval configurable via broker.webhookDeliveryBatchSize (default: 50) and broker.webhookDeliveryIntervalSeconds (default: 5). Failed deliveries are retried with exponential backoff.
OpenTelemetry tracing, when enabled, establishes gRPC connections to an OTLP collector. The collector endpoint is configured via telemetry.otlpEndpoint, and the sampling rate via telemetry.samplingRate. The Helm chart optionally deploys an OTel collector sidecar for environments where the main collector is not directly accessible.
Database Connectivity
The broker uses Diesel ORM with an r2d2 connection pool for PostgreSQL connectivity. Connection strings follow the standard PostgreSQL URI format:
postgres://user:password@host:5432/database
For production deployments, TLS should be enabled by appending ?sslmode=require or stronger modes to the connection string. The broker supports multi-tenant deployments through PostgreSQL schema isolation—the postgresql.external.schema value specifies which schema to use for data storage.
Agent Network Requirements
Outbound-Only Architecture
Agents operate with an outbound-only network model. They initiate all connections and require no inbound ports for their primary function. This design enables agents to operate behind restrictive firewalls and NAT gateways without special configuration—a critical feature for edge deployments and air-gapped environments.
The agent’s network requirements are minimal: connectivity to the broker API and the local Kubernetes API server. When metrics scraping is enabled, the agent also accepts inbound connections from Prometheus on port 8080.
| Destination | Port | Protocol | Purpose |
|---|---|---|---|
| Broker API | 3000 | HTTPS | Fetch deployments, report events |
| Kubernetes API | 6443 | HTTPS | Manage cluster resources |
| OTLP Collector | 4317 | gRPC | Telemetry (optional) |
Kubernetes API Access
Agents communicate with their local Kubernetes API server to apply and manage resources. When deployed via the Helm chart, agents use in-cluster configuration automatically—the Kubernetes client discovers the API server address from the cluster’s DNS and service account credentials.
The Helm chart creates RBAC resources that grant agents permission to manage resources across the cluster (when rbac.clusterWide: true) or within specific namespaces. The agent requires broad resource access for deployment management but can be restricted from sensitive resources like Secrets through the rbac.secretAccess configuration.
Broker Connectivity
Agents poll the broker at a configurable interval (default: 10 seconds, set via agent.pollingInterval). Each polling cycle fetches pending deployment objects and reports events for completed operations. The agent also sends deployment health status updates at a separate interval (default: 60 seconds, set via agent.deploymentHealth.intervalSeconds).
The broker URL is configured via the broker.url value in the agent’s Helm chart. For deployments where the agent and broker share a cluster, an internal URL like http://brokkr-broker:3000 provides optimal performance. For multi-cluster deployments, agents use the broker’s external URL with TLS: https://broker.example.com.
Authentication uses Prefixed API Keys (PAKs), which agents include in the Authorization header of every request. The PAK is generated when an agent is registered and should be provided via broker.pak in the Helm values or through a Kubernetes Secret.
TLS Configuration
Broker TLS Options
The broker supports three TLS configuration approaches, each suited to different deployment scenarios.
Ingress TLS Termination is the recommended approach for most production deployments. TLS terminates at the ingress controller, and internal traffic between the ingress and broker uses plain HTTP. This approach centralizes certificate management and integrates smoothly with cert-manager:
ingress:
enabled: true
className: 'nginx'
tls:
- secretName: brokkr-tls
hosts:
- broker.example.com
Direct TLS on Broker enables TLS termination at the broker itself, useful for deployments without an ingress controller or when end-to-end encryption is required. Enable via tls.enabled: true and provide certificates either through tls.existingSecret or inline via tls.cert and tls.key.
Cert-Manager Integration automates certificate provisioning when combined with ingress TLS. The Helm chart can configure cert-manager annotations to automatically request and renew certificates:
tls:
enabled: true
certManager:
enabled: true
issuer: 'letsencrypt-prod'
issuerKind: 'ClusterIssuer'
Agent-to-Broker TLS
Agents should always communicate with the broker over HTTPS in production. The agent validates the broker’s TLS certificate using the system’s trusted certificate authorities. For deployments with self-signed or private CA certificates, the CA must be added to the agent’s trust store or mounted as a volume.
Kubernetes NetworkPolicy Configuration
NetworkPolicies provide defense-in-depth by restricting pod-to-pod and pod-to-external communication at the network layer. Both the broker and agent Helm charts include optional NetworkPolicy resources that implement least-privilege network access.
Broker NetworkPolicy
The broker’s NetworkPolicy allows inbound connections from configured sources and outbound connections to the database and webhook destinations. When networkPolicy.enabled: true, the generated policy includes:
Ingress rules permit connections on port 3000 from pods matching the selectors specified in networkPolicy.allowIngressFrom. If no selectors are specified, the policy allows connections from any pod in the same namespace. Metrics scraping can be separately controlled via networkPolicy.allowMetricsFrom.
Egress rules permit DNS resolution (UDP/TCP port 53), database connectivity (port 5432 to PostgreSQL pods or external IPs), and optionally webhook delivery (HTTPS port 443 to external IPs, excluding private ranges). The networkPolicy.allowWebhookEgress value controls whether webhook egress is permitted.
networkPolicy:
enabled: true
allowIngressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
allowMetricsFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
allowWebhookEgress: true
Agent NetworkPolicy
The agent’s NetworkPolicy restricts traffic to essential destinations only. When enabled, the policy permits:
Egress to DNS (UDP/TCP port 53) for name resolution.
Egress to the Kubernetes API server (ports 443 and 6443). The networkPolicy.kubernetesApiCidr value controls which IP ranges can receive this traffic—in production, restrict this to the actual API server IP for maximum security.
Egress to the broker on the configured port (default 3000). The destination can be specified as a pod selector for same-cluster deployments or as an IP block for external brokers.
Ingress for metrics (port 8080) when metrics.enabled: true and networkPolicy.allowMetricsFrom specifies allowed scrapers.
networkPolicy:
enabled: true
kubernetesApiCidr: "10.0.0.1/32" # API server IP
brokerEndpoint:
podSelector:
matchLabels:
app.kubernetes.io/name: brokkr-broker
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: brokkr
allowMetricsFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
Firewall Configuration
Minimum Required Ports
Organizations deploying Brokkr must configure firewalls to permit the following traffic:
For the broker host:
| Direction | Port | Protocol | Source/Destination | Purpose |
|---|---|---|---|---|
| Inbound | 3000 (or 443 via ingress) | TCP | Agents, Admins, Generators | API access |
| Outbound | 5432 | TCP | PostgreSQL database | Database connectivity |
| Outbound | 443 | TCP | Webhook endpoints | Event delivery |
For the agent host:
| Direction | Port | Protocol | Source/Destination | Purpose |
|---|---|---|---|---|
| Outbound | 3000 or 443 | TCP | Broker | API communication |
| Outbound | 6443 | TCP | Kubernetes API server | Cluster management |
Cloud Provider Security Groups
Cloud deployments require security group configuration that permits the traffic flows described above. The following examples demonstrate typical configurations for major cloud providers.
AWS Security Groups:
For the broker, create an inbound rule permitting TCP port 3000 from the VPC CIDR (or specific agent security groups). Create outbound rules for PostgreSQL (port 5432 to the database security group) and webhook delivery (port 443 to 0.0.0.0/0 or specific webhook destinations).
GCP Firewall Rules:
Create an ingress rule with a target tag for broker instances, permitting TCP port 3000 from authorized sources. Create egress rules permitting port 5432 to the Cloud SQL instance and port 443 for webhooks.
Azure Network Security Groups:
Configure inbound rules for port 3000 from the virtual network address space. Configure outbound rules for database connectivity and webhook delivery similar to the AWS and GCP examples.
Troubleshooting Network Issues
Common Problems
Agent cannot reach broker: This typically manifests as repeated connection timeouts or DNS resolution failures in agent logs. Begin by verifying the broker URL is correct and resolvable from the agent pod. Use nslookup or dig to test DNS resolution. Check that no NetworkPolicy or firewall rule blocks egress on the broker port. For TLS connections, verify the agent trusts the broker’s certificate.
Broker cannot reach database: Database connectivity failures prevent the broker from starting. Verify the database host is resolvable and the credentials are correct. Check security group rules permit traffic on port 5432. For TLS-enabled database connections, verify the sslmode parameter is correctly configured.
Webhooks not being delivered: Check the broker logs for delivery errors, which indicate whether the issue is connection-related or response-related. Verify egress rules permit HTTPS traffic to external IPs. If NetworkPolicy is enabled, confirm allowWebhookEgress: true is set. Test webhook endpoint accessibility using curl from a pod in the broker namespace.
Metrics not being scraped: If Prometheus cannot reach the metrics endpoints, verify the ServiceMonitor is correctly configured and the Prometheus operator’s selector matches. Check that NetworkPolicy allows ingress from the monitoring namespace on the metrics port (3000 for broker, 8080 for agent).
Diagnostic Commands
The following commands help diagnose network connectivity issues:
# Test broker connectivity from agent pod
kubectl exec -it deploy/brokkr-agent -- wget -qO- http://brokkr-broker:3000/healthz
# Test database connectivity from broker pod
kubectl exec -it deploy/brokkr-broker -- nc -zv postgresql 5432
# List NetworkPolicies in the namespace
kubectl get networkpolicy -n brokkr
# Examine NetworkPolicy details
kubectl describe networkpolicy brokkr-broker -n brokkr
# Check agent logs for connection errors
kubectl logs deploy/brokkr-agent | grep -i "connection\|error\|timeout"
# Verify DNS resolution from within a pod
kubectl exec -it deploy/brokkr-agent -- nslookup brokkr-broker
Data Flows
This document traces the journey of data through the Brokkr system, from initial deployment creation through resource application in target clusters and event propagation to external systems. Understanding these flows is essential for debugging issues, optimizing performance, and building integrations with Brokkr.
Deployment Lifecycle
The deployment lifecycle encompasses the complete journey of a deployment object from creation through application on target clusters. This flow demonstrates Brokkr’s immutable, append-only data model and its approach to eventual consistency.
Creating a Deployment
Deployments begin their lifecycle when an administrator or generator creates a stack and submits deployment objects to it. The broker processes these submissions through several stages, ultimately targeting them to appropriate agents.
sequenceDiagram
participant Client as Admin/Generator
participant Broker as Broker API
participant DB as PostgreSQL
participant Match as Targeting Logic
Client->>Broker: POST /api/v1/stacks
Broker->>DB: INSERT stack
DB-->>Broker: Stack created
Broker-->>Client: Stack response (with ID)
Client->>Broker: POST /api/v1/stacks/{id}/labels
Broker->>DB: INSERT stack_labels
Note over Broker: Labels used for agent targeting
Client->>Broker: POST /api/v1/stacks/{id}/deployment-objects
Broker->>DB: INSERT deployment_object
Broker->>Match: Find matching agents
Match->>DB: Query agents by labels
DB-->>Match: Matching agents
Match->>DB: INSERT agent_targets
Broker-->>Client: Deployment object response
Note over DB: Deployment objects are immutable after creation
The broker assigns each deployment object a sequence ID upon creation, establishing a strict ordering that agents use to process updates in the correct sequence. This sequence ID is monotonically increasing within each stack, ensuring that newer deployment objects always have higher sequence IDs than older ones. The combination of stack ID and sequence ID provides a reliable mechanism for agents to track which objects they have already processed.
When a deployment object is created, the broker does not immediately push it to agents. Instead, the targeting logic creates entries in the agent_targets table that associate the stack with eligible agents. Agents discover these targets during their next polling cycle and fetch the relevant deployment objects.
Agent Reconciliation
Agents continuously poll the broker and reconcile their cluster state to match the desired state defined by deployment objects. The reconciliation loop runs at a configurable interval, defaulting to 10 seconds.
sequenceDiagram
participant Agent as Agent
participant Broker as Broker API
participant DB as PostgreSQL
participant K8s as Kubernetes API
participant Cluster as Cluster State
loop Every polling interval (default: 10s)
Agent->>Broker: GET /api/v1/agents/{id}/target-state
Broker->>DB: Query targeted objects for agent
DB-->>Broker: Deployment objects list
Broker-->>Agent: Deployment objects (with sequence IDs)
Agent->>Agent: Calculate diff (desired vs actual)
alt New objects to apply
Agent->>K8s: Apply resource (create/update)
K8s->>Cluster: Resource created/updated
K8s-->>Agent: Success/Failure
Agent->>Broker: POST /api/v1/agents/{id}/events
Broker->>DB: INSERT agent_event
end
alt Objects to delete (deletion markers)
Agent->>K8s: Delete resource
K8s->>Cluster: Resource deleted
K8s-->>Agent: Success/Failure
Agent->>Broker: POST /api/v1/agents/{id}/events
Broker->>DB: INSERT agent_event
end
Agent->>Agent: Update local state cache
end
The agent’s GET /api/v1/agents/{id}/target-state endpoint returns deployment objects the agent is responsible for, filtered to exclude objects already deployed (based on agent events). This optimization reduces payload size and processing time for agents managing large numbers of deployments.
During reconciliation, the agent uses Kubernetes server-side apply to create or update resources. This approach preserves fields managed by other controllers while allowing the agent to manage its own fields. The agent orders resource application to respect dependencies: Namespaces and CustomResourceDefinitions are applied before resources that depend on them.
After each successful operation, the agent reports an event to the broker. These events serve multiple purposes: they update the broker’s view of deployment state, they trigger webhook notifications to external systems, and they provide an audit trail of all operations.
Deployment Object States
Deployment objects follow an implicit lifecycle tracked through their presence, associated agent events, and deletion markers. The state model uses soft deletion to maintain a complete audit trail while supporting reliable cleanup.
stateDiagram-v2
[*] --> Created: POST deployment-object
Created --> Targeted: Agent matching
Targeted --> Applied: Agent applies
Applied --> Updated: New version created
Updated --> Applied: Agent applies update
Applied --> MarkedForDeletion: Deletion marker created
MarkedForDeletion --> Deleted: Agent deletes
Deleted --> [*]: Soft delete (retained)
Created indicates a deployment object exists in the database but has not yet been targeted to any agents. This state typically transitions quickly to Targeted as the broker processes agent matching.
Targeted means one or more agents are responsible for this deployment object. The agent_targets table records these associations, linking stacks to agents based on label matching.
Applied indicates the agent has successfully applied the resource to its cluster and reported an event confirming the operation. The agent event records the deployment object ID, timestamp, and outcome.
Updated represents a transitional state where a new deployment object with a higher sequence ID has been created for the same logical resource. The agent detects this during reconciliation and applies the update.
MarkedForDeletion occurs when a deletion marker deployment object is created. Deletion markers are deployment objects with a special flag indicating the agent should delete the referenced resource rather than apply it.
Deleted indicates the agent has removed the resource from the cluster. Both the original deployment object and the deletion marker remain in the database with deleted_at timestamps for audit purposes.
Deletion Flow
Deleting resources uses a marker pattern that ensures reliable cleanup even when agents are temporarily unavailable. Rather than immediately removing data, the broker creates a deletion marker that agents process during their normal reconciliation cycle.
sequenceDiagram
participant Client as Admin
participant Broker as Broker API
participant DB as PostgreSQL
participant Agent as Agent
participant K8s as Kubernetes
Client->>Broker: POST /api/v1/stacks/{id}/deployment-objects
Note over Client,Broker: is_deletion_marker: true
Broker->>DB: INSERT deployment_object (deletion marker)
Broker-->>Client: Deletion marker created
Note over Agent: Next polling interval
Agent->>Broker: GET /api/v1/agents/{id}/target-state
Broker-->>Agent: Includes deletion marker
Agent->>Agent: Detect deletion marker
Agent->>K8s: DELETE resource
K8s-->>Agent: Deleted
Agent->>Broker: POST /api/v1/agents/{id}/events
Note over Broker: Event type: DELETED
Note over DB: Both original and marker<br/>remain for audit trail
This approach has several advantages over immediate deletion. First, it provides reliable cleanup even when agents are offline—when they reconnect, they process accumulated deletion markers. Second, it maintains a complete history of what was deployed and when it was removed. Third, it allows for rollback by creating new deployment objects that restore deleted resources.
Event Flow
Events form the nervous system of Brokkr, propagating state changes from agents through the broker to external systems. The event system handles agent reports, webhook notifications, and audit logging through an asynchronous architecture designed for high throughput and reliability.
Event Architecture
The broker uses a database-centric approach to event emission. Rather than an in-memory pub/sub bus, events are directly matched against webhook subscriptions and inserted into the delivery queue. Audit logging operates independently through its own asynchronous channel.
flowchart LR
subgraph Agent
Apply[Apply Resource]
Report[Event Reporter]
end
subgraph Broker
API[API Handler]
DB[(Database)]
Emit[Event Emitter]
Webhook[Webhook Worker]
Audit[Audit Logger]
end
subgraph External
Endpoints[Webhook Endpoints]
Logs[Audit Logs]
end
Apply --> Report
Report -->|POST /agents/{id}/events| API
API --> DB
API --> Emit
Emit -->|Match subscriptions & insert deliveries| DB
Webhook -->|Poll pending deliveries| DB
Webhook --> Endpoints
API --> Audit
Audit --> Logs
When an event occurs, the emit_event() function queries the database for webhook subscriptions whose event type pattern matches the event. For each matching subscription, a delivery record is created in PENDING status. The webhook delivery worker then processes these records independently, ensuring webhook delivery doesn’t block API responses.
Audit logging uses a separate asynchronous channel with a 10,000-entry buffer. A background writer task batches entries (up to 100 per batch or every 1 second) for efficient database writes.
Agent Event Reporting
Agents report events to the broker after completing each operation. The POST /api/v1/agents/{id}/events endpoint accepts event data and persists it to the agent_events table.
| Event Type | Trigger | Data Included |
|---|---|---|
APPLIED | Resource successfully applied | Resource details, timestamp |
UPDATED | Resource successfully updated | Resource details, changes |
DELETED | Resource successfully deleted | Resource details |
FAILED | Operation failed | Error message, resource details |
HEALTH_CHECK | Periodic health status | Deployment health summary |
The agent includes comprehensive metadata with each event: the deployment object ID, resource GVK (Group/Version/Kind), namespace and name, operation result, and any error messages. This data enables precise tracking of deployment state and troubleshooting of failures.
Events are processed synchronously in the API handler—the database insert must succeed before the endpoint returns. However, downstream processing (webhook delivery, audit logging) happens asynchronously through the event bus.
Webhook Delivery
Webhook subscriptions enable external systems to receive notifications when events occur in Brokkr. The delivery system prioritizes reliability through persistent queuing and automatic retries, with two delivery modes for different network topologies.
Broker Delivery (Default)
When no target_labels are specified on a subscription, the broker delivers webhooks directly. This is suitable for external endpoints accessible from the broker’s network.
sequenceDiagram
participant EventBus as Event Bus
participant DB as PostgreSQL
participant Worker as Webhook Worker
participant Endpoint as External Endpoint
EventBus->>DB: Find matching subscriptions
DB-->>EventBus: Subscription list
EventBus->>DB: INSERT webhook_delivery (per subscription)
loop Every 5 seconds
Worker->>DB: SELECT pending deliveries (batch of 50)
DB-->>Worker: Delivery batch
par For each delivery
Worker->>DB: Get subscription details
Worker->>Worker: Decrypt URL and auth header
Worker->>Endpoint: POST event payload
alt Success (2xx)
Endpoint-->>Worker: 200 OK
Worker->>DB: Mark success
else Failure
Endpoint-->>Worker: Error
Worker->>DB: Schedule retry (exponential backoff)
end
end
end
The webhook worker runs as a background task, polling for pending deliveries every 5 seconds (configurable via broker.webhookDeliveryIntervalSeconds). Each polling cycle processes up to 50 deliveries (configurable via broker.webhookDeliveryBatchSize), enabling high throughput while controlling resource usage.
Agent Delivery
When target_labels are specified on a subscription, agents matching those labels deliver the webhooks. This enables webhooks to reach in-cluster endpoints (e.g., http://service.namespace.svc.cluster.local) that the broker cannot access due to network separation.
sequenceDiagram
participant EventBus as Event Bus
participant DB as PostgreSQL
participant Agent as Agent (matching labels)
participant Endpoint as In-Cluster Endpoint
EventBus->>DB: Find matching subscriptions
DB-->>EventBus: Subscription list
EventBus->>DB: INSERT webhook_delivery with target_labels
loop Every 10 seconds (agent heartbeat)
Agent->>DB: Fetch pending deliveries matching my labels
DB-->>Agent: Delivery batch
par For each delivery
Agent->>Agent: Decrypt URL and auth header
Agent->>Endpoint: POST event payload
alt Success (2xx)
Endpoint-->>Agent: 200 OK
Agent->>DB: Report success
else Failure
Endpoint-->>Agent: Error
Agent->>DB: Report failure (schedules retry)
end
end
end
Agent delivery requires the agent to have ALL labels specified in target_labels. For example, a subscription with target_labels: ["env:prod", "region:us"] will only be delivered by agents with both labels. This allows precise control over which agents handle which webhooks.
Encryption and Security
Delivery URLs and authentication headers are stored encrypted in the database using AES-256-GCM. The worker decrypts these values just before making the HTTP request, minimizing the time sensitive data exists in memory.
Retry Behavior
Failed deliveries are retried with exponential backoff. The first retry occurs after 2 seconds, the second after 4 seconds, then 8, 16, and so on. After exhausting the maximum retry count (configurable), deliveries are marked as “dead” and no longer retried. A cleanup task removes old delivery records after 7 days (configurable via broker.webhookCleanupRetentionDays).
Event Types
Brokkr emits events for various system activities, enabling external systems to react to state changes.
| Category | Event Types | Description |
|---|---|---|
| Agent | agent.registered, agent.deregistered | Agent lifecycle events |
| Stack | stack.created, stack.deleted | Stack lifecycle events |
| Deployment | deployment.created, deployment.applied, deployment.failed, deployment.deleted | Deployment object lifecycle and application results |
| Work Order | workorder.created, workorder.claimed, workorder.completed, workorder.failed | Work order lifecycle from creation to completion |
Webhook subscriptions can filter by event type using exact matches or wildcards (e.g., deployment.* matches all deployment events). This filtering reduces unnecessary network traffic and processing on the receiving end. See the Webhooks Reference for complete details on event payloads and subscription configuration.
Authentication Flows
All actors in Brokkr authenticate using Prefixed API Keys (PAKs) sent via the Authorization: Bearer header. The authentication middleware checks the PAK against three tables in order—admin roles, agents, and generators—to determine the identity type.
PAK Authentication
Prefixed API Keys (PAKs) provide secure, stateless authentication for agents. The PAK contains both an identifier and a secret component, enabling the broker to authenticate requests without storing plaintext secrets.
sequenceDiagram
participant Agent as Agent
participant Broker as Broker API
participant Auth as Auth Middleware
participant DB as PostgreSQL
Note over Agent: Agent startup
Agent->>Agent: Load PAK from config
Agent->>Broker: GET /api/v1/agents/{id}/target-state
Note over Agent,Broker: Authorization: Bearer {PAK}
Broker->>Auth: Validate PAK
Auth->>Auth: Parse PAK structure
Auth->>Auth: Extract short token (identifier)
Auth->>DB: Lookup agent by short token
DB-->>Auth: Agent record with hash
Auth->>Auth: Hash long token from request
Auth->>Auth: Compare with stored hash
alt Hashes match
Auth-->>Broker: Agent identity
Broker->>Broker: Continue with request
Broker-->>Agent: Response
else Invalid/Revoked
Auth-->>Broker: Authentication failed
Broker-->>Agent: 401 Unauthorized
end
PAK structure follows a defined format: brokkr_BR{short_token}_{long_token}. The short token serves as an identifier that can be safely logged and displayed. The long token is the secret component—it is hashed with SHA-256 before storage, and the plaintext is never persisted.
When an agent authenticates, the middleware extracts the short token to locate the agent record, then hashes the provided long token and compares it with the stored hash. This constant-time comparison prevents timing attacks that could reveal information about valid tokens.
PAKs can be rotated through the POST /api/v1/agents/{id}/rotate-pak endpoint, which generates a new PAK and invalidates the previous one. The new PAK is returned only once—it cannot be retrieved later.
Admin Authentication
Administrators authenticate using PAKs stored in the admin_role table. Admin PAKs grant access to sensitive management operations that regular agents and generators cannot perform.
sequenceDiagram
participant Admin as Admin Client
participant Broker as Broker API
participant Auth as Auth Middleware
participant DB as PostgreSQL
Admin->>Broker: POST /api/v1/admin/config/reload
Note over Admin,Broker: Authorization: Bearer {PAK}
Broker->>Auth: Validate PAK
Auth->>Auth: Parse PAK structure
Auth->>Auth: Extract short token
Auth->>DB: Lookup admin by pak_hash
DB-->>Auth: Admin record with hash
Auth->>Auth: Hash long token and compare
alt Valid PAK
Auth-->>Broker: Admin identity (admin flag set)
Broker->>Broker: Execute admin operation
Broker-->>Admin: Response
else Invalid PAK
Auth-->>Broker: Authentication failed
Broker-->>Admin: 401 Unauthorized
end
Admin PAKs enable access to sensitive operations including configuration reload, audit log queries, agent management, and system health endpoints. The PAK is verified using the same mechanism as agent authentication—SHA-256 hashing with constant-time comparison.
Generator Authentication
Generators, typically CI/CD systems, authenticate using PAKs just like agents and admins. These keys enable automated deployment workflows while maintaining security boundaries.
sequenceDiagram
participant Generator as Generator/CI
participant Broker as Broker API
participant Auth as Auth Middleware
participant DB as PostgreSQL
Generator->>Broker: POST /api/v1/stacks
Note over Generator,Broker: Authorization: Bearer {PAK}
Broker->>Auth: Validate PAK
Auth->>Auth: Parse PAK structure
Auth->>Auth: Extract short token
Auth->>DB: Lookup generator by pak_hash
DB-->>Auth: Generator record with hash
Auth->>Auth: Hash long token and compare
alt Valid PAK
Auth-->>Broker: Generator identity
Broker->>DB: Create stack (with generator_id)
Broker-->>Generator: Stack created
else Invalid PAK
Auth-->>Broker: Authentication failed
Broker-->>Generator: 401 Unauthorized
end
Generators can create and manage stacks and deployment objects, but they cannot access admin endpoints or manage other generators. Resources created by a generator are associated with its identity, enabling audit tracking and future access control enhancements.
Work Order Flow
Work orders enable the broker to dispatch tasks to agents for execution. Unlike deployment objects which represent desired state, work orders represent one-time operations like container image builds or diagnostic commands.
sequenceDiagram
participant Client as Admin/API
participant Broker as Broker API
participant DB as PostgreSQL
participant Agent as Agent
participant K8s as Kubernetes
participant Build as Build System
Client->>Broker: POST /api/v1/work-orders
Broker->>DB: INSERT work_order (status: PENDING)
Broker-->>Client: Work order created
Note over Agent: Next polling interval
Agent->>Broker: GET /api/v1/agents/{id}/work-orders/pending
Broker->>DB: Query matching work orders
DB-->>Broker: Work orders
Broker-->>Agent: Work order details
Agent->>Broker: POST /api/v1/work-orders/{id}/claim
Broker->>DB: Update status: CLAIMED
Broker-->>Agent: Claim confirmed
Agent->>K8s: Create Build resource
K8s->>Build: Execute build
loop Monitor progress
Agent->>K8s: Check build status
K8s-->>Agent: Build status
end
Build-->>K8s: Build complete
K8s-->>Agent: Success + artifacts
Agent->>Broker: POST /api/v1/work-orders/{id}/complete
Broker->>DB: Move to work_order_log
Broker->>DB: DELETE work_order
Broker-->>Agent: Acknowledged
Work orders support sophisticated targeting through three mechanisms: hard targets (specific agent IDs), label matching (agents with matching labels), and annotation matching (agents with matching annotations). An agent is eligible to claim a work order if it matches any of these criteria.
The claiming mechanism prevents multiple agents from processing the same work order. When an agent claims a work order, the broker atomically updates its status to CLAIMED and records the claiming agent’s ID. If the claim succeeds, the agent proceeds with execution; if another agent already claimed it, the claim fails.
Work Order States
Work orders transition through a defined set of states, with automatic retry handling for transient failures.
| State | Description | Transitions To |
|---|---|---|
| PENDING | Awaiting claim by an agent | CLAIMED |
| CLAIMED | Agent is processing | SUCCESS (to log), RETRY_PENDING |
| RETRY_PENDING | Scheduled for retry after failure | PENDING (after backoff) |
Successful completion moves the work order to the work_order_log table and deletes the original record. This design keeps the active work order table small while maintaining a complete history of completed operations.
Failed work orders may be retried depending on configuration. When a retryable failure occurs, the work order enters RETRY_PENDING status with a scheduled retry time based on exponential backoff. A background task runs every 10 seconds, resetting RETRY_PENDING work orders to PENDING once their retry time has elapsed.
Stale claims are automatically detected and reset. If an agent claims a work order but fails to complete it within the configured timeout (due to crash or network partition), the broker resets the work order to PENDING, allowing another agent to claim it.
Data Retention
Brokkr maintains extensive data for auditing, debugging, and compliance purposes. Different data types have different retention characteristics based on their importance and storage requirements.
Immutability Pattern
Deployment objects use an append-only pattern that preserves complete history. Objects are never modified after creation—updates create new objects with higher sequence IDs, and deletions create deletion markers rather than removing data. This approach enables precise audit trails and potential rollback to any previous state.
The deleted_at timestamp implements soft deletion across most entity types. Queries filter by deleted_at IS NULL by default, hiding deleted records from normal operations while preserving them for auditing. Special “include deleted” query variants provide access to the full history when needed.
Retention Policies
| Data Type | Default Retention | Cleanup Method |
|---|---|---|
| Deployment objects | Permanent | Soft delete only |
| Agent events | Permanent | Soft delete only |
| Webhook deliveries | 7 days | Background cleanup task |
| Audit logs | 90 days | Background cleanup task |
| Diagnostic results | 1 hour | Background cleanup task |
Background tasks run at regular intervals to enforce retention policies. The webhook cleanup task runs hourly, removing deliveries older than the configured retention period. The audit log cleanup task runs daily, removing entries beyond the retention window. Diagnostic results have a short retention period (1 hour by default) as they contain point-in-time debugging information.
Sequence ID Tracking
Agents track the highest sequence ID they have processed for each stack, enabling efficient incremental fetching. When an agent reconnects after downtime, it requests only objects with sequence IDs higher than its last processed value. This mechanism ensures reliable delivery of all updates while minimizing network traffic and processing time.
The GET /api/v1/agents/{id}/target-state endpoint leverages sequence tracking to return only unprocessed objects, reducing response size for agents managing large deployments.
Security Model
Security in Brokkr follows a defense-in-depth approach, implementing multiple layers of protection from network boundaries through application-level access controls. This document describes the trust boundaries, authentication mechanisms, authorization model, and operational security practices that protect Brokkr deployments.
Trust Boundaries
Brokkr defines four distinct security zones, each with different trust levels and access controls. Understanding these boundaries is essential for secure deployment architecture and incident response.
C4Context
title Brokkr Security Trust Boundaries (C4 System Context)
Person(admin, "Admin Users", "Untrusted zone — must authenticate")
System_Ext(generator, "Generators / CI", "Untrusted zone — must authenticate")
System_Ext(internet, "Internet / External", "Untrusted zone")
Enterprise_Boundary(dmz, "DMZ / Edge") {
System_Ext(ingress, "Ingress Controller", "TLS termination and routing")
}
Enterprise_Boundary(trusted, "Trusted Zone") {
System(broker, "Broker Service", "Authenticated API with authorization")
SystemDb(db, "PostgreSQL", "Encrypted secrets, hashed credentials")
}
Enterprise_Boundary(semi_trusted, "Semi-Trusted Zone (per cluster)") {
System(agent, "Brokkr Agent", "Scoped access — own resources only")
System_Ext(k8s, "Kubernetes API", "Target cluster API server")
}
Rel(admin, ingress, "API requests", "HTTPS")
Rel(generator, ingress, "Deployment requests", "HTTPS")
Rel(internet, ingress, "External traffic", "HTTPS")
Rel(ingress, broker, "Authenticated traffic", "HTTP :3000")
Rel(broker, db, "Read/Write", "TCP :5432")
Rel(agent, broker, "Poll & report", "HTTPS :3000")
Rel(agent, k8s, "Manage resources", "HTTPS :6443")
The Untrusted Zone encompasses all external entities: internet traffic, administrator clients, and CI/CD generators. Nothing in this zone receives implicit trust—every request must authenticate before accessing protected resources.
The DMZ provides the transition layer where external traffic enters the system. The ingress controller terminates TLS connections, validating certificates and establishing encrypted channels. This layer handles the initial security negotiation before traffic reaches application components.
The Trusted Zone contains the broker service and its PostgreSQL database. Components in this zone communicate over internal networks with mutual trust. The database accepts connections only from the broker, and the broker applies authentication and authorization before exposing data to external zones.
The Semi-Trusted Zone exists in each target cluster where agents operate. Agents receive scoped trust: they can access resources targeted specifically to them but cannot see resources belonging to other agents. This isolation prevents a compromised agent from affecting deployments on other clusters.
Security Principles
Four principles guide Brokkr’s security architecture:
Zero Trust by Default requires all external requests to authenticate. The broker’s API middleware rejects any request without valid credentials before route handlers execute. There are no anonymous endpoints except health checks.
Least Privilege restricts each identity to the minimum permissions necessary. Agents can only access deployment objects targeted to them through the agent_targets association. Generators can only manage stacks they created. This scoping limits the blast radius of credential compromise.
Defense in Depth implements multiple overlapping security controls. Even if an attacker bypasses network security, they face application-level authentication. Even with valid credentials, authorization limits accessible resources. Even with resource access, audit logging records all actions.
Immutable Audit Trail records every significant action in the system. Audit logs support only create and read operations—no updates or deletions are possible. This immutability ensures forensic evidence remains intact regardless of what an attacker does after gaining access.
Authentication Mechanisms
Brokkr implements three authentication mechanisms, each designed for different actor types and usage patterns.
Prefixed API Keys (PAKs)
PAKs serve as the primary authentication mechanism for agents and can also authenticate administrators and generators. The PAK design balances security with operational simplicity, enabling stateless authentication without storing plaintext secrets.
PAK Structure
Every PAK follows a structured format that embeds both an identifier and a secret component:
brokkr_BR{short_token}_{long_token}
^ ^ ^
| | |
| | +-- Long token (secret, used for verification)
| +--------------- Short token (identifier, safe to log)
+------------------ Prefix (identifies key type)
The prefix brokkr_BR identifies this as a Brokkr PAK, distinguishing it from other credentials. The short token serves as an identifier that can appear in logs and error messages without compromising security. The long token is the secret component—it proves the holder possesses the original PAK.
A typical PAK looks like brokkr_BRabc123_xyzSecretTokenHere.... In this example, abc123 is the short token (identifier) and xyzSecretTokenHere... is the long token (secret).
Generation Process
PAK generation occurs when creating agents, generators, or admin credentials. The process uses cryptographically secure randomness from the operating system’s entropy source:
-
The system generates a random short token of configurable length. This token uses URL-safe characters and serves as the lookup key in the database.
-
A separate random long token is generated with sufficient entropy for cryptographic security. This token never leaves the generation response.
-
The long token is hashed using SHA-256, producing a fixed-size digest that represents the secret without revealing it.
-
The database stores only the hash. The original long token exists only in the complete PAK string returned to the caller.
-
The complete PAK is returned exactly once. If the caller loses it, a new PAK must be generated—the original cannot be recovered.
This design means the broker never stores information sufficient to reconstruct a PAK. A database breach reveals only hashes, which cannot authenticate without the original long tokens.
Verification Process
When a request arrives with a PAK, the authentication middleware executes a verification sequence:
sequenceDiagram
participant Client
participant Middleware as Auth Middleware
participant DB as PostgreSQL
Client->>Middleware: Request with PAK header
Middleware->>Middleware: Parse PAK structure
Middleware->>Middleware: Extract short token
Middleware->>DB: Lookup by pak_hash index
DB-->>Middleware: Record with stored hash
Middleware->>Middleware: Hash long token from request
Middleware->>Middleware: Constant-time hash comparison
alt Hashes match
Middleware-->>Client: Authenticated (continue to handler)
else Hashes don't match
Middleware-->>Client: 401 Unauthorized
end
The middleware first parses the PAK to extract its components. Using the short token as an identifier, it performs an indexed database lookup to find the associated record. This lookup uses the pak_hash column index, ensuring O(1) performance regardless of how many credentials exist.
The middleware then hashes the long token from the incoming request using the same SHA-256 algorithm used during generation. Finally, it compares this computed hash with the stored hash. The comparison uses constant-time algorithms to prevent timing attacks that could reveal information about valid hashes.
If verification succeeds, the middleware populates an AuthPayload structure identifying the authenticated entity (agent, generator, or admin) and attaches it to the request for downstream handlers. If verification fails, the request is rejected with a 401 status before reaching any route handler.
PAK Security Properties
| Property | Implementation |
|---|---|
| Secrecy | Long token never stored; only SHA-256 hash persisted |
| Non-repudiation | PAK uniquely identifies the acting entity |
| Revocation | Entity can be disabled; PAK immediately invalid |
| Rotation | New PAK generated via rotate endpoint; old one invalidated |
| Performance | Indexed lookup prevents timing-based enumeration |
Admin Authentication
Administrative users authenticate using PAKs stored in the admin_role table. Admin PAKs grant access to sensitive management operations that regular agents and generators cannot perform.
Admin authentication follows the same verification process as agent authentication, but the resulting AuthPayload sets the admin flag to true. Route handlers check this flag to authorize access to admin-only endpoints.
# Example admin API call
curl -X POST https://broker.example.com/api/v1/admin/config/reload \
-H "Authorization: Bearer brokkr_BR..."
Admin credentials should be treated with extreme care. A compromised admin PAK grants access to all system data, configuration changes, and audit logs. Organizations should implement additional controls around admin credential storage and usage, such as hardware security modules or secrets management systems.
Generator Authentication
Generators authenticate using PAKs stored in the generators table. Generator credentials enable CI/CD systems to create and manage deployments programmatically.
Generator permissions are scoped to resources they create. When a generator creates a stack, the broker records the generator’s ID with that stack. Future operations on the stack verify the requesting generator matches the owner. This scoping prevents one generator from modifying another’s deployments.
# Example generator API call
curl -X POST https://broker.example.com/api/v1/stacks \
-H "Authorization: Bearer brokkr_BR..." \
-H "Content-Type: application/json" \
-d '{"name": "my-stack"}'
Generators cannot access admin endpoints regardless of their PAK. The authorization layer checks identity type before granting access to protected routes.
Authorization Model
Brokkr implements implicit role-based access control (RBAC) where roles are determined by authentication type rather than explicit role assignments.
Role Definitions
| Role | Authentication | Capabilities |
|---|---|---|
| Agent | PAK via agents table | Read targeted deployments, report events, claim work orders |
| Generator | PAK via generators table | Manage own stacks and deployment objects |
| Admin | PAK via admin_role table | Full system access including configuration and audit logs |
| System | Internal only | Background tasks, automated cleanup |
Endpoint Authorization
The following table summarizes which roles can access each API endpoint category:
| Endpoint Pattern | Agent | Generator | Admin |
|---|---|---|---|
/api/v1/agents/{id}/target-state | Own ID only | No | Yes |
/api/v1/agents/{id}/events | Own ID only | No | Yes |
/api/v1/agents/{id}/work-orders/* | Own ID only | No | Yes |
/api/v1/stacks/* | No | Own stacks | Yes |
/api/v1/agents/* (management) | No | No | Yes |
/api/v1/admin/* | No | No | Yes |
/api/v1/webhooks/* | No | No | Yes |
/healthz, /readyz | Yes | Yes | Yes |
/metrics | No | No | Yes |
Resource-Level Access Control
Beyond endpoint-level authorization, Brokkr enforces resource-level access control through database queries.
Agent Scope limits agents to resources explicitly targeted to them. When an agent requests deployment objects, the query joins through the agent_targets table:
SELECT do.* FROM deployment_objects do
JOIN agent_targets at ON at.stack_id = do.stack_id
WHERE at.agent_id = :requesting_agent_id
AND at.deleted_at IS NULL
AND do.deleted_at IS NULL;
This query structure ensures agents can never see deployment objects from stacks not targeted to them, regardless of what parameters they provide in API requests.
Generator Scope restricts generators to stacks they created:
SELECT * FROM stacks
WHERE generator_id = :requesting_generator_id
AND deleted_at IS NULL;
Generators cannot list, read, or modify stacks created by other generators or through admin operations.
Credential Management
Storage
Brokkr stores credentials using appropriate protection levels based on sensitivity:
| Credential Type | Storage Location | Protection |
|---|---|---|
| PAK hashes | PostgreSQL | SHA-256 hash (plaintext never stored) |
| Webhook URLs | PostgreSQL | AES-256-GCM encryption |
| Webhook auth headers | PostgreSQL | AES-256-GCM encryption |
| Database password | Kubernetes Secret | Base64 encoding (use sealed-secrets in production) |
| Webhook encryption key | Environment variable | Should use Kubernetes Secret |
Webhook Secret Encryption
Webhook URLs and authentication headers may contain sensitive information like API keys or tokens. Brokkr encrypts these values at rest using AES-256-GCM, a modern authenticated encryption algorithm.
The encryption format includes version information for future algorithm upgrades:
version (1 byte) || nonce (12 bytes) || ciphertext || tag (16 bytes)
The current version byte (0x01) indicates AES-256-GCM encryption. The 12-byte nonce ensures each encryption produces unique ciphertext even for identical plaintexts. The 16-byte authentication tag detects any tampering with the encrypted data.
The encryption key is configured via the BROKKR__BROKER__WEBHOOK_ENCRYPTION_KEY environment variable as a 64-character hexadecimal string (representing 32 bytes). If no key is configured, the broker generates a random key at startup and logs a warning. Production deployments should always configure an explicit key to ensure encrypted data survives broker restarts.
PAK Rotation
PAK rotation replaces an entity’s authentication credential without disrupting its identity or permissions. The POST /api/v1/agents/{id}/rotate-pak endpoint (and similar endpoints for generators) generates a new PAK and invalidates the previous one atomically.
# Rotate an agent's PAK
curl -X POST https://broker/api/v1/agents/{id}/rotate-pak \
-H "Authorization: Bearer <admin-pak>"
# Returns: new PAK (store immediately; cannot be retrieved again)
# Update agent configuration with new PAK
kubectl set env deployment/brokkr-agent BROKKR__AGENT__PAK=<new-pak>
After rotation, the old PAK becomes invalid immediately. Any requests using the old PAK receive 401 Unauthorized responses. The agent must be updated with the new PAK before its next broker communication.
Credential Revocation
Revoking access involves soft-deleting the associated entity. When an agent is deleted, its record remains in the database with a deleted_at timestamp, but authentication queries filter out deleted records:
# Revoke agent access
curl -X DELETE https://broker/api/v1/agents/{id} \
-H "Authorization: Bearer <admin-pak>"
After revocation, the agent’s PAK becomes invalid immediately. Existing deployments remain in the target cluster (Brokkr doesn’t forcibly remove resources), but the agent can no longer fetch new deployments or report events.
Audit Logging
The audit logging system records significant actions for security monitoring, compliance, and forensic analysis. The system prioritizes completeness and immutability while minimizing performance impact on normal operations.
Architecture
The audit logger uses an asynchronous design to avoid blocking API request handlers:
API Handler → mpsc channel (10,000 buffer) → Background Writer → PostgreSQL
When an action occurs, the handler sends an audit entry to a bounded channel. A background task collects entries from this channel and writes them to PostgreSQL in batches. This design ensures audit logging never blocks request processing, even under high load.
The background writer batches entries for efficiency, flushing when the batch reaches 100 entries or after 1 second, whichever comes first. This batching reduces database round trips while ensuring entries are persisted within a predictable time window.
Audit Log Contents
Each audit log entry captures comprehensive context about the action:
| Field | Description |
|---|---|
timestamp | When the action occurred (UTC) |
actor_type | Identity type: admin, agent, generator, or system |
actor_id | UUID of the acting entity (if applicable) |
action | What happened (e.g., agent.created, pak.rotated) |
resource_type | Type of affected resource |
resource_id | UUID of affected resource (if applicable) |
details | Structured JSON with action-specific data |
ip_address | Client IP address (when available) |
user_agent | Client user agent string (when available) |
Recorded Actions
The audit system records actions across several categories:
Authentication Events track credential usage and failures:
auth.success- Successful authenticationauth.failed- Failed authentication attemptpak.created- New PAK generatedpak.rotated- PAK rotatedpak.deleted- PAK revoked
Resource Lifecycle tracks creation, modification, and deletion:
agent.created,agent.updated,agent.deletedstack.created,stack.updated,stack.deletedgenerator.created,generator.updated,generator.deletedwebhook.created,webhook.updated,webhook.deleted
Operational Events record system activities:
workorder.created,workorder.claimed,workorder.completed,workorder.failedconfig.reloaded- Configuration hot-reload triggeredwebhook.delivery_failed- Webhook delivery failure
Query Capabilities
The audit log API supports filtering to find relevant entries:
# Recent authentication failures
curl "https://broker/api/v1/admin/audit-logs?action=auth.failed&limit=100" \
-H "Authorization: Bearer <admin-pak>"
# Actions by specific agent
curl "https://broker/api/v1/admin/audit-logs?actor_type=agent&actor_id=<uuid>" \
-H "Authorization: Bearer <admin-pak>"
# All admin actions in a time range
curl "https://broker/api/v1/admin/audit-logs?actor_type=admin&from=2024-01-01T00:00:00Z" \
-H "Authorization: Bearer <admin-pak>"
Filters support actor type and ID, action (with wildcard prefix matching), resource type and ID, and time ranges. Pagination limits responses to manageable sizes, with a maximum of 1000 entries per request.
Retention and Cleanup
Audit logs are retained for a configurable period (default: 90 days). A background task runs daily to remove entries older than the retention period. This cleanup prevents unbounded database growth while maintaining sufficient history for security investigations.
The retention period is configurable via broker settings, allowing organizations to meet their specific compliance requirements.
Kubernetes Security
Pod Security
Both broker and agent deployments configure security contexts that follow Kubernetes security best practices:
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false # Configurable
capabilities:
drop:
- ALL
Non-root execution prevents containers from running as the root user, limiting the impact of container escape vulnerabilities. The UID 10001 is arbitrary but consistent across components.
Privilege escalation prevention blocks processes from gaining additional capabilities after container startup, closing a common attack vector.
Capability dropping removes all Linux capabilities by default. Most applications don’t need capabilities, and removing them reduces attack surface.
AppArmor support is available when enabled via apparmor.enabled: true. AppArmor provides mandatory access control at the kernel level, further restricting what container processes can do.
Agent RBAC
The agent requires Kubernetes permissions to manage resources in target clusters. The Helm chart creates RBAC resources that implement least-privilege access:
Resource management access to core resources (pods, services, configmaps, deployments, and others) enables the agent to apply, update, and delete resources as part of deployment management. The agent requires create, get, list, watch, update, patch, and delete permissions on the resources it manages.
Shipwright access (when enabled) grants create, update, and delete permissions on Build and BuildRun resources for container image builds.
Optional secret access is disabled by default. When enabled via rbac.secretAccess.enabled, the agent can list and watch secrets. The rbac.secretAccess.readContents flag controls whether the agent can read actual secret values.
The RBAC configuration supports both namespace-scoped (Role/RoleBinding) and cluster-wide (ClusterRole/ClusterRoleBinding) permissions, configured via rbac.clusterWide.
Security Best Practices
Production Deployment Checklist
Before deploying Brokkr to production, verify these security configurations:
- TLS Everywhere: Enable TLS for all external connections via ingress or direct TLS
- Strong Secrets: Use cryptographically secure random values for all PAKs and encryption keys
- External Database: Use managed PostgreSQL with encryption at rest
- Secret Management: Store credentials in Kubernetes Secrets or external vault
- NetworkPolicy: Enable and configure network policies to restrict traffic
- RBAC: Use minimal required permissions for service accounts
- Pod Security: Enable pod security standards (restricted profile)
- Audit Logging: Enable and monitor audit logs
- Resource Limits: Set CPU/memory limits to prevent resource exhaustion
- Image Scanning: Scan container images for vulnerabilities before deployment
Monitoring for Security Events
Monitor these metrics and events for security-relevant activity:
| Indicator | Alert Threshold | Potential Issue |
|---|---|---|
| Failed authentication rate | > 10/minute | Brute force attack |
| Unexpected agent disconnections | Any | Possible compromise or network attack |
| Webhook delivery failure rate | > 50% | Network issues or endpoint compromise |
| Audit log volume spike | 10x normal | Unusual activity, possible attack |
| Admin action from unknown IP | Any | Credential theft |
Incident Response
Suspected Agent Compromise
If you suspect an agent’s credentials have been compromised:
- Revoke immediately: Delete or disable the agent via the admin API
- Review audit logs: Search for unusual actions by the agent’s actor_id
- Inspect cluster: Review resources the agent may have created or modified
- Rotate secrets: Generate new PAK if re-enabling the agent
- Investigate: Determine how the compromise occurred
Suspected Broker Compromise
If you suspect the broker itself has been compromised:
- Isolate: Remove external network access to the broker
- Preserve evidence: Capture logs, database state, and container images
- Rotate all credentials: Generate new PAKs for all agents, generators, and admins
- Review webhooks: Check for unauthorized webhook subscriptions
- Audit database: Look for unauthorized modifications to stacks or agents
- Rebuild: Consider deploying fresh broker instances rather than cleaning compromised ones
Compliance Considerations
Data Protection
Brokkr implements several controls relevant to data protection regulations:
Access logging records all API access with actor identification, supporting accountability requirements.
Encryption at rest protects sensitive webhook data using AES-256-GCM.
Encryption in transit via TLS protects all external communications.
Data minimization ensures PAK secrets are never stored, only their hashes.
Retention controls automatically remove old audit logs after the configured period.
Regulatory Mapping
| Requirement | Brokkr Feature |
|---|---|
| Access control | PAK authentication + implicit RBAC |
| Audit trail | Immutable audit logs with comprehensive action recording |
| Data encryption | TLS in transit, AES-256-GCM for secrets at rest |
| Least privilege | Scoped agent and generator access |
| Monitoring | Metrics endpoint, audit log queries |
| Incident response | Credential revocation, audit log forensics |
Container Image Publishing Strategy
This document explains Brokkr’s approach to building and publishing container images, including the reasoning behind our tagging strategy, multi-architecture support, and distribution decisions.
Publishing to GitHub Container Registry
Brokkr uses GitHub Container Registry (GHCR) as its primary container image registry for several reasons:
- Integrated authentication: Leverages GitHub’s existing access control and tokens
- Co-located with source: Images live alongside the code repository
- Cost effective: Free for public open-source projects
- Multi-architecture support: Full support for AMD64 and ARM64 platforms
- OCI compliance: Standards-compliant container registry
Public vs Private Distribution
Brokkr container images are published publicly despite being licensed under Elastic License 2.0. This decision balances openness with commercial protection:
Why Public?
- Easy evaluation: Users can try Brokkr without requesting access
- Community adoption: Lower barrier to entry encourages experimentation
- Source already available: The code is public on GitHub, so binaries being public is consistent
- Modern expectations: Developers expect to
docker pullopen-source-adjacent projects
License Protection Remains
Making images publicly accessible does not grant additional rights beyond the Elastic License 2.0:
- Users cannot offer Brokkr as a managed service
- Commercial restrictions still apply
- The license terms must be honored regardless of distribution method
- Source-available ≠ open-source
This approach follows the model used by Elasticsearch, Kibana, and other ELv2 projects.
Image Tagging Strategy
Brokkr uses multiple tagging strategies to support different use cases and deployment patterns.
Semantic Versioning for Releases
When a release is tagged (e.g., v1.2.3), multiple version tags are created:
- Full version (
v1.2.3): Exact release identifier - Minor version (
v1.2): Latest patch within minor version - Major version (
v1): Latest minor within major version - Latest (
latest): Most recent stable release
Rationale: This allows users to choose their update cadence:
- Pin to
v1.2.3for no automatic updates - Use
v1.2to get patch updates automatically - Use
v1to track the major version - Use
latestfor the bleeding edge (not recommended for production)
Commit SHA Tags
Every commit that triggers a build gets a SHA-based tag (e.g., sha-abc1234).
Rationale:
- Enables exact reproducibility
- Supports bisecting issues to specific commits
- Provides audit trail for security and compliance
- Immutable and unique across the repository’s lifetime
Branch Tags
Active branches get updated tags matching the branch name (e.g., main, develop).
Rationale:
- Development teams can track branch heads
- Continuous integration environments can use consistent tag names
- Useful for testing changes before they’re released
Pull Request Tags (Optional)
Pull requests can optionally generate tags (e.g., pr-123).
Rationale:
- Test changes in isolation before merging
- Share pre-merge builds with reviewers or QA
- Verify changes work in containerized environments
Tag Immutability
Not all tags are created equal. Understanding mutability is critical for production deployments:
Immutable Tags
These tags never change once created:
- Semantic versions:
v1.2.3,v1.2,v1 - SHA tags:
sha-abc1234
Mutable Tags
These tags are updated with new pushes:
- Branch names:
main,develop,feature-xyz - Latest:
latest - PR tags:
pr-123(if the PR is updated)
Production Deployment Recommendation
For production deployments, always use digest references instead of tags:
# Best - digest reference (immutable)
image: ghcr.io/colliery-io/brokkr-broker@sha256:9fc91fae...
# Good - semantic version (immutable)
image: ghcr.io/colliery-io/brokkr-broker:v1.2.3
# Acceptable - minor version (gets patches)
image: ghcr.io/colliery-io/brokkr-broker:v1.2
# Avoid - mutable tags
image: ghcr.io/colliery-io/brokkr-broker:latest
Using digests ensures that a deployment always references the exact image that was tested and approved, preventing unexpected changes from tag updates.
Multi-Architecture Support
All Brokkr images are built for both AMD64 and ARM64 architectures.
Why Multi-Architecture?
- Apple Silicon support: Developers on M1/M2/M3 Macs run ARM64 natively
- AWS Graviton: ARM64 instances offer better price/performance
- Edge computing: ARM64 is common in edge and IoT deployments
- Future-proofing: ARM64 adoption is accelerating across cloud providers
Implementation
Brokkr uses Docker Buildx to create multi-architecture manifest lists. When you pull an image, Docker automatically selects the correct architecture:
# Same command works on AMD64 and ARM64
docker pull ghcr.io/colliery-io/brokkr-broker:v1.0.0
The manifest list contains references to both architectures, and Docker pulls the appropriate one based on the host platform.
Local Development Considerations
Local builds with --load can only target a single architecture due to Docker limitations. The build tools automatically detect your platform and build for it:
- Apple Silicon (M1/M2/M3): Builds
linux/arm64 - Intel/AMD systems: Builds
linux/amd64
For multi-architecture builds, use --push to publish directly to the registry without loading locally.
Security Considerations
Image Content Security
Before any image is published:
- No embedded secrets: Credentials must never be baked into images
- Build argument hygiene: Ensure build args don’t leak sensitive data
- Minimal base images: Use slim Debian images to reduce attack surface
- Dependency scanning: Automated scanning for known vulnerabilities (planned)
Authentication and Authorization
- GitHub Actions: Uses built-in
GITHUB_TOKENwith automatic permissions - Manual publishing: Requires Personal Access Token with
write:packagesscope - Token security: Tokens stored as GitHub secrets, never committed to source
Public Registry Security
Public images mean:
- Anyone can pull and inspect the images
- Image layers and content are visible
- Security through obscurity does not apply
Therefore, all security must be built into the application itself, not rely on image privacy.
Automated vs Manual Publishing
Automated Publishing (Preferred)
GitHub Actions workflows handle publishing for:
- Release tags → semantic version tags
- Main branch pushes →
maintag and SHA tags - Develop branch pushes →
developtag and SHA tags
Benefits:
- Consistent build environment
- Multi-architecture builds guaranteed
- Security scanning integrated
- Audit trail in GitHub Actions logs
Manual Publishing
Manual publishing is supported for:
- Testing the build process
- Emergency releases
- Local development verification
Manual builds use the angreal build multi-arch command with authentication to GHCR.
Future Enhancements
Planned improvements to the publishing strategy:
- Image signing: Cosign signatures for supply chain security
- SBOM generation: Software Bill of Materials for dependency tracking
- Vulnerability scanning: Automated Trivy or Grype integration
- Helm chart publishing: OCI-based Helm chart distribution via GHCR
- Image attestations: Build provenance and SLSA compliance
Related Documentation
- Container Images Reference - Repository URLs and tag formats
Work Orders & Build System
This document explains the design rationale behind Brokkr’s work order system and its integration with Shipwright for container builds. Work orders extend Brokkr beyond Kubernetes manifest distribution into task orchestration across clusters.
Why Work Orders Exist
Brokkr’s core workflow — pushing YAML deployment objects to agents — solves the “distribute manifests” problem well. But real-world operations need more:
- Build container images on specific clusters (near source registries, with GPU access, etc.)
- Run one-off tasks like database migrations, cleanup scripts, or certificate rotations
- Execute maintenance operations that need to happen on specific clusters
Work orders address these needs without changing the pull-based deployment model. They’re a parallel system: deployment objects are continuous desired state; work orders are discrete, one-time tasks.
Design Decisions
Pull-Based, Not Push-Based
Like deployment objects, work orders use a pull model. The broker doesn’t push commands to agents. Instead:
- Admin creates a work order in the broker
- Broker determines eligible agents via targeting rules
- Agents poll for pending work orders they’re authorized to claim
- One agent claims and executes the work order
- Agent reports completion (success or failure)
This preserves Brokkr’s key property: clusters behind firewalls work without inbound connections.
Single-Claim Semantics
A work order is claimed by exactly one agent. This prevents duplicate execution — you don’t want two clusters running the same database migration. The claiming process is atomic: the work order transitions from PENDING to CLAIMED in a single database transaction.
Retry with Exponential Backoff
When a work order fails, the system supports automatic retries:
PENDING → CLAIMED → (failure) → RETRY_PENDING → (backoff expires) → PENDING → CLAIMED
The backoff follows the formula: 2^retry_count × backoff_seconds
| Retry | Backoff (60s base) | Wait |
|---|---|---|
| 1st | 2¹ × 60 | 2 minutes |
| 2nd | 2² × 60 | 4 minutes |
| 3rd | 2³ × 60 | 8 minutes |
After max_retries failures, the work order moves to the log with success: false.
Why Retryability Is Caller-Declared
The retryable flag on completion is set by the agent, not the broker. This is intentional: only the agent knows whether the failure was transient (network timeout, resource contention) or permanent (invalid YAML, missing permissions). Non-retryable failures skip the retry loop entirely.
Stale Claim Detection
If an agent claims a work order but crashes before completing it, the work order would be stuck in CLAIMED forever. The broker’s maintenance task detects stale claims:
- Each work order has a
claim_timeout_seconds(default: 3600 = 1 hour) - If a claimed work order exceeds its timeout, it’s released back to
PENDING - The maintenance task runs every 10 seconds
State Machine
┌──────────┐
│ PENDING │◄─────────────────────┐
└────┬─────┘ │
│ claim() │ backoff expires
▼ │
┌──────────┐ ┌───────┴──────┐
│ CLAIMED │ │RETRY_PENDING │
└────┬─────┘ └──────────────┘
│ ▲
┌──────────┴──────────┐ │
▼ ▼ │
┌────────────────┐ ┌────────────────┐ │
│ complete_success│ │complete_failure │────────┘
└────────┬───────┘ └────────┬───────┘ (retryable + retries left)
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ WORK_ORDER_LOG│ │ WORK_ORDER_LOG│
│ success=true │ │ success=false │
└────────────────┘ └────────────────┘
Targeting
Work orders support three targeting mechanisms: hard targets (explicit agent UUIDs), label matching, and annotation matching. At least one must be specified — the API rejects work orders with no targeting.
All three mechanisms use OR logic: an agent is eligible if it matches any of the specified targets, labels, or annotations. When multiple types are combined, they’re also OR’d together — agent UUID-1 OR any agent with label builder:true can claim the work order.
Design note: This differs from template matching, which uses AND logic (a template’s labels must all be present on the stack). The rationale: work orders need to reach at least one capable agent (OR is permissive), while template matching needs to ensure full compatibility with a stack (AND is restrictive). See Template Matching & Rendering for comparison.
See the Work Orders Reference for the complete targeting API with request body examples.
Shipwright Build Integration
The primary built-in work order type is build, which integrates with Shipwright for container image builds.
The agent claims a build work order, applies the Shipwright Build resource, creates a BuildRun, watches it until completion, and reports back (including the image digest on success). See Container Builds with Shipwright for the complete operational guide.
Why Shipwright?
Shipwright provides a Kubernetes-native build abstraction:
- No privileged containers — uses unprivileged build strategies (Buildah, Kaniko, etc.)
- Cluster-native — builds run as Kubernetes resources, leverage cluster scheduling
- Strategy flexibility — swap between Buildah, Kaniko, ko, S2I without changing build definitions
- Build caching — strategies can cache layers for faster rebuilds
Builds have a 15-minute timeout — if the BuildRun doesn’t complete in time, it’s reported as failed. These timeouts are compile-time constants in the agent, not configurable at runtime.
Work Order Log
Completed work orders (success or failure) are moved from the active work_orders table to the work_order_log table. This is an immutable audit trail:
| Active Table | Log Table |
|---|---|
work_orders | work_order_log |
| Mutable (status changes) | Immutable (write-once) |
| Current/pending work | Historical record |
| Cleaned up on completion | Retained indefinitely |
The log records: original ID, work type, timing, claiming agent, success/failure, retry count, and result message.
Custom Work Orders
Beyond builds, work orders support arbitrary YAML:
{
"work_type": "custom",
"yaml_content": "apiVersion: batch/v1\nkind: Job\nmetadata:\n name: db-migration\nspec:\n template:\n spec:\n containers:\n - name: migrate\n image: myapp/migrate:v1\n restartPolicy: Never"
}
Custom work orders apply the YAML to the cluster and monitor completion. This enables arbitrary Kubernetes jobs, CronJobs, or any other resource to be orchestrated through Brokkr.
Related Documentation
- Work Orders Reference — API endpoints and data model
- Container Builds with Shipwright — setup and usage guide
- Data Flows — work order lifecycle in context
- Architecture — system-level view
Template Matching & Rendering
This document explains the design of Brokkr’s template system — how templates are matched to stacks and rendered into deployment objects via the Tera engine with JSON Schema validation.
The Problem Templates Solve
Without templates, every deployment requires hand-crafted YAML. In a multi-environment setup (staging, production, 10 regional clusters), you end up with nearly-identical YAML files that differ only in replica counts, image tags, resource limits, and environment variables. This leads to:
- Duplication drift — copies fall out of sync
- Manual errors — wrong value in wrong environment
- No validation — any YAML is accepted, mistakes caught only at apply time
Templates solve this with parameterized YAML, schema validation, and matching rules that prevent production templates from being instantiated into staging stacks.
Architecture
The template system has three components:
┌─────────────────────────────────────────────┐
│ Template │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Tera Content │ │ JSON Schema │ │
│ │ (YAML with │ │ (parameter │ │
│ │ placeholders) │ │ validation) │ │
│ └─────────────────┘ └──────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ Labels + Annotations (matching rules) │ │
│ └─────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────┘
│ instantiate(parameters)
▼
┌────────────────┐
│ Validate params │ ← JSON Schema check
│ Match stack │ ← Label/annotation check
│ Render Tera │ ← Variable substitution
└────────┬───────┘
▼
┌────────────────┐
│ Deployment │
│ Object │ (rendered YAML)
└────────────────┘
Rendering Pipeline
Step 1: Parameter Validation
Before touching the template, Brokkr validates the provided parameters against the JSON Schema. This catches issues early:
// Schema requires service_name (string, 1-63 chars) and replicas (integer, 1-20)
// Caller provides: {"service_name": "", "replicas": 100}
// Result: Validation fails — service_name too short, replicas exceeds maximum
If validation fails, the request is rejected with a detailed error message explaining which constraints were violated. No YAML is rendered.
Step 2: Stack Matching
Templates can have labels and annotations that restrict which stacks they can be instantiated into. This is a safety mechanism — it prevents, for example, a production-hardened template from being used in a development stack where the configuration doesn’t make sense.
The matching is strict AND logic: the stack must have every label and annotation the template requires. A template with no labels/annotations matches any stack (universal). Extra labels on the stack are ignored — it only matters that the required ones are present.
For the complete matching rules table with examples, see the Templates Reference.
Step 3: Tera Rendering
With validation and matching passed, Brokkr renders the Tera template:
- Creates a Tera context from the JSON parameters (flat key-value mapping)
- Adds the parameter values to the context
- Renders the template content through Tera
- The resulting string is the final Kubernetes YAML
Tera supports rich template features:
- Variables:
{{ service_name }} - Conditionals:
{% if enable_monitoring %}...{% endif %} - Loops:
{% for port in ports %}...{% endfor %} - Filters:
{{ name | upper }},{{ x | default(value="fallback") }} - Math:
{{ replicas * 2 }}
Step 4: Deployment Object Creation
The rendered YAML is stored as a new deployment object in the target stack, along with provenance metadata:
rendered_deployment_objects.template_id— which template was usedrendered_deployment_objects.template_version— which versionrendered_deployment_objects.template_parameters— the exact parameters provided
This provenance enables re-rendering with different parameters or auditing what parameters produced a given deployment.
Versioning Design
Why Version Templates?
Templates evolve: you add a liveness probe, change resource defaults, introduce a new parameter. Without versioning, updating a template could silently change the meaning of existing deployments.
Brokkr’s versioning ensures:
- Existing deployments are immutable — a deployment object rendered from template v1 stays as-is even after v2 is created
- New instantiations use latest — when you instantiate a template, you always get the newest version
- Provenance is preserved — you can trace any deployment back to the exact template version and parameters
Version Lifecycle
Version 1 ────── Version 2 ────── Version 3 (latest)
│ │
│ ├── Deployment Object A (rendered from v2)
│ └── Deployment Object B (rendered from v2)
│
├── Deployment Object C (rendered from v1, still lives in cluster)
└── Deployment Object D (rendered from v1)
Updating a template via PUT /api/v1/templates/{id} creates a new version. The version number auto-increments. Old versions remain in the database.
System Templates vs. Generator Templates
Templates have two ownership modes:
System Templates (generator_id = NULL)
- Created by admins
- Visible to all generators and admins
- Represent organization-wide standards (e.g., “standard web service”, “batch job”)
- Cannot be modified by generators
Generator Templates (generator_id = UUID)
- Created by a specific generator
- Visible only to the owning generator and admins
- Represent pipeline-specific templates (e.g., templates tailored for a particular CI/CD system)
- Can be modified by the owning generator
This separation allows centralized governance (admin-managed standards) while still allowing individual teams (generators) to create specialized templates.
Why Tera?
Brokkr chose Tera over alternatives:
| Feature | Tera | Go templates | Jinja2 | Handlebars |
|---|---|---|---|---|
| Language | Rust-native | Go | Python | JS/Rust |
| Syntax | {{ var }}, {% if %} | {{ .Var }}, {{ if }} | {{ var }}, {% if %} | {{ var }}, {{#if}} |
| Filters | Rich built-in | Limited | Rich | Limited |
| Whitespace control | Yes | Yes | Yes | Yes |
| Safe by default | Yes (auto-escape) | No | Yes (configurable) | Yes |
Tera was chosen because:
- Native Rust integration (no FFI or subprocess)
- Familiar Jinja2-like syntax widely known by DevOps engineers
- Rich filter and function library
- Compile-time syntax validation via
add_raw_template
Why JSON Schema?
JSON Schema was chosen for parameter validation because:
- Industry standard — widely understood, extensive tooling
- Declarative — schema defines constraints, engine enforces them
- Rich constraints — types, ranges, patterns, required fields, enums, string lengths
- Self-documenting — the
descriptionfield in each property serves as parameter documentation - Client-side validation — CI/CD systems can validate parameters before hitting the API
Related Documentation
- Templates Reference — API endpoints and data model
- Using Stack Templates — how-to guide
- Tutorial: Standardized Deployments — step-by-step tutorial
- Data Model — template entity relationships
Technical Reference
This section provides comprehensive technical documentation for Brokkr, including API references, code documentation, and implementation details.
API Documentation
The Brokkr API provides a comprehensive set of endpoints for managing deployments, agents, and system configuration. You can explore the complete API documentation in two ways:
Interactive API Documentation
Our interactive Swagger UI provides a complete reference of all available endpoints, including:
- Detailed request/response schemas
- Authentication requirements
- Example requests and responses
- Interactive testing interface
View Interactive API Documentation
API Endpoints Overview
The Brokkr API is organized into the following main sections:
Health Check
GET /healthz- Liveness checkGET /readyz- Readiness checkGET /api/v1/health- Detailed health diagnostics
Agent Management
POST /api/v1/agents- Create a new agentGET /api/v1/agents- List all agentsGET /api/v1/agents/{agent_id}- Get agent detailsPUT /api/v1/agents/{agent_id}- Update an agentDELETE /api/v1/agents/{agent_id}- Delete an agent
Stack Management
POST /api/v1/stacks- Create a new stackGET /api/v1/stacks- List all stacksGET /api/v1/stacks/{stack_id}- Get stack detailsPUT /api/v1/stacks/{stack_id}- Update a stackDELETE /api/v1/stacks/{stack_id}- Delete a stack
Deployment Object Management
POST /api/v1/stacks/{stack_id}/deployment-objects- Create a deployment objectGET /api/v1/stacks/{stack_id}/deployment-objects- List deployment objects
Event Management
POST /api/v1/events- Report a deployment eventGET /api/v1/events- List events
For detailed information about each endpoint, including request/response formats and examples, please refer to the Interactive API Documentation.
Rust API Documentation
The Brokkr codebase is written in Rust and provides a rich set of APIs for both the Broker and Agent components. You can explore the complete Rust API documentation here:
The Rust documentation includes:
- Detailed module and function documentation
- Type definitions and trait implementations
- Code examples and usage patterns
- Implementation details for core components
Key Components
Broker
- API Server implementation
- Database layer
- Event system
- Authentication and authorization
Agent
- Kubernetes client
- Broker communication
- State management
- Deployment orchestration
For detailed information about the Rust implementation, including module structure and function documentation, please refer to the Rust API Documentation.
API Reference
Brokkr provides a comprehensive REST API for managing deployments, agents, stacks, templates, and work orders across your Kubernetes clusters.
Interactive API Documentation
The Brokkr broker includes an interactive Swagger UI that provides complete API documentation with:
- All available endpoints with request/response schemas
- Authentication requirements for each endpoint
- Try-it-out functionality for testing endpoints
- Example requests and responses
Access Swagger UI at: http://<broker-url>/swagger-ui
OpenAPI spec available at: http://<broker-url>/docs/openapi.json
API Overview
All API endpoints are prefixed with /api/v1/ and require authentication via PAK (Pre-Authenticated Key) in the Authorization header.
Authentication
# All requests require a PAK in the Authorization header
curl -H "Authorization: Bearer <your-pak>" http://localhost:3000/api/v1/...
There are three types of PAKs:
- Admin PAK: Full access to all endpoints
- Agent PAK: Access to agent-specific endpoints (target state, events, heartbeat)
- Generator PAK: Access to create deployment objects for assigned stacks
Core Resources
Stacks
Stacks are collections of Kubernetes resources managed as a unit.
| Method | Endpoint | Description |
|---|---|---|
| GET | /stacks | List all stacks |
| POST | /stacks | Create a new stack |
| GET | /stacks/:id | Get stack by ID |
| PUT | /stacks/:id | Update a stack |
| DELETE | /stacks/:id | Delete a stack |
| GET | /stacks/:id/labels | List stack labels |
| POST | /stacks/:id/labels | Add label to stack |
| DELETE | /stacks/:id/labels/:label | Remove label from stack |
| GET | /stacks/:id/annotations | List stack annotations |
| POST | /stacks/:id/annotations | Add annotation to stack |
| DELETE | /stacks/:id/annotations/:key | Remove annotation |
| GET | /stacks/:id/deployment-objects | List deployment objects |
| POST | /stacks/:id/deployment-objects | Create deployment object |
| POST | /stacks/:id/deployment-objects/from-template | Instantiate template |
Agents
Agents run in Kubernetes clusters and apply deployment objects.
| Method | Endpoint | Description |
|---|---|---|
| GET | /agents | List all agents |
| POST | /agents | Register a new agent |
| GET | /agents/:id | Get agent by ID |
| PUT | /agents/:id | Update an agent |
| DELETE | /agents/:id | Delete an agent |
| GET | /agents/:id/target-state | Get agent’s target state |
| POST | /agents/:id/heartbeat | Record agent heartbeat |
| GET | /agents/:id/labels | List agent labels |
| POST | /agents/:id/labels | Add label to agent |
| GET | /agents/:id/annotations | List agent annotations |
| POST | /agents/:id/annotations | Add annotation to agent |
| GET | /agents/:id/stacks | List agent’s associated stacks |
| GET | /agents/:id/targets | List agent’s stack targets |
| POST | /agents/:id/targets | Add stack target |
| DELETE | /agents/:id/targets/:stack_id | Remove stack target |
| POST | /agents/:id/rotate-pak | Rotate agent PAK |
Templates
Reusable stack templates with Tera templating and JSON Schema validation.
| Method | Endpoint | Description |
|---|---|---|
| GET | /templates | List all templates |
| POST | /templates | Create a new template |
| GET | /templates/:id | Get template by ID |
| PUT | /templates/:id | Update a template |
| DELETE | /templates/:id | Delete a template |
| GET | /templates/:id/labels | List template labels |
| POST | /templates/:id/labels | Add label to template |
| GET | /templates/:id/annotations | List template annotations |
| POST | /templates/:id/annotations | Add annotation to template |
Work Orders
Transient operations like container builds routed to agents.
| Method | Endpoint | Description |
|---|---|---|
| GET | /work-orders | List all work orders |
| POST | /work-orders | Create a new work order |
| GET | /work-orders/:id | Get work order by ID |
| DELETE | /work-orders/:id | Cancel a work order |
| POST | /work-orders/:id/claim | Claim a work order (agent) |
| POST | /work-orders/:id/complete | Complete a work order (agent) |
| GET | /agents/:id/work-orders/pending | Get pending work orders for agent |
| GET | /work-order-log | List completed work orders |
| GET | /work-order-log/:id | Get completed work order details |
Generators
External systems that create deployment objects.
| Method | Endpoint | Description |
|---|---|---|
| GET | /generators | List all generators |
| POST | /generators | Create a new generator |
| GET | /generators/:id | Get generator by ID |
| PUT | /generators/:id | Update a generator |
| DELETE | /generators/:id | Delete a generator |
| POST | /generators/:id/rotate-pak | Rotate generator PAK |
Other Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /agent-events | List agent events |
| GET | /agent-events/:id | Get agent event by ID |
| GET | /deployment-objects/:id | Get deployment object by ID |
| POST | /auth/pak | Verify a PAK |
Webhooks
| Method | Endpoint | Description |
|---|---|---|
| GET | /webhooks | List webhook subscriptions |
| POST | /webhooks | Create webhook subscription |
| GET | /webhooks/event-types | List available event types |
| GET | /webhooks/:id | Get webhook subscription |
| PUT | /webhooks/:id | Update webhook subscription |
| DELETE | /webhooks/:id | Delete webhook subscription |
| POST | /webhooks/:id/test | Test webhook delivery |
| GET | /webhooks/:id/deliveries | List webhook deliveries |
Admin
| Method | Endpoint | Description |
|---|---|---|
| GET | /admin/audit-logs | Query audit logs |
| POST | /admin/config/reload | Reload broker configuration |
Health Monitoring
| Method | Endpoint | Description |
|---|---|---|
| GET | /deployment-objects/:id/health | Get deployment health |
| GET | /deployment-objects/:id/diagnostics | Get diagnostics |
| POST | /deployment-objects/:id/diagnostics | Request diagnostic |
Health Endpoints
The broker exposes health endpoints (not under /api/v1/):
| Endpoint | Description |
|---|---|
/healthz | Basic health check |
/readyz | Readiness probe |
/metrics | Prometheus metrics |
See Health Endpoints for details.
Error Handling
All API errors return JSON with an error field:
{
"error": "Description of what went wrong"
}
Common HTTP status codes:
400- Bad request (invalid input)401- Unauthorized (missing or invalid PAK)403- Forbidden (valid PAK but insufficient permissions)404- Not found422- Unprocessable entity (validation failed)500- Internal server error
Rate Limiting
The API does not currently implement rate limiting. For production deployments, consider placing a reverse proxy in front of the broker.
CLI Reference
Brokkr provides two command-line binaries: brokkr-broker for the central management server and brokkr-agent for the cluster-side agent. Both accept configuration via files, environment variables, and command-line flags.
brokkr-broker
The broker binary runs the central API server and provides administrative commands for managing agents, generators, and keys.
Commands
brokkr-broker serve
Starts the broker HTTP server on 0.0.0.0:3000.
brokkr-broker serve
Endpoints exposed:
| Path | Purpose |
|---|---|
/api/v1/* | REST API (see API Reference) |
/healthz | Liveness probe |
/readyz | Readiness probe |
/metrics | Prometheus metrics |
/swagger-ui | Interactive API documentation |
brokkr-broker create agent
Creates a new agent record and generates its initial PAK.
brokkr-broker create agent --name <name> --cluster-name <cluster>
Flags:
| Flag | Required | Description |
|---|---|---|
--name | Yes | Human-readable agent name |
--cluster-name | Yes | Name of the Kubernetes cluster this agent represents |
Output:
Agent created successfully:
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Name: production-us-east
Cluster: us-east-1-prod
Initial PAK: brokkr_BRx9y2Kq_A1B2C3D4E5F6G7H8I9J0K1L2
Important: The PAK is only displayed once. Store it securely.
brokkr-broker create generator
Creates a new generator for CI/CD integration.
brokkr-broker create generator --name <name> [--description <desc>]
Flags:
| Flag | Required | Description |
|---|---|---|
--name | Yes | Generator name (1-255 characters) |
--description | No | Optional description |
Output:
Generator created successfully:
ID: f8e7d6c5-b4a3-2109-8765-432109876543
Name: github-actions
Initial PAK: brokkr_BRy8z3Lp_M1N2O3P4Q5R6S7T8U9V0W1X2
brokkr-broker rotate admin
Generates a new admin PAK, replacing the existing one.
brokkr-broker rotate admin
The new PAK is printed to stdout. The old PAK immediately stops working.
brokkr-broker rotate agent
Rotates an agent’s PAK.
brokkr-broker rotate agent --uuid <uuid>
Flags:
| Flag | Required | Description |
|---|---|---|
--uuid | Yes | The agent’s UUID |
brokkr-broker rotate generator
Rotates a generator’s PAK.
brokkr-broker rotate generator --uuid <uuid>
Flags:
| Flag | Required | Description |
|---|---|---|
--uuid | Yes | The generator’s UUID |
brokkr-agent
The agent binary runs in each target Kubernetes cluster and polls the broker for deployment objects to apply.
Commands
brokkr-agent start
Starts the agent process.
brokkr-agent start
Health endpoints exposed on agent.health_port (default: 8080):
| Path | Purpose |
|---|---|
/healthz | Liveness probe (always 200 OK) |
/readyz | Readiness probe (checks K8s + broker connectivity) |
/health | Detailed health status (JSON) |
/metrics | Prometheus metrics |
Configuration
Both binaries read configuration from the same layered system:
- Embedded defaults (
default.tomlcompiled into the binary) - Configuration file (optional, path passed at startup or via
BROKKR_CONFIG_FILE) - Environment variables (prefix:
BROKKR__, separator:__)
See the Configuration Guide for all available settings and the Environment Variables Reference for the complete variable listing.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Clean shutdown |
| 1 | Startup failure (database, config, migration error) |
| 130 | SIGINT (Ctrl+C) received |
| 143 | SIGTERM received |
Examples
# Start broker with custom config file
BROKKR_CONFIG_FILE=/etc/brokkr/broker.toml brokkr-broker serve
# Start broker with environment overrides
BROKKR__DATABASE__URL=postgres://user:pass@db:5432/brokkr \
BROKKR__LOG__LEVEL=info \
BROKKR__LOG__FORMAT=json \
brokkr-broker serve
# Create an agent and capture its PAK
brokkr-broker create agent --name prod-1 --cluster_name us-east-1 2>&1 | grep "Initial PAK"
# Start agent with environment config
BROKKR__AGENT__BROKER_URL=https://broker.example.com \
BROKKR__AGENT__PAK=brokkr_BRx9y2Kq_A1B2C3D4E5F6G7H8I9J0K1L2 \
BROKKR__AGENT__AGENT_NAME=prod-1 \
BROKKR__AGENT__CLUSTER_NAME=us-east-1 \
brokkr-agent start
Environment Variables Reference
Complete listing of all environment variables supported by Brokkr. All variables use the BROKKR__ prefix with double underscores (__) as nested separators.
Configuration precedence (highest wins): Environment variables > Config file > Embedded defaults.
Database
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__DATABASE__URL | String | postgres://brokkr:brokkr@localhost:5432/brokkr | PostgreSQL connection URL |
BROKKR__DATABASE__SCHEMA | String | (none) | Schema name for multi-tenant isolation. When set, all queries use this schema. |
Logging
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__LOG__LEVEL | String | debug | Log level: trace, debug, info, warn, error |
BROKKR__LOG__FORMAT | String | text | Log format: text (human-readable) or json (structured) |
The log level is hot-reloadable — changes take effect without restarting.
Broker
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__BROKER__PAK_HASH | String | (generated) | Admin PAK hash (set during first startup) |
BROKKR__BROKER__DIAGNOSTIC_CLEANUP_INTERVAL_SECONDS | Integer | 900 | Interval for diagnostic cleanup task (seconds) |
BROKKR__BROKER__DIAGNOSTIC_MAX_AGE_HOURS | Integer | 1 | Max age for completed diagnostics before deletion (hours) |
BROKKR__BROKER__WEBHOOK_ENCRYPTION_KEY | String | (random) | Hex-encoded 32-byte AES-256 key for encrypting webhook URLs and auth headers. If empty, a random key is generated on startup (not recommended for production — webhooks won’t decrypt after restart). |
BROKKR__BROKER__WEBHOOK_DELIVERY_INTERVAL_SECONDS | Integer | 5 | Webhook delivery worker poll interval (seconds) |
BROKKR__BROKER__WEBHOOK_DELIVERY_BATCH_SIZE | Integer | 50 | Max webhook deliveries processed per batch |
BROKKR__BROKER__WEBHOOK_CLEANUP_RETENTION_DAYS | Integer | 7 | How long to keep completed/dead webhook deliveries (days) |
BROKKR__BROKER__AUDIT_LOG_RETENTION_DAYS | Integer | 90 | How long to keep audit log entries (days) |
BROKKR__BROKER__AUTH_CACHE_TTL_SECONDS | Integer | 60 | TTL for PAK authentication cache (seconds). Set to 0 to disable caching. |
Agent
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__AGENT__BROKER_URL | String | http://localhost:3000 | Broker API base URL |
BROKKR__AGENT__POLLING_INTERVAL | Integer | 10 | How often to poll broker for updates (seconds) |
BROKKR__AGENT__KUBECONFIG_PATH | String | (in-cluster) | Path to kubeconfig file. If unset, uses in-cluster configuration. |
BROKKR__AGENT__MAX_RETRIES | Integer | 60 | Max retries when waiting for broker on startup |
BROKKR__AGENT__PAK | String | (required) | Agent’s PAK for broker authentication |
BROKKR__AGENT__AGENT_NAME | String | DEFAULT | Agent name (must match broker registration) |
BROKKR__AGENT__CLUSTER_NAME | String | DEFAULT | Cluster name (must match broker registration) |
BROKKR__AGENT__MAX_EVENT_MESSAGE_RETRIES | Integer | 2 | Max retries for event message delivery |
BROKKR__AGENT__EVENT_MESSAGE_RETRY_DELAY | Integer | 5 | Delay between event message retries (seconds) |
BROKKR__AGENT__HEALTH_PORT | Integer | 8080 | Port for agent health check HTTP server |
BROKKR__AGENT__DEPLOYMENT_HEALTH_ENABLED | Boolean | true | Enable deployment health checking |
BROKKR__AGENT__DEPLOYMENT_HEALTH_INTERVAL | Integer | 60 | Interval for deployment health checks (seconds) |
PAK (Pre-Authentication Key) Generation
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__PAK__PREFIX | String | brokkr | Prefix for generated PAKs |
BROKKR__PAK__RNG | String | osrng | Random number generator type |
BROKKR__PAK__DIGEST | Integer | 8 | Digest algorithm identifier |
BROKKR__PAK__SHORT_TOKEN_LENGTH | Integer | 8 | Length of the short token portion |
BROKKR__PAK__LONG_TOKEN_LENGTH | Integer | 24 | Length of the long token portion |
BROKKR__PAK__SHORT_TOKEN_PREFIX | String | BR | Prefix for the short token |
Generated PAK format: {prefix}_{short_token_prefix}{short_token}_{long_token}
Example: brokkr_BR3rVsDa_GK3QN7CDUzYc6iKgMkJ98M2WSimM5t6U8
CORS
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__CORS__ALLOWED_ORIGINS | String (comma-separated) | http://localhost:3001 | Allowed CORS origins. Use * to allow all (not recommended for production). |
BROKKR__CORS__ALLOWED_METHODS | String (comma-separated) | GET,POST,PUT,DELETE,OPTIONS | Allowed HTTP methods |
BROKKR__CORS__ALLOWED_HEADERS | String (comma-separated) | Authorization,Content-Type | Allowed request headers |
BROKKR__CORS__MAX_AGE_SECONDS | Integer | 3600 | Preflight response cache duration (seconds) |
Note: Array-type CORS settings accept comma-separated strings when set via environment variables (e.g.,
BROKKR__CORS__ALLOWED_ORIGINS=http://a.com,http://b.com).
Telemetry (OpenTelemetry)
Base Settings
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__TELEMETRY__ENABLED | Boolean | false | Enable OpenTelemetry tracing |
BROKKR__TELEMETRY__OTLP_ENDPOINT | String | http://localhost:4317 | OTLP gRPC endpoint for trace export |
BROKKR__TELEMETRY__SERVICE_NAME | String | brokkr | Service name for traces |
BROKKR__TELEMETRY__SAMPLING_RATE | Float | 0.1 | Sampling rate (0.0 to 1.0, where 1.0 = 100%) |
Broker-Specific Overrides
These override the base telemetry settings for the broker component only. If unset, the base value is used.
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__TELEMETRY__BROKER__ENABLED | Boolean | (inherits) | Override enabled for broker |
BROKKR__TELEMETRY__BROKER__OTLP_ENDPOINT | String | (inherits) | Override OTLP endpoint for broker |
BROKKR__TELEMETRY__BROKER__SERVICE_NAME | String | brokkr-broker | Override service name for broker |
BROKKR__TELEMETRY__BROKER__SAMPLING_RATE | Float | (inherits) | Override sampling rate for broker |
Agent-Specific Overrides
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR__TELEMETRY__AGENT__ENABLED | Boolean | (inherits) | Override enabled for agent |
BROKKR__TELEMETRY__AGENT__OTLP_ENDPOINT | String | (inherits) | Override OTLP endpoint for agent |
BROKKR__TELEMETRY__AGENT__SERVICE_NAME | String | brokkr-agent | Override service name for agent |
BROKKR__TELEMETRY__AGENT__SAMPLING_RATE | Float | (inherits) | Override sampling rate for agent |
Configuration File and Hot-Reload
These environment variables control the configuration system itself and are not part of the BROKKR__ namespace:
| Variable | Type | Default | Description |
|---|---|---|---|
BROKKR_CONFIG_FILE | String | (none) | Path to TOML configuration file |
BROKKR_CONFIG_WATCHER_ENABLED | Boolean | (auto) | Enable/disable ConfigMap hot-reload watcher |
BROKKR_CONFIG_WATCHER_DEBOUNCE_SECONDS | Integer | 5 | Debounce window for config file changes |
Hot-Reloadable Settings
The following settings can be changed at runtime without restarting the broker (via config file change or admin API):
log.levelbroker.diagnostic_cleanup_interval_secondsbroker.diagnostic_max_age_hoursbroker.webhook_delivery_interval_secondsbroker.webhook_delivery_batch_sizebroker.webhook_cleanup_retention_dayscors.allowed_originscors.max_age_seconds
Related Documentation
- Configuration Guide — configuration system overview
- CLI Reference — command-line usage
- Multi-Tenancy — schema-based isolation
- Monitoring & Observability — telemetry and metrics setup
Templates Reference
Stack templates provide reusable, parameterized Kubernetes manifests with JSON Schema validation. This reference covers the data model, API endpoints, Tera template syntax, and matching rules.
Data Model
StackTemplate
| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier |
created_at | DateTime | Creation timestamp |
updated_at | DateTime | Last update timestamp |
deleted_at | DateTime? | Soft deletion timestamp |
generator_id | UUID? | Owning generator (NULL = system template, admin-only) |
name | String | Template name (1-255 characters) |
description | String? | Optional description |
version | Integer | Version number (starts at 1, auto-increments) |
template_content | String | Tera template (Kubernetes YAML with placeholders) |
parameters_schema | String | JSON Schema defining valid parameters |
checksum | String | SHA-256 hash of template_content |
Constraints:
- Unique combination of
(generator_id, name, version) versionmust be >= 1name,template_content, andparameters_schemacannot be emptychecksumis auto-computed on creation
Template Types
| Type | generator_id | Created By | Visible To |
|---|---|---|---|
| System template | NULL | Admin | Admin + all generators |
| Generator template | UUID | Generator | Admin + owning generator |
RenderedDeploymentObject
When a template is instantiated, Brokkr records the provenance:
| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier |
deployment_object_id | UUID | Resulting deployment object |
template_id | UUID | Source template |
template_version | Integer | Version used |
template_parameters | String (JSON) | Parameters provided |
created_at | DateTime | Instantiation timestamp |
API Endpoints
List Templates
GET /api/v1/templates
Auth: Admin sees all templates. Generator sees system templates + own templates.
Response: 200 OK — StackTemplate[]
Create Template
POST /api/v1/templates
Auth: Admin only (creates system templates). Generators can also create templates (owned by the generator).
Request body:
{
"name": "web-service",
"description": "Standard web service template",
"template_content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: {{ service_name }}\nspec:\n replicas: {{ replicas }}",
"parameters_schema": "{\"type\": \"object\", \"required\": [\"service_name\"], \"properties\": {\"service_name\": {\"type\": \"string\"}, \"replicas\": {\"type\": \"integer\", \"default\": 2}}}"
}
Validation:
- Template content is validated for Tera syntax errors
- Parameters schema is validated as a valid JSON Schema
- Name must be 1-255 characters
Response: 201 Created — StackTemplate
Get Template
GET /api/v1/templates/{id}
Auth: Admin or owning generator.
Response: 200 OK — StackTemplate
Update Template (New Version)
PUT /api/v1/templates/{id}
Auth: Admin or owning generator.
Updating a template creates a new version. The previous version remains available. The version number auto-increments.
Request body:
{
"description": "Standard web service template v2",
"template_content": "...",
"parameters_schema": "..."
}
Note: The
namefield is not accepted on update — it is preserved from the existing template.
Response: 200 OK — StackTemplate (with incremented version)
Delete Template
DELETE /api/v1/templates/{id}
Auth: Admin or owning generator.
Response: 204 No Content
Template Labels
GET /api/v1/templates/{id}/labels
POST /api/v1/templates/{id}/labels Body: "label-string"
DELETE /api/v1/templates/{id}/labels/{label}
Auth: Admin or owning generator.
Labels control which stacks a template can be instantiated into. See Matching Rules.
Template Annotations
GET /api/v1/templates/{id}/annotations
POST /api/v1/templates/{id}/annotations Body: {"key": "k", "value": "v"}
DELETE /api/v1/templates/{id}/annotations/{key}
Auth: Admin or owning generator.
Instantiate Template
POST /api/v1/stacks/{stack_id}/deployment-objects/from-template
Auth: Admin or owning generator (for the stack).
Request body:
{
"template_id": "uuid-of-template",
"parameters": {
"service_name": "frontend",
"replicas": 3
}
}
Process:
- Fetches the latest version of the template
- Validates parameters against the JSON Schema
- Checks template-to-stack matching rules (labels/annotations)
- Renders the Tera template with the provided parameters
- Creates a deployment object with the rendered YAML
- Records the rendered deployment object provenance
Response: 200 OK — DeploymentObject[]
Tera Template Syntax
Templates use the Tera engine. Key features:
Variable Substitution
name: {{ service_name }}
replicas: {{ replicas }}
image: {{ repository }}:{{ tag }}
Conditionals
{% if enable_hpa %}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ service_name }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ service_name }}
minReplicas: {{ min_replicas }}
maxReplicas: {{ max_replicas }}
{% endif %}
Loops
env:
{% for key, value in env_vars %}
- name: {{ key }}
value: "{{ value }}"
{% endfor %}
Filters
| Filter | Usage | Result |
|---|---|---|
default | {{ x | default(value="y") }} | Use “y” if x is undefined |
upper | {{ x | upper }} | Uppercase |
lower | {{ x | lower }} | Lowercase |
trim | {{ x | trim }} | Strip whitespace |
replace | {{ x | replace(from="a", to="b") }} | String replacement |
json_encode | {{ x | json_encode }} | JSON-encode value |
See the Tera documentation for the complete filter and function reference.
JSON Schema for Parameters
The parameters_schema field accepts a standard JSON Schema document. Commonly used features:
Type Constraints
{
"type": "object",
"properties": {
"replicas": { "type": "integer", "minimum": 1, "maximum": 100 },
"name": { "type": "string", "minLength": 1, "maxLength": 63 },
"debug": { "type": "boolean" },
"cpu": { "type": "string", "pattern": "^[0-9]+m$" }
}
}
Required Fields
{
"type": "object",
"required": ["name", "image"],
"properties": {
"name": { "type": "string" },
"image": { "type": "string" }
}
}
Defaults
{
"properties": {
"replicas": { "type": "integer", "default": 2 },
"port": { "type": "integer", "default": 8080 }
}
}
Enum Values
{
"properties": {
"environment": {
"type": "string",
"enum": ["development", "staging", "production"]
}
}
}
Matching Rules
Templates with labels or annotations are restricted to stacks with matching metadata. This prevents production-only templates from being instantiated into staging stacks.
Rules:
- Template with no labels and no annotations → matches any stack (universal)
- Template with labels → stack must have all of the template’s labels
- Template with annotations → stack must have all of the template’s annotations (key-value match)
- Template with both → stack must satisfy both label AND annotation requirements
Example:
Template with labels ["env:production", "tier:frontend"]:
- Stack with
["env:production", "tier:frontend", "region:us"]→ matches (has all required) - Stack with
["env:production"]→ no match (missingtier:frontend) - Stack with
["env:staging", "tier:frontend"]→ no match (wrong env)
Versioning Behavior
- Creating a template starts at version 1
- Updating via
PUTauto-increments the version - Instantiation always uses the latest version of the template
- Old versions remain in the database for provenance
- Deployment objects rendered from old versions are not affected by template updates
- The
rendered_deployment_objectstable records which version was used
Related Documentation
- Using Stack Templates — how-to guide for template workflows
- Tutorial: Standardized Deployments — step-by-step tutorial
- API Reference — complete API documentation
- Data Model — entity relationships
Work Orders
Work orders are transient operations that Brokkr routes to agents for execution. Unlike deployment objects which represent persistent desired state, work orders are one-time operations such as container builds, tests, or backups.
Concepts
Work Order vs Deployment Object
| Aspect | Deployment Object | Work Order |
|---|---|---|
| Purpose | Persistent state | One-time operation |
| Lifecycle | Applied, reconciled, deleted | Created, claimed, completed |
| Examples | Deployments, ConfigMaps | Container builds, tests |
| Storage | Permanent in stack | Moved to log after completion |
Work Order Lifecycle
PENDING -> CLAIMED -> (success) -> work_order_log
\-> (failure) -> RETRY_PENDING -> PENDING (retry)
\-> work_order_log (max retries)
- PENDING: Work order created, waiting for an agent to claim
- CLAIMED: Agent has claimed the work order and is executing
- RETRY_PENDING: Execution failed, waiting for retry backoff
- Completed: Moved to
work_order_log(success or max retries exceeded)
Targeting
Work orders are routed to agents using the same targeting mechanisms as stacks:
- Direct agent IDs: Route to specific agents by UUID
- Labels: Route to agents with matching labels (OR logic)
- Annotations: Route to agents with matching annotations (OR logic)
An agent can claim a work order if it matches ANY of the specified targeting criteria.
Work Types
Build (build)
Container image builds using Shipwright. The yaml_content should contain a Shipwright Build specification.
See Container Builds with Shipwright for details.
API Reference
Create Work Order
POST /api/v1/work-orders
Authorization: Bearer <admin-pak>
Content-Type: application/json
{
"work_type": "build",
"yaml_content": "<shipwright-build-yaml>",
"max_retries": 3,
"backoff_seconds": 60,
"claim_timeout_seconds": 3600,
"targeting": {
"labels": ["env=dev"],
"annotations": {"capability": "builder"}
}
}
Parameters:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
work_type | string | Yes | - | Type of work (e.g., “build”) |
yaml_content | string | Yes | - | YAML content for the work |
max_retries | integer | No | 3 | Maximum retry attempts |
backoff_seconds | integer | No | 60 | Base backoff for exponential retry |
claim_timeout_seconds | integer | No | 3600 | Seconds before claimed work is considered stale |
targeting | object | Yes | - | Targeting configuration |
targeting.agent_ids | array | No | - | Direct agent UUIDs |
targeting.labels | array | No | - | Agent labels to match |
targeting.annotations | object | No | - | Agent annotations to match |
List Work Orders
GET /api/v1/work-orders?status=PENDING&work_type=build
Authorization: Bearer <admin-pak>
Query Parameters:
| Parameter | Description |
|---|---|
status | Filter by status (PENDING, CLAIMED, RETRY_PENDING) |
work_type | Filter by work type |
Get Work Order
GET /api/v1/work-orders/:id
Authorization: Bearer <admin-pak>
Cancel Work Order
DELETE /api/v1/work-orders/:id
Authorization: Bearer <admin-pak>
Get Pending Work Orders (Agent)
GET /api/v1/agents/:agent_id/work-orders/pending?work_type=build
Authorization: Bearer <agent-pak>
Returns work orders that the agent can claim based on targeting rules.
Claim Work Order (Agent)
POST /api/v1/work-orders/:id/claim
Authorization: Bearer <agent-pak>
Content-Type: application/json
{
"agent_id": "<agent-uuid>"
}
Atomically claims the work order. Returns 404 Not Found if the work order does not exist or is not in a claimable state.
Complete Work Order (Agent)
POST /api/v1/work-orders/:id/complete
Authorization: Bearer <agent-pak>
Content-Type: application/json
{
"success": true,
"message": "sha256:abc123..."
}
Parameters:
| Field | Type | Description |
|---|---|---|
success | boolean | Whether the work completed successfully |
message | string | Optional result message (image digest on success, error on failure) |
retryable | boolean | Whether the work order can be retried on failure (default: true) |
Get Work Order Details
When retrieving a work order, the response includes error tracking fields:
| Field | Type | Description |
|---|---|---|
last_error | string | Error message from the most recent failed attempt (null if no failures) |
last_error_at | timestamp | When the last error occurred (null if no failures) |
retry_count | integer | Number of retry attempts so far |
next_retry_after | timestamp | When the work order will be eligible for retry (null if not in retry) |
These fields help diagnose failed work orders without needing to check the work order log.
Work Order Log
Completed work orders are moved to the log for audit purposes.
# List completed work orders
GET /api/v1/work-order-log?work_type=build&success=true&limit=100
Authorization: Bearer <admin-pak>
# Get specific completed work order
GET /api/v1/work-order-log/:id
Authorization: Bearer <admin-pak>
Query Parameters:
| Parameter | Description |
|---|---|
work_type | Filter by work type |
success | Filter by success status (true/false) |
agent_id | Filter by agent that executed |
limit | Maximum results to return |
Retry Behavior
When a work order fails:
- Agent reports failure via
/completewithsuccess: false - Broker increments
retry_count - If
retry_count < max_retries:- Status set to
RETRY_PENDING next_retry_aftercalculated with exponential backoff- After backoff period, status returns to
PENDING
- Status set to
- If
retry_count >= max_retries:- Work order moved to
work_order_logwithsuccess: false
- Work order moved to
Backoff Formula:
next_retry_after = now + (backoff_seconds * 2^retry_count)
Stale Claim Detection
The broker runs a background job every 30 seconds to detect and recover stale claims. A claim is considered stale when an agent has held a work order for longer than claim_timeout_seconds without completing it.
When a stale claim is detected:
- The work order’s
claimed_attimestamp is compared against the current time - If the elapsed time exceeds
claim_timeout_seconds, the claim is released - The work order status returns to
PENDING - The
claimed_byfield is cleared, allowing any eligible agent to claim it - The
retry_countis incremented (counts as a failed attempt)
This mechanism handles several failure scenarios:
- Agent crashes: If an agent crashes mid-execution, the work order becomes claimable again
- Network partitions: If an agent loses connectivity, work doesn’t remain stuck indefinitely
- Slow operations: Legitimate long-running operations should set an appropriate
claim_timeout_secondsvalue
The default claim_timeout_seconds is 3600 (1 hour). For build operations that may take longer, increase this value in the work order creation request.
Example: Container Build
# Create a build work order
curl -X POST http://localhost:3000/api/v1/work-orders \
-H "Authorization: Bearer $ADMIN_PAK" \
-H "Content-Type: application/json" \
-d '{
"work_type": "build",
"yaml_content": "apiVersion: shipwright.io/v1beta1\nkind: Build\nmetadata:\n name: my-build\nspec:\n source:\n type: Git\n git:\n url: https://github.com/org/repo\n strategy:\n name: buildah\n kind: ClusterBuildStrategy\n output:\n image: ttl.sh/my-image:latest",
"targeting": {
"labels": ["capability=builder"]
}
}'
# Check status
curl http://localhost:3000/api/v1/work-orders/$WORK_ORDER_ID \
-H "Authorization: Bearer $ADMIN_PAK"
# View completed builds
curl "http://localhost:3000/api/v1/work-order-log?work_type=build" \
-H "Authorization: Bearer $ADMIN_PAK"
Database Schema
Work orders use a two-table design:
work_orders: Active queue for routing and retry managementwork_order_log: Permanent audit trail of completed work
This separation optimizes queue operations while maintaining complete execution history.
Webhooks Reference
This reference documents Brokkr’s webhook system for receiving real-time event notifications via HTTP callbacks.
Overview
Webhooks enable external systems to receive notifications when events occur in Brokkr. The system supports:
- Subscription-based event filtering
- Broker or agent-side delivery
- Automatic retries with exponential backoff
- Encrypted URL and authentication storage
Event Types
Agent Events
| Event Type | Description | Payload Fields |
|---|---|---|
agent.registered | Agent registered with broker | agent_id, name, cluster |
agent.deregistered | Agent deregistered | agent_id, name |
Stack Events
| Event Type | Description | Payload Fields |
|---|---|---|
stack.created | New stack created | stack_id, name, created_at |
stack.deleted | Stack soft-deleted | stack_id, deleted_at |
Deployment Events
| Event Type | Description | Payload Fields |
|---|---|---|
deployment.created | New deployment object created | deployment_object_id, stack_id, sequence_id |
deployment.applied | Deployment successfully applied by agent | deployment_object_id, agent_id, status |
deployment.failed | Deployment failed to apply | deployment_object_id, agent_id, error |
deployment.deleted | Deployment object soft-deleted | deployment_object_id, stack_id |
Work Order Events
| Event Type | Description | Payload Fields |
|---|---|---|
workorder.created | New work order created | work_order_id, work_type, status |
workorder.claimed | Work order claimed by agent | work_order_id, agent_id, claimed_at |
workorder.completed | Work order completed successfully | work_order_log_id, work_type, success, result_message |
workorder.failed | Work order failed | work_order_log_id, work_type, success, result_message |
Wildcard Patterns
| Pattern | Matches |
|---|---|
agent.* | All agent events |
stack.* | All stack events |
deployment.* | All deployment events |
workorder.* | All work order events |
* | All events |
API Reference
Subscription Endpoints
List Subscriptions
GET /api/v1/webhooks
Authorization: Bearer <admin_pak>
Response:
[
{
"id": "uuid",
"name": "string",
"has_url": true,
"has_auth_header": false,
"event_types": ["deployment.*"],
"filters": null,
"target_labels": null,
"enabled": true,
"max_retries": 5,
"timeout_seconds": 30,
"created_at": "2025-01-02T10:00:00Z",
"updated_at": "2025-01-02T10:00:00Z",
"created_by": "admin"
}
]
Create Subscription
POST /api/v1/webhooks
Authorization: Bearer <admin_pak>
Content-Type: application/json
Request body:
{
"name": "string (required)",
"url": "string (required, http:// or https://)",
"auth_header": "string (optional)",
"event_types": ["string (required, at least one)"],
"filters": {
"agent_id": "uuid (optional)",
"stack_id": "uuid (optional)",
"labels": {"key": "value"}
},
"target_labels": ["string (optional)"],
"max_retries": 5,
"timeout_seconds": 30,
"validate": false
}
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Human-readable subscription name |
url | string | required | Webhook endpoint URL (encrypted at rest) |
auth_header | string | null | Authorization header value (encrypted at rest) |
event_types | string[] | required | Event types to subscribe to |
filters | object | null | Filter events by agent/stack/labels |
target_labels | string[] | null | Labels for agent-based delivery |
max_retries | int | 5 | Maximum delivery retry attempts |
timeout_seconds | int | 30 | HTTP request timeout |
validate | bool | false | Send test request on creation |
Response: 201 Created with subscription object
Get Subscription
GET /api/v1/webhooks/{id}
Authorization: Bearer <admin_pak>
Response: 200 OK with subscription object
Update Subscription
PUT /api/v1/webhooks/{id}
Authorization: Bearer <admin_pak>
Content-Type: application/json
Request body (all fields optional):
{
"name": "string",
"url": "string",
"auth_header": "string or null",
"event_types": ["string"],
"filters": {},
"target_labels": ["string"] or null,
"enabled": true,
"max_retries": 5,
"timeout_seconds": 30
}
Response: 200 OK with updated subscription object
Delete Subscription
DELETE /api/v1/webhooks/{id}
Authorization: Bearer <admin_pak>
Response: 204 No Content
Test Subscription
POST /api/v1/webhooks/{id}/test
Authorization: Bearer <admin_pak>
Sends a test event to the webhook endpoint.
Response:
{
"success": true,
"status_code": 200,
"message": "Test delivery successful"
}
List Event Types
GET /api/v1/webhooks/event-types
Authorization: Bearer <admin_pak>
Response:
[
"agent.registered",
"agent.deregistered",
"stack.created",
"stack.deleted",
"deployment.created",
"deployment.applied",
"deployment.failed",
"deployment.deleted",
"workorder.created",
"workorder.claimed",
"workorder.completed",
"workorder.failed"
]
Delivery Endpoints
List Deliveries
GET /api/v1/webhooks/{id}/deliveries
Authorization: Bearer <admin_pak>
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | null | Filter by status |
limit | int | 50 | Maximum results |
offset | int | 0 | Pagination offset |
Response:
[
{
"id": "uuid",
"subscription_id": "uuid",
"event_type": "deployment.applied",
"event_id": "uuid",
"payload": "{}",
"target_labels": null,
"status": "success",
"acquired_by": null,
"acquired_until": null,
"attempts": 1,
"last_attempt_at": "2025-01-02T10:00:00Z",
"next_retry_at": null,
"last_error": null,
"created_at": "2025-01-02T10:00:00Z",
"completed_at": "2025-01-02T10:00:01Z"
}
]
Delivery Modes
Broker Delivery (Default)
When target_labels is null or empty, the broker delivers webhooks directly:
- Event occurs and is emitted
- Broker matches event to subscriptions
- Broker creates delivery records
- Background task claims and delivers via HTTP POST
- Success/failure is recorded
Use for external endpoints accessible from the broker.
Agent Delivery
When target_labels is set, matching agents deliver webhooks:
- Event occurs and is emitted
- Broker creates delivery with
target_labels - Agent polls for pending deliveries during heartbeat loop
- Agent claims deliveries matching its labels
- Agent delivers via HTTP POST from inside cluster
- Agent reports result back to broker
Use for in-cluster endpoints (e.g., *.svc.cluster.local) that the broker cannot reach.
Label Matching
An agent can claim a delivery only if it has ALL the specified target labels:
| Subscription Labels | Agent Labels | Can Claim? |
|---|---|---|
["env:prod"] | ["env:prod", "region:us"] | Yes |
["env:prod", "region:us"] | ["env:prod"] | No |
["env:prod"] | ["env:staging"] | No |
Delivery Status
| Status | Description |
|---|---|
pending | Waiting to be claimed and delivered |
acquired | Claimed by broker or agent, delivery in progress |
success | Successfully delivered (HTTP 2xx) |
failed | Delivery failed, will retry after backoff |
dead | Max retries exceeded, no more attempts |
State Transitions
pending → acquired → success
→ failed → pending (after backoff)
→ dead (if max_retries exceeded)
Retry Behavior
- Exponential backoff: 2^attempts seconds (2s, 4s, 8s, 16s…)
- Retryable errors: HTTP 5xx, timeouts, connection failures
- Non-retryable errors: HTTP 4xx (except 429)
- TTL: Acquired deliveries expire after 60 seconds if no result reported
Webhook Payload Format
HTTP Headers
Content-Type: application/json
X-Brokkr-Event-Type: deployment.applied
X-Brokkr-Delivery-Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Authorization: <configured auth_header>
Body Structure
{
"id": "event-uuid",
"event_type": "deployment.applied",
"timestamp": "2025-01-02T10:00:00Z",
"data": {
// Event-specific fields
}
}
Example Payloads
deployment.applied
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"event_type": "deployment.applied",
"timestamp": "2025-01-02T10:00:00Z",
"data": {
"deployment_object_id": "a1b2c3d4-...",
"agent_id": "e5f6g7h8-...",
"status": "SUCCESS"
}
}
workorder.completed
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"event_type": "workorder.completed",
"timestamp": "2025-01-02T10:05:00Z",
"data": {
"work_order_log_id": "b2c3d4e5-...",
"work_type": "custom",
"success": true,
"result_message": "Applied 3 resources successfully",
"agent_id": "e5f6g7h8-...",
"completed_at": "2025-01-02T10:05:00Z"
}
}
workorder.failed
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"event_type": "workorder.failed",
"timestamp": "2025-01-02T10:05:00Z",
"data": {
"work_order_log_id": "c3d4e5f6-...",
"work_type": "build",
"success": false,
"result_message": "Build failed: Dockerfile not found",
"agent_id": "e5f6g7h8-...",
"completed_at": "2025-01-02T10:05:00Z"
}
}
Database Schema
webhook_subscriptions
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
name | VARCHAR(255) | Subscription name |
url_encrypted | BYTEA | Encrypted webhook URL |
auth_header_encrypted | BYTEA | Encrypted auth header (nullable) |
event_types | TEXT[] | Event type patterns |
filters | TEXT | JSON-encoded filters (nullable) |
target_labels | TEXT[] | Labels for agent delivery (nullable) |
enabled | BOOLEAN | Whether subscription is active |
max_retries | INT | Max delivery attempts |
timeout_seconds | INT | HTTP timeout |
created_at | TIMESTAMP | Creation timestamp |
updated_at | TIMESTAMP | Last update timestamp |
created_by | VARCHAR(255) | Creator identifier |
webhook_deliveries
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
subscription_id | UUID | Foreign key to subscription |
event_type | VARCHAR(100) | Event type |
event_id | UUID | Idempotency key |
payload | TEXT | JSON event payload |
target_labels | TEXT[] | Copied from subscription |
status | VARCHAR(20) | Delivery status |
acquired_by | UUID | Agent ID (nullable, NULL = broker) |
acquired_until | TIMESTAMP | TTL for claim |
attempts | INT | Number of attempts |
last_attempt_at | TIMESTAMP | Last attempt time |
next_retry_at | TIMESTAMP | Next retry time |
last_error | TEXT | Error from last attempt |
created_at | TIMESTAMP | Creation timestamp |
completed_at | TIMESTAMP | Completion timestamp |
Security Considerations
Encryption at Rest
Webhook URLs and authentication headers contain sensitive information and are encrypted before storage:
- Algorithm: AES-256-GCM (Authenticated Encryption with Associated Data)
- Key management: Encryption key configured via
BROKKR__WEBHOOKS__ENCRYPTION_KEYenvironment variable - Fields encrypted:
url_encrypted,auth_header_encrypted - Response handling: API responses show
has_url: trueandhas_auth_header: true/falserather than the actual values
When updating a subscription, provide the URL or auth header to re-encrypt with the current key.
Access Control
- Admin-only access: All webhook endpoints require admin PAK authentication
- Agent authentication: Agents use their PAK to fetch and report deliveries
- TLS recommended: Use HTTPS endpoints in production
- Secret rotation: Rotate auth headers by updating the subscription
Data Retention
The webhook system automatically cleans up old delivery records to prevent unbounded database growth:
- Retention period: 7 days
- Cleanup frequency: Every hour
- Scope: All deliveries older than 7 days are permanently deleted, regardless of status
- Subscriptions: Deleted when explicitly removed; delivery history is cleaned up by the retention policy
Deliveries in terminal states (success, dead) are retained for the full 7-day period to support troubleshooting and audit requirements. Adjust retention by modifying the cleanup background task configuration if needed.
Performance Characteristics
Broker Delivery
- Background task polls every 5 seconds
- Batch size: 10 deliveries per cycle
- Concurrent delivery: single-threaded per broker instance
Agent Delivery
- Polling interval: 10 seconds
- Batch size: 10 deliveries per poll
- Concurrent delivery: single-threaded per agent
- TTL: 60 seconds for acquired deliveries
Scaling Considerations
- Multiple broker instances share the delivery workload
- Agent delivery scales with number of matching agents
- Delivery latency: typically < 15 seconds from event to delivery
Related Documentation
- How to Configure Webhooks - Step-by-step setup guide
- Architecture - System architecture overview
- Data Flows - Event flow through the system
Generators API Reference
This reference documents the API endpoints for managing generators in Brokkr.
Overview
Generators are identity principals that enable external systems (CI/CD pipelines, automation tools) to authenticate with Brokkr and manage resources. Each generator has its own Pre-Authentication Key (PAK) and can only access resources it created.
Data Model
Generator Object
| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier |
name | string | Human-readable name (unique, non-null) |
description | string | Optional description |
pak_hash | string | Hashed PAK (never returned in API responses) |
created_at | timestamp | Creation timestamp |
updated_at | timestamp | Last update timestamp |
deleted_at | timestamp | Soft-delete timestamp (null if active) |
last_active_at | timestamp | Last activity timestamp (null if never active) |
is_active | boolean | Whether the generator is currently active |
NewGenerator Object
Used when creating a generator:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique name for the generator |
description | string | No | Optional description |
API Endpoints
List Generators
List all generators. Requires admin access.
GET /api/v1/generators
Authorization: Bearer <admin_pak>
Response: 200 OK
[
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "github-actions-prod",
"description": "Production deployment pipeline",
"created_at": "2025-01-02T10:00:00Z",
"updated_at": "2025-01-02T10:00:00Z",
"deleted_at": null
}
]
Error Responses:
| Status | Description |
|---|---|
| 403 | Admin access required |
| 500 | Internal server error |
Create Generator
Create a new generator and receive its PAK. Requires admin access.
POST /api/v1/generators
Authorization: Bearer <admin_pak>
Content-Type: application/json
Request Body:
{
"name": "github-actions-prod",
"description": "Production deployment pipeline"
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique name (max 255 characters) |
description | string | No | Optional description |
Response: 200 OK
{
"generator": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "github-actions-prod",
"description": "Production deployment pipeline",
"created_at": "2025-01-02T10:00:00Z",
"updated_at": "2025-01-02T10:00:00Z",
"deleted_at": null
},
"pak": "brk_gen_abc123...xyz789"
}
The pak field is only returned once at creation time. Store it securely immediately.
Error Responses:
| Status | Description |
|---|---|
| 400 | Invalid generator data (e.g., duplicate name) |
| 403 | Admin access required |
| 500 | Internal server error |
Get Generator
Retrieve a specific generator by ID. Accessible by admin or the generator itself.
GET /api/v1/generators/{id}
Authorization: Bearer <admin_pak | generator_pak>
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | UUID | Generator ID |
Response: 200 OK
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "github-actions-prod",
"description": "Production deployment pipeline",
"created_at": "2025-01-02T10:00:00Z",
"updated_at": "2025-01-02T10:00:00Z",
"deleted_at": null
}
Error Responses:
| Status | Description |
|---|---|
| 403 | Unauthorized access (not admin and not the generator) |
| 404 | Generator not found |
| 500 | Internal server error |
Update Generator
Update a generator’s metadata. Accessible by admin or the generator itself.
PUT /api/v1/generators/{id}
Authorization: Bearer <admin_pak | generator_pak>
Content-Type: application/json
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | UUID | Generator ID |
Request Body:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "github-actions-prod",
"description": "Updated description"
}
All fields from the Generator object can be provided, though id, created_at, and pak_hash are ignored if included.
Response: 200 OK
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "github-actions-prod",
"description": "Updated description",
"created_at": "2025-01-02T10:00:00Z",
"updated_at": "2025-01-02T11:00:00Z",
"deleted_at": null
}
Error Responses:
| Status | Description |
|---|---|
| 403 | Unauthorized access |
| 404 | Generator not found |
| 500 | Internal server error |
Delete Generator
Soft-delete a generator. Accessible by admin or the generator itself.
DELETE /api/v1/generators/{id}
Authorization: Bearer <admin_pak | generator_pak>
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | UUID | Generator ID |
Response: 204 No Content
The generator is soft-deleted (marked with deleted_at timestamp). A database trigger cascades the soft-delete to all stacks owned by this generator and their deployment objects.
Error Responses:
| Status | Description |
|---|---|
| 403 | Unauthorized access |
| 404 | Generator not found |
| 500 | Internal server error |
Rotate Generator PAK
Generate a new PAK for the generator, invalidating the previous one. Accessible by admin or the generator itself.
POST /api/v1/generators/{id}/rotate-pak
Authorization: Bearer <admin_pak | generator_pak>
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | UUID | Generator ID |
Response: 200 OK
{
"generator": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "github-actions-prod",
"description": "Production deployment pipeline",
"created_at": "2025-01-02T10:00:00Z",
"updated_at": "2025-01-02T12:00:00Z",
"deleted_at": null
},
"pak": "brk_gen_new123...newxyz"
}
The old PAK is immediately invalidated. Store the new PAK securely and update all systems using the old PAK.
Error Responses:
| Status | Description |
|---|---|
| 403 | Unauthorized access |
| 404 | Generator not found |
| 500 | Internal server error |
Authentication
PAK Format
Generator PAKs follow the format: brk_gen_<random_string>
The prefix identifies this as a generator PAK (as opposed to admin or agent PAKs).
Authorization Header
Include the PAK in the Authorization header:
Authorization: Bearer brk_gen_abc123...xyz789
Permission Model
| Operation | Admin PAK | Generator PAK (own) | Generator PAK (other) |
|---|---|---|---|
| List generators | Yes | No | No |
| Create generator | Yes | No | No |
| Get generator | Yes | Yes | No |
| Update generator | Yes | Yes | No |
| Delete generator | Yes | Yes | No |
| Rotate PAK | Yes | Yes | No |
Resource Scoping
Resources created by a generator are scoped to that generator:
Stacks
When a generator creates a stack, the stack’s generator_id is set to the generator’s ID. The generator can only view and modify its own stacks.
Templates
Templates can be:
- Generator-scoped: Created by a generator, only visible to that generator
- System templates: Created by admin (no
generator_id), visible to all generators
Deployment Objects
Deployment objects inherit the generator_id from their parent stack.
Database Schema
generators Table
| Column | Type | Constraints |
|---|---|---|
id | UUID | PRIMARY KEY, DEFAULT uuid_generate_v4() |
name | VARCHAR(255) | NOT NULL, UNIQUE |
description | TEXT | |
pak_hash | VARCHAR(255) | |
created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() |
updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() |
deleted_at | TIMESTAMP | NULL (soft delete) |
last_active_at | TIMESTAMP | NULL |
is_active | BOOLEAN | NOT NULL, DEFAULT false |
Unique Constraint
The name column has a partial unique index excluding soft-deleted rows:
CREATE UNIQUE INDEX generators_name_unique
ON generators (name)
WHERE deleted_at IS NULL;
This allows reusing names after a generator is deleted.
Related Documentation
- Working with Generators - How-to guide
- Stack Templates - Using templates with generators
- Authentication - Security model overview
Diagnostics Reference
Brokkr provides an on-demand diagnostic system for collecting Kubernetes pod information, events, and logs from remote clusters. Administrators request diagnostics through the broker API, and agents collect the data from their local clusters.
Diagnostic Request Lifecycle
Created (pending) → Claimed (by agent) → Result submitted (completed)
→ Expired (past retention)
→ Failed (agent error)
Status Values
| Status | Description |
|---|---|
pending | Request created, waiting for agent to claim |
claimed | Agent has claimed the request and is collecting data |
completed | Agent submitted diagnostic results |
failed | Agent encountered an error during collection |
expired | Request exceeded its retention period without completion |
Data Model
DiagnosticRequest
| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier |
agent_id | UUID | Target agent to collect from |
deployment_object_id | UUID | Deployment object to diagnose |
status | String | Current status (see above) |
requested_by | String? | Who requested the diagnostic (free-text) |
created_at | DateTime | Request creation time |
claimed_at | DateTime? | When agent claimed the request |
completed_at | DateTime? | When result was submitted |
expires_at | DateTime | When the request expires |
DiagnosticResult
| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier |
request_id | UUID | Associated diagnostic request |
pod_statuses | String (JSON) | Pod status information |
events | String (JSON) | Kubernetes events |
log_tails | String? (JSON) | Container log tails (last 100 lines per container) |
collected_at | DateTime | When data was collected on the agent |
created_at | DateTime | Record creation time |
API Endpoints
Create Diagnostic Request
POST /api/v1/deployment-objects/{deployment_object_id}/diagnostics
Auth: Admin only.
Request body:
{
"agent_id": "uuid-of-target-agent",
"requested_by": "oncall-engineer",
"retention_minutes": 60
}
| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
agent_id | UUID | Yes | — | Must be a valid agent |
requested_by | String | No | null | Free-text identifier |
retention_minutes | Integer | No | 60 | 1-1440 (max 24 hours) |
Response: 201 Created
{
"id": "diag-uuid",
"agent_id": "agent-uuid",
"deployment_object_id": "do-uuid",
"status": "pending",
"requested_by": "oncall-engineer",
"created_at": "2025-01-15T10:00:00Z",
"expires_at": "2025-01-15T11:00:00Z"
}
Get Diagnostic
GET /api/v1/diagnostics/{id}
Auth: Admin or the target agent.
Response: 200 OK
If the diagnostic is completed, the response includes the result:
{
"request": {
"id": "diag-uuid",
"status": "completed",
"claimed_at": "2025-01-15T10:00:15Z",
"completed_at": "2025-01-15T10:00:20Z"
},
"result": {
"pod_statuses": "[{\"name\": \"myapp-abc12\", \"namespace\": \"default\", \"phase\": \"Running\", ...}]",
"events": "[{\"event_type\": \"Normal\", \"reason\": \"Pulled\", ...}]",
"log_tails": "{\"myapp-abc12/myapp\": \"2025-01-15 10:00:00 INFO Starting...\\n...\"}",
"collected_at": "2025-01-15T10:00:18Z"
}
}
Get Pending Diagnostics (Agent)
GET /api/v1/agents/{agent_id}/diagnostics/pending
Auth: Agent (own ID only).
Returns all pending diagnostic requests for the agent.
Response: 200 OK — DiagnosticRequest[]
Claim Diagnostic Request
POST /api/v1/diagnostics/{id}/claim
Auth: Agent.
Transitions the request from pending to claimed. Only one agent can claim a request.
Response: 200 OK
Submit Diagnostic Result
POST /api/v1/diagnostics/{id}/result
Auth: Agent (must have claimed the request).
Request body:
{
"pod_statuses": "[{\"name\": \"myapp-abc12\", \"namespace\": \"default\", \"phase\": \"Running\", \"conditions\": [{\"condition_type\": \"Ready\", \"status\": \"True\"}], \"containers\": [{\"name\": \"myapp\", \"ready\": true, \"restart_count\": 0, \"state\": \"running\"}]}]",
"events": "[{\"event_type\": \"Normal\", \"reason\": \"Pulled\", \"message\": \"Successfully pulled image\", \"involved_object_kind\": \"Pod\", \"involved_object_name\": \"myapp-abc12\", \"count\": 1}]",
"log_tails": "{\"myapp-abc12/myapp\": \"2025-01-15 10:00:00 INFO Starting server on :8080\\n2025-01-15 10:00:01 INFO Ready to accept connections\"}",
"collected_at": "2025-01-15T10:00:18Z"
}
Response: 201 Created
Collected Data
Pod Statuses
Each pod status includes:
| Field | Type | Description |
|---|---|---|
name | String | Pod name |
namespace | String | Pod namespace |
phase | String | Pod phase (Running, Pending, Failed, etc.) |
conditions | Array | Pod conditions (Ready, Initialized, etc.) |
containers | Array | Container statuses |
Container status fields:
| Field | Type | Description |
|---|---|---|
name | String | Container name |
ready | Boolean | Whether the container is ready |
restart_count | Integer | Number of restarts |
state | String | Current state (running, waiting, terminated) |
state_reason | String? | Reason for waiting/terminated state |
state_message | String? | Message for waiting/terminated state |
Events
| Field | Type | Description |
|---|---|---|
event_type | String? | Normal or Warning |
reason | String? | Short reason string |
message | String? | Human-readable message |
involved_object_kind | String? | Kind of involved object (Pod, ReplicaSet, etc.) |
involved_object_name | String? | Name of involved object |
count | Integer? | Number of occurrences |
first_timestamp | String? | First occurrence |
last_timestamp | String? | Last occurrence |
Log Tails
A JSON object mapping pod-name/container-name to the last 100 lines of logs:
{
"myapp-abc12/myapp": "line 1\nline 2\n...",
"myapp-abc12/sidecar": "line 1\nline 2\n..."
}
The maximum log lines collected per container is 100 (configured via MAX_LOG_LINES).
Automatic Cleanup
The broker runs a background task that periodically cleans up diagnostic data:
| Setting | Default | Description |
|---|---|---|
broker.diagnostic_cleanup_interval_seconds | 900 (15 min) | How often cleanup runs |
broker.diagnostic_max_age_hours | 1 | Max age for completed/expired/failed diagnostics |
The cleanup task:
- Expires pending requests past their
expires_attime - Deletes completed, expired, and failed requests older than
diagnostic_max_age_hours - Deletes associated diagnostic results
Related Documentation
- How-To: Running On-Demand Diagnostics — step-by-step guide
- Monitoring Deployment Health — continuous health monitoring
- Health Endpoints — broker and agent health checks
Multi-Tenancy Reference
Brokkr supports multi-tenant deployments through PostgreSQL schema isolation. Each tenant gets a separate database schema, providing logical separation of all data while sharing a single database server and Brokkr broker instance.
Behavior
When database.schema is set, the broker creates the schema on startup (CREATE SCHEMA IF NOT EXISTS), sets search_path on every connection checkout, and runs migrations within the schema. Each tenant has its own complete set of tables invisible to other tenants.
Configuration
Single Tenant (Default)
No schema configuration needed. All data lives in the public schema.
[database]
url = "postgres://brokkr:password@db:5432/brokkr"
Multi-Tenant
Set the schema for each tenant’s broker instance:
[database]
url = "postgres://brokkr:password@db:5432/brokkr"
schema = "tenant_acme"
Or via environment variable:
BROKKR__DATABASE__SCHEMA=tenant_acme
Schema Name Constraints
Schema names are validated to prevent SQL injection:
- Allowed characters: alphanumeric (
a-z,A-Z,0-9) and underscores (_) - Maximum length: limited by PostgreSQL (63 characters)
- No special characters, spaces, or SQL keywords
Valid examples: tenant_acme, org_12345, production_v2
Invalid examples: tenant-acme (hyphen), drop table; (SQL injection), my schema (space)
Deployment Topology
In a multi-tenant deployment, you run one broker process per tenant, all pointing to the same PostgreSQL server but with different schema configurations:
┌──────────────────────────────────────┐
│ PostgreSQL Server │
│ ┌────────────┐ ┌────────────────┐ │
│ │ tenant_acme│ │ tenant_globex │ │
│ │ agents │ │ agents │ │
│ │ stacks │ │ stacks │ │
│ │ ... │ │ ... │ │
│ └────────────┘ └────────────────┘ │
└──────────────────────────────────────┘
▲ ▲
│ │
┌───────┴───────┐ ┌────────┴────────┐
│ Broker (Acme) │ │ Broker (Globex) │
│ schema= │ │ schema= │
│ tenant_acme │ │ tenant_globex │
└───────────────┘ └─────────────────┘
Each broker instance:
- Has its own admin PAK
- Manages its own agents and generators
- Runs its own migrations on startup
- Operates independently
Kubernetes Deployment
In Kubernetes, deploy separate broker instances per tenant using the Helm chart with different schema values:
# Tenant: Acme
helm install brokkr-acme oci://ghcr.io/colliery-io/charts/brokkr-broker \
--set postgresql.enabled=false \
--set postgresql.external.host=shared-postgres.example.com \
--set postgresql.external.database=brokkr \
--set postgresql.external.username=brokkr \
--set postgresql.external.password=secret \
--set postgresql.external.schema=tenant_acme \
--namespace brokkr-acme
# Tenant: Globex
helm install brokkr-globex oci://ghcr.io/colliery-io/charts/brokkr-broker \
--set postgresql.enabled=false \
--set postgresql.external.host=shared-postgres.example.com \
--set postgresql.external.database=brokkr \
--set postgresql.external.username=brokkr \
--set postgresql.external.password=secret \
--set postgresql.external.schema=tenant_globex \
--namespace brokkr-globex
Connection Pool Behavior
When a schema is configured:
- The connection pool calls
setup_schema(schema)at initialization - Every connection acquired from the pool automatically executes
SET search_path TO <schema>before use - This happens at the r2d2 pool level, so application code doesn’t need schema awareness
The connection pool size is 50 by default. In multi-tenant deployments with many tenants on one database server, consider the total connection count across all broker instances against PostgreSQL’s max_connections.
Data Isolation Guarantees
| Aspect | Isolation Level |
|---|---|
| Tables | Full — each schema has its own tables |
| Sequences | Full — sequence counters are per-schema |
| Migrations | Full — each schema migrates independently |
| Admin PAK | Full — each tenant has its own admin |
| Agents | Full — agents belong to one tenant |
| Generators | Full — generators belong to one tenant |
Not isolated:
- PostgreSQL server resources (CPU, memory, disk, connections)
- Network access to the database server
- Database-level settings (e.g.,
max_connections)
For stronger isolation requirements, use separate PostgreSQL databases or servers.
Migration Behavior
| Startup | What Happens |
|---|---|
| First | All migrations + admin role creation + admin PAK generation |
| Subsequent | Pending migrations only |
Each schema has its own app_initialization table. Different tenants can be at different migration versions if their broker instances are updated at different times.
Limitations
- No cross-tenant queries: the broker can only see data in its configured schema
- No tenant management API: tenants are created by configuring new broker instances; there’s no API to list or manage tenants
- Shared database resources: high load on one tenant can affect others on the same database server
- Schema name is static: changing a tenant’s schema name requires data migration
Related Documentation
- Configuration Guide — database configuration options
- Installation Guide — deployment options including external PostgreSQL
- Security Model — authentication and authorization
Soft Deletion Pattern
Brokkr uses soft deletion as the default deletion strategy for primary entities. Rather than removing records from the database, soft deletion marks records with a deleted_at timestamp while preserving all data. This design enables audit compliance, accidental deletion recovery, and maintains referential integrity across related resources.
How Soft Deletion Works
When you delete a resource through the Brokkr API, the system sets the deleted_at timestamp to the current time rather than executing a SQL DELETE. This approach has several consequences:
- The record remains in the database with all its data intact
- Standard API queries filter out records where
deleted_at IS NOT NULL - Related resources may be cascade soft-deleted depending on the entity type
- The record can potentially be recovered by clearing the
deleted_atfield - Unique constraints are scoped to only active (non-deleted) records
Entities Supporting Soft Deletion
| Entity | Cascade Behavior | API Endpoint |
|---|---|---|
| Agents | Soft deletes agent; cascade soft-deletes agent events | DELETE /api/v1/agents/{id} |
| Stacks | Cascades to deployment objects; creates deletion marker | DELETE /api/v1/stacks/{id} |
| Generators | Cascades to stacks and deployment objects | DELETE /api/v1/generators/{id} |
| Templates | Soft deletes template only | DELETE /api/v1/templates/{id} |
| Agent Events | Cascade soft-deleted when parent agent is soft-deleted | Not directly exposed |
| Deployment Objects | Soft deletes object only | Generally not exposed directly |
Cascade Behavior
Stack Deletion
When a stack is soft-deleted, the system triggers several cascading operations through database triggers:
- All deployment objects belonging to the stack are soft-deleted
- A special deletion marker deployment object is created with
is_deletion_marker: true - The deletion marker notifies agents to remove the resources from their clusters
-- Simplified trigger logic
UPDATE deployment_objects
SET deleted_at = NEW.deleted_at
WHERE stack_id = NEW.id AND deleted_at IS NULL;
INSERT INTO deployment_objects (stack_id, yaml_content, is_deletion_marker)
VALUES (NEW.id, '', TRUE);
Generator Deletion
When a generator is soft-deleted, the cascade propagates to all resources created by that generator:
- All stacks owned by the generator are soft-deleted
- All deployment objects in those stacks are soft-deleted
- Each stack’s soft deletion also creates deletion markers
This ensures that deleting a generator properly cleans up resources across all clusters that received deployments from that generator.
Agent Deletion
When an agent is soft-deleted, a database trigger cascade soft-deletes all associated agent events by setting their deleted_at timestamps.
Unique Constraints
Brokkr uses partial unique indexes that exclude soft-deleted records. This design allows you to reuse names after deleting resources:
-- Example: Stack name uniqueness
CREATE UNIQUE INDEX unique_stack_name
ON stacks (name)
WHERE deleted_at IS NULL;
This means:
- You cannot create two active stacks with the same name
- After soft-deleting a stack, you can create a new stack with the same name
- The original soft-deleted stack remains in the database with its historical data
Entities with partial unique constraints:
| Entity | Unique Fields |
|---|---|
| Agents | (name, cluster_name) |
| Stacks | name |
| Generators | name |
| Templates | (generator_id, name, version) |
Querying Deleted Records
Standard API endpoints automatically filter out soft-deleted records. However, some DAL methods allow querying including deleted records for administrative purposes:
#![allow(unused)]
fn main() {
// Standard query - excludes deleted
dal.stacks().get(vec![stack_id])
// Include deleted records
dal.stacks().get_including_deleted(stack_id)
// List all including deleted
dal.stacks().list_all()
}
These methods are primarily used internally for:
- Audit trail queries
- Database maintenance
- Recovery operations
Hard Deletion
Hard deletion (permanent removal from database) is available but used sparingly. When a hard delete occurs, additional cleanup is performed through BEFORE DELETE triggers:
Stack Hard Delete
-- Cleanup performed before hard delete
DELETE FROM agent_targets WHERE stack_id = OLD.id;
DELETE FROM agent_events WHERE deployment_object_id IN (
SELECT id FROM deployment_objects WHERE stack_id = OLD.id
);
DELETE FROM deployment_objects WHERE stack_id = OLD.id;
Hard deletion is typically reserved for:
- Data retention compliance after the retention period
- Cleaning up test data
- Emergency data removal scenarios
Recovery Considerations
While soft-deleted records remain in the database, Brokkr does not currently expose a recovery API. Manual recovery requires database access:
-- Example: Recover a soft-deleted stack
UPDATE stacks SET deleted_at = NULL WHERE id = 'stack-uuid';
-- Note: Cascade-deleted children must also be recovered
UPDATE deployment_objects SET deleted_at = NULL WHERE stack_id = 'stack-uuid';
Recovery is complicated by several factors:
- Cascade-deleted children must be individually recovered
- Deletion markers created during soft deletion remain
- Unique constraint conflicts may arise if a new resource was created with the same name
Database Schema Details
deleted_at Column
All soft-deletable entities include:
deleted_at TIMESTAMP WITH TIME ZONE
This column is:
NULLfor active records- Set to the deletion timestamp for deleted records
- Indexed for query performance:
CREATE INDEX idx_entity_deleted_at ON entity(deleted_at);
Trigger Functions
Soft deletion cascades are implemented as PostgreSQL trigger functions:
| Trigger | Table | Event | Function |
|---|---|---|---|
trigger_handle_stack_soft_delete | stacks | AFTER UPDATE of deleted_at | handle_stack_soft_delete() |
cascade_soft_delete_generators | generators | AFTER UPDATE | cascade_soft_delete_generators() |
trigger_stack_hard_delete | stacks | BEFORE DELETE | handle_stack_hard_delete() |
Performance Implications
Soft deletion has modest performance implications:
Query overhead: Every query must include WHERE deleted_at IS NULL. This is mitigated by indexing the deleted_at column.
Table growth: Soft-deleted records accumulate over time. For high-churn environments, periodic hard deletion of old soft-deleted records may be necessary.
Index size: Partial unique indexes only include active records, keeping index size proportional to active data rather than total data.
Best Practices
Prefer soft deletion: Use the standard DELETE endpoints which perform soft deletion. This preserves audit trails and enables recovery.
Monitor table growth: Track the ratio of soft-deleted to active records. Consider periodic cleanup of very old soft-deleted records.
Test recovery procedures: If recovery is important for your use case, establish and test recovery procedures before you need them.
Understand cascade effects: Before deleting generators or stacks, understand the cascade implications for dependent resources.
Related Documentation
- Data Model Design - Entity relationships and design philosophy
- Audit Logs - Tracking actions for compliance
- Managing Stacks - Stack lifecycle including deletion
Audit Logs
Brokkr maintains an immutable audit trail of administrative and security-sensitive operations. Every PAK creation, resource modification, authentication attempt, and significant system event is recorded with details about who performed the action, what was affected, and when it occurred. This documentation covers the audit log schema, available actions, query patterns, and retention policies.
Schema
Each audit log entry captures comprehensive information about an event:
| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier for the log entry |
timestamp | timestamp | When the event occurred |
actor_type | string | Type of actor: admin, agent, generator, system |
actor_id | UUID | ID of the actor (null for system or unauthenticated) |
action | string | The action performed (e.g., agent.created) |
resource_type | string | Type of resource affected (e.g., agent, stack) |
resource_id | UUID | ID of the affected resource (null if not applicable) |
details | JSON | Structured details about the action |
ip_address | string | Client IP address |
user_agent | string | Client user agent string |
created_at | timestamp | When the record was created |
Actor Types
The actor_type field identifies what kind of entity performed the action:
| Type | Description |
|---|---|
admin | Administrator using an admin PAK |
agent | An agent performing its own operations |
generator | A generator creating or managing resources |
system | System-initiated operations (background tasks, scheduled jobs) |
Actions
Actions follow a resource.verb naming convention. The following actions are currently logged:
Authentication
| Action | Description |
|---|---|
pak.created | A new PAK was generated |
pak.rotated | An existing PAK was rotated |
pak.deleted | A PAK was invalidated |
auth.failed | Authentication attempt failed |
auth.success | Authentication succeeded |
Resource Management
| Action | Description |
|---|---|
agent.created | New agent registered |
agent.updated | Agent details modified |
agent.deleted | Agent removed |
stack.created | New stack created |
stack.updated | Stack details modified |
stack.deleted | Stack removed |
generator.created | New generator created |
generator.updated | Generator details modified |
generator.deleted | Generator removed |
template.created | New template created |
template.updated | Template modified |
template.deleted | Template removed |
Webhooks
| Action | Description |
|---|---|
webhook.created | New webhook subscription created |
webhook.updated | Webhook subscription modified |
webhook.deleted | Webhook subscription removed |
webhook.delivery_failed | Webhook delivery failed after retries |
Work Orders
| Action | Description |
|---|---|
workorder.created | New work order created |
workorder.claimed | Work order claimed by an agent |
workorder.completed | Work order completed successfully |
workorder.failed | Work order failed |
workorder.retry | Work order returned for retry |
Administration
| Action | Description |
|---|---|
config.reloaded | Configuration hot-reload performed |
Resource Types
The resource_type field identifies what kind of resource was affected:
| Type | Description |
|---|---|
agent | An agent resource |
stack | A stack resource |
generator | A generator resource |
template | A stack template |
webhook_subscription | A webhook subscription |
work_order | A work order |
pak | A PAK (authentication key) |
config | System configuration |
system | System-level resource |
Querying Audit Logs
API Endpoint
Query audit logs through the admin API:
GET /api/v1/admin/audit-logs
Authorization: Bearer <admin_pak>
Query Parameters
| Parameter | Type | Description |
|---|---|---|
actor_type | string | Filter by actor type |
actor_id | UUID | Filter by actor ID |
action | string | Filter by action (exact match or prefix with *) |
resource_type | string | Filter by resource type |
resource_id | UUID | Filter by resource ID |
from | timestamp | Start time (inclusive, ISO 8601) |
to | timestamp | End time (exclusive, ISO 8601) |
limit | integer | Maximum results (default 100, max 1000) |
offset | integer | Results to skip (for pagination) |
Response Format
{
"logs": [
{
"id": "a1b2c3d4-...",
"timestamp": "2025-01-02T10:00:00Z",
"actor_type": "admin",
"actor_id": null,
"action": "agent.created",
"resource_type": "agent",
"resource_id": "e5f6g7h8-...",
"details": {
"agent_name": "production-cluster",
"cluster_name": "prod-us-east"
},
"ip_address": "192.168.1.100",
"user_agent": "curl/8.0.0",
"created_at": "2025-01-02T10:00:00Z"
}
],
"total": 150,
"count": 100,
"limit": 100,
"offset": 0
}
Example Queries
All agent creation events:
curl "http://localhost:3000/api/v1/admin/audit-logs?action=agent.created" \
-H "Authorization: Bearer $ADMIN_PAK"
All actions by a specific generator:
curl "http://localhost:3000/api/v1/admin/audit-logs?actor_type=generator&actor_id=$GENERATOR_ID" \
-H "Authorization: Bearer $ADMIN_PAK"
Failed authentication attempts in the last hour:
curl "http://localhost:3000/api/v1/admin/audit-logs?action=auth.failed&from=$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
-H "Authorization: Bearer $ADMIN_PAK"
All webhook-related actions (using prefix matching):
curl "http://localhost:3000/api/v1/admin/audit-logs?action=webhook.*" \
-H "Authorization: Bearer $ADMIN_PAK"
History of a specific resource:
curl "http://localhost:3000/api/v1/admin/audit-logs?resource_type=stack&resource_id=$STACK_ID" \
-H "Authorization: Bearer $ADMIN_PAK"
Details Field
The details field contains structured JSON with context specific to each action type. Common patterns include:
Resource creation:
{
"name": "my-stack",
"generator_id": "abc123-..."
}
Authentication failure:
{
"reason": "invalid_pak",
"pak_prefix": "brk_gen_"
}
Configuration changes:
{
"key": "webhook.timeout",
"old_value": "30",
"new_value": "60"
}
Retention Policy
Audit logs are subject to a retention policy that automatically removes old entries:
- Retention period: Configurable (default varies by deployment)
- Cleanup frequency: Background task runs periodically
- Deletion method: Hard delete (permanent removal)
Configure retention through broker settings:
broker:
audit_log_retention_days: 90
The cleanup task uses the created_at index for efficient deletion of old records.
Immutability
Audit log records are immutable after creation. The database schema enforces this by:
- No
updated_atcolumn exists - No update operations are exposed through the API or DAL
- Records can only be deleted by the retention policy
This immutability is essential for compliance requirements—audit logs must accurately reflect what happened without possibility of after-the-fact modification.
Database Indexes
For query performance, the following indexes exist:
| Index | Columns | Purpose |
|---|---|---|
idx_audit_logs_timestamp | (timestamp DESC) | Time-based queries |
idx_audit_logs_actor | (actor_type, actor_id, timestamp DESC) | Actor queries |
idx_audit_logs_resource | (resource_type, resource_id, timestamp DESC) | Resource history |
idx_audit_logs_action | (action, timestamp DESC) | Action filtering |
idx_audit_logs_cleanup | (created_at) | Retention cleanup |
Security Considerations
Access control: Only admin PAKs can query audit logs. Agents and generators cannot access the audit log API.
Sensitive data: The details field may contain resource names and identifiers but should not contain secrets. PAK values are never logged—only the action of creation or rotation is recorded.
IP address logging: Client IP addresses are captured for security investigation. Consider privacy implications for your deployment.
Failed auth tracking: Failed authentication attempts are logged with IP addresses, enabling detection of brute force attacks or credential stuffing.
Integration Patterns
Security Monitoring
Query failed authentication attempts to detect attack patterns:
# Get failed auth count by IP in last 24 hours
curl "http://localhost:3000/api/v1/admin/audit-logs?action=auth.failed&from=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)&limit=1000" \
-H "Authorization: Bearer $ADMIN_PAK" | \
jq '[.logs[].ip_address] | group_by(.) | map({ip: .[0], count: length}) | sort_by(-.count)'
Compliance Reporting
Export audit logs for compliance audits:
# Export all actions for a time period
curl "http://localhost:3000/api/v1/admin/audit-logs?from=2025-01-01T00:00:00Z&to=2025-02-01T00:00:00Z&limit=1000" \
-H "Authorization: Bearer $ADMIN_PAK" > audit-january-2025.json
Change Tracking
Track changes to a specific resource over time:
# See complete history of an agent
curl "http://localhost:3000/api/v1/admin/audit-logs?resource_type=agent&resource_id=$AGENT_ID&limit=50" \
-H "Authorization: Bearer $ADMIN_PAK"
Related Documentation
- Security Model - Authentication and authorization
- Soft Deletion - Resource deletion patterns
- Webhooks - Event notification system
Health Check Endpoints
Brokkr provides comprehensive health check endpoints for both the broker and agent components. These endpoints follow a three-tier pattern designed for different use cases: simple liveness checks, readiness validation, and detailed health diagnostics.
Three-Tier Health Check Pattern
Brokkr implements a three-tier health check system:
/healthz- Liveness probe: Simple check that the process is alive/readyz- Readiness probe: Validates that the service is ready to accept traffic/health- Detailed diagnostics: Comprehensive JSON status for monitoring and debugging
This pattern aligns with Kubernetes best practices and provides appropriate checks for different operational needs.
Broker Health Endpoints
The broker exposes health check endpoints on port 3000.
/healthz - Liveness Probe
Purpose: Verify that the broker process is alive and responding to requests.
Details:
- URL:
http://<broker-host>:3000/healthz - Method:
GET - Response:
200 OKwith plain text body"OK" - Checks: None (process must be alive to respond)
- Use case: Kubernetes livenessProbe to restart failed containers
Example Request:
curl http://brokkr-broker:3000/healthz
Example Response:
OK
Failure Scenarios:
- Process crashed or hung: No response (Kubernetes will restart container)
/readyz - Readiness Probe
Purpose: Verify that the broker is ready to accept API requests.
Details:
- URL:
http://<broker-host>:3000/readyz - Method:
GET - Response:
200 OKif ready, returns plain text"Ready" - Checks: Basic broker readiness (currently lightweight check)
- Use case: Kubernetes readinessProbe to control traffic routing
Example Request:
curl http://brokkr-broker:3000/readyz
Example Response (Healthy):
Ready
Failure Scenarios:
- Broker not ready: Returns appropriate error status
- Database connectivity issues would be detected by application errors
/health - Detailed Status
The broker currently provides basic health information. For detailed metrics about database connectivity, active agents, and system state, use the /metrics endpoint or the monitoring integration (see Monitoring & Observability).
Agent Health Endpoints
The agent exposes health check endpoints on port 8080 with comprehensive dependency checking.
/healthz - Liveness Probe
Purpose: Verify that the agent process is alive and responding to requests.
Details:
- URL:
http://<agent-host>:8080/healthz - Method:
GET - Response:
200 OKwith plain text body"OK" - Checks: None (process must be alive to respond)
- Use case: Kubernetes livenessProbe to restart failed containers
Example Request:
curl http://brokkr-agent:8080/healthz
Example Response:
OK
Failure Scenarios:
- Process crashed or hung: No response (Kubernetes will restart container)
/readyz - Readiness Probe
Purpose: Verify that the agent can perform its core functions.
Details:
- URL:
http://<agent-host>:8080/readyz - Method:
GET - Response:
200 OKif ready,503 Service Unavailableif not - Checks: Kubernetes API connectivity
- Use case: Kubernetes readinessProbe to control agent availability
Example Request:
curl http://brokkr-agent:8080/readyz
Example Response (Healthy):
Ready
Example Response (Unhealthy):
Kubernetes API unavailable
HTTP Status: 503 Service Unavailable
Failure Scenarios:
- Kubernetes API unreachable: Returns
503 Service Unavailable - Invalid kubeconfig or expired credentials: Returns
503 Service Unavailable
/health - Detailed Status
Purpose: Provide comprehensive JSON status for monitoring systems and debugging.
Details:
- URL:
http://<agent-host>:8080/health - Method:
GET - Response:
200 OKif healthy,503 Service Unavailableif any check fails - Checks:
- Kubernetes API connectivity
- Broker connection status
- Service uptime
- Application version
- Use case: Monitoring systems, operational dashboards, debugging
Example Request:
curl http://brokkr-agent:8080/health
Example Response (Healthy):
{
"status": "healthy",
"kubernetes": {
"connected": true
},
"broker": {
"connected": true,
"last_heartbeat": "2024-01-15T10:29:55Z"
},
"uptime_seconds": 3600,
"version": "0.1.0",
"timestamp": "2024-01-15T10:30:00Z"
}
HTTP Status: 200 OK
Example Response (Unhealthy - K8s Issue):
{
"status": "unhealthy",
"kubernetes": {
"connected": false,
"error": "connection refused: Unable to connect to the server"
},
"broker": {
"connected": true,
"last_heartbeat": "2024-01-15T10:29:55Z"
},
"uptime_seconds": 3600,
"version": "0.1.0",
"timestamp": "2024-01-15T10:30:00Z"
}
HTTP Status: 503 Service Unavailable
Example Response (Unhealthy - Broker Issue):
{
"status": "unhealthy",
"kubernetes": {
"connected": true
},
"broker": {
"connected": false
},
"uptime_seconds": 3600,
"version": "0.1.0",
"timestamp": "2024-01-15T10:30:00Z"
}
HTTP Status: 503 Service Unavailable
Response Fields:
status: Overall health status ("healthy"or"unhealthy")kubernetes.connected: Boolean indicating K8s API connectivitykubernetes.error: Optional error message if connection failedbroker.connected: Boolean indicating broker connectivitybroker.last_heartbeat: ISO 8601 timestamp of last successful heartbeatuptime_seconds: Service uptime in secondsversion: Application version from Cargo.tomltimestamp: Current timestamp in RFC3339 format
Kubernetes Probe Configuration
Broker Deployment
The broker Helm chart includes these recommended probe configurations:
apiVersion: apps/v1
kind: Deployment
metadata:
name: brokkr-broker
spec:
template:
spec:
containers:
- name: broker
image: ghcr.io/colliery-io/brokkr-broker:latest
ports:
- name: http
containerPort: 3000
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
Configuration Rationale:
- Liveness:
initialDelaySeconds: 30- Allow broker startup and database connectionperiodSeconds: 10- Check every 10 secondsfailureThreshold: 3- Restart after 30 seconds of failures
- Readiness:
initialDelaySeconds: 10- Quick readiness check after startupperiodSeconds: 5- Check frequently to minimize downtimefailureThreshold: 3- Remove from service after 15 seconds
Agent Deployment
The agent Helm chart includes these recommended probe configurations:
apiVersion: apps/v1
kind: Deployment
metadata:
name: brokkr-agent
spec:
template:
spec:
containers:
- name: agent
image: ghcr.io/colliery-io/brokkr-agent:latest
ports:
- name: http
containerPort: 8080
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
Configuration Rationale:
- Liveness:
initialDelaySeconds: 30- Allow agent startup and K8s/broker connectionperiodSeconds: 10- Check every 10 secondsfailureThreshold: 3- Restart after 30 seconds of failures
- Readiness:
initialDelaySeconds: 10- Quick readiness check after startupperiodSeconds: 5- Check frequently for K8s API issuesfailureThreshold: 3- Remove from service after 15 seconds
Monitoring Integration
Prometheus Health Check Monitoring
While health endpoints are primarily for Kubernetes probes, you can also monitor them with Prometheus using the Blackbox Exporter:
# Prometheus scrape config for blackbox exporter
scrape_configs:
- job_name: 'brokkr-health-checks'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- http://brokkr-broker:3000/healthz
- http://brokkr-broker:3000/readyz
- http://brokkr-agent:8080/healthz
- http://brokkr-agent:8080/readyz
- http://brokkr-agent:8080/health
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
Custom Health Check Script
You can create custom monitoring scripts to poll the health endpoints:
#!/bin/bash
# check-brokkr-health.sh - Monitor Brokkr component health
BROKER_URL="http://brokkr-broker:3000"
AGENT_URL="http://brokkr-agent:8080"
# Check broker readiness
if ! curl -sf "$BROKER_URL/readyz" > /dev/null; then
echo "ALERT: Broker not ready"
# Send alert to monitoring system
fi
# Check agent detailed health
AGENT_HEALTH=$(curl -sf "$AGENT_URL/health")
if [ $? -ne 0 ]; then
echo "ALERT: Agent health check failed"
# Send alert
else
STATUS=$(echo "$AGENT_HEALTH" | jq -r '.status')
if [ "$STATUS" != "healthy" ]; then
echo "ALERT: Agent unhealthy - $AGENT_HEALTH"
# Send alert with details
fi
fi
Datadog Integration
Monitor health endpoints using Datadog’s HTTP check:
# datadog-checks.yaml
init_config:
instances:
# Broker health checks
- name: brokkr-broker-liveness
url: http://brokkr-broker:3000/healthz
timeout: 3
method: GET
- name: brokkr-broker-readiness
url: http://brokkr-broker:3000/readyz
timeout: 3
method: GET
# Agent health checks
- name: brokkr-agent-liveness
url: http://brokkr-agent:8080/healthz
timeout: 3
method: GET
- name: brokkr-agent-readiness
url: http://brokkr-agent:8080/readyz
timeout: 3
method: GET
- name: brokkr-agent-detailed
url: http://brokkr-agent:8080/health
timeout: 5
method: GET
content_match: '"status":"healthy"'
Troubleshooting
Health Check Failures
Symptom: Broker /readyz returning errors or timeouts
Possible Causes:
- Database connectivity issues
- Broker process overloaded
- Network policy blocking health probe
Resolution:
# Check broker logs
kubectl logs -l app.kubernetes.io/name=brokkr-broker
# Test database connectivity
kubectl exec -it <broker-pod> -- env | grep DATABASE
# Test health endpoint manually
kubectl port-forward svc/brokkr-broker 3000:3000
curl -v http://localhost:3000/readyz
Symptom: Agent /readyz failing with “Kubernetes API unavailable”
Possible Causes:
- Invalid or expired service account credentials
- RBAC permissions insufficient
- Kubernetes API server unreachable
- Network policy blocking API access
Resolution:
# Check agent logs for detailed error
kubectl logs -l app.kubernetes.io/name=brokkr-agent
# Verify service account exists
kubectl get serviceaccount brokkr-agent
# Test K8s API access from agent pod
kubectl exec -it <agent-pod> -- sh
# Inside pod:
curl -k https://kubernetes.default.svc/api/v1/namespaces/default
Symptom: Agent /health showing "broker.connected": false
Possible Causes:
- Broker service unavailable
- Invalid broker URL configuration
- Network policy blocking broker access
- Authentication issues (invalid PAK)
Resolution:
# Check broker service
kubectl get svc brokkr-broker
# Test connectivity from agent to broker
kubectl exec -it <agent-pod> -- sh
# Inside pod:
curl http://brokkr-broker:3000/healthz
# Check agent configuration
kubectl get configmap <agent-configmap> -o yaml | grep BROKER
# Check agent logs for authentication errors
kubectl logs -l app.kubernetes.io/name=brokkr-agent | grep -i "auth\|broker"
Probe Configuration Issues
Symptom: Container restarting frequently due to failed liveness probes
Possible Causes:
initialDelaySecondstoo low for startup timetimeoutSecondstoo low for slow responsesfailureThresholdtoo low (not enough retry tolerance)
Resolution:
# Check recent pod events
kubectl describe pod <pod-name>
# Look for "Liveness probe failed" messages
# Adjust probe configuration based on actual startup time
# For slow-starting containers, increase initialDelaySeconds:
kubectl edit deployment brokkr-broker
# Set initialDelaySeconds: 60 for livenessProbe
Symptom: Pod marked not ready immediately after deployment
Possible Causes:
- Dependencies not available at startup
initialDelaySecondson readiness probe too aggressive
Resolution:
# Check readiness probe configuration
kubectl get deployment brokkr-agent -o yaml | grep -A10 readinessProbe
# Test readiness endpoint manually during startup
kubectl port-forward <pod-name> 8080:8080
# In another terminal:
watch -n 1 'curl -i http://localhost:8080/readyz'
Performance Considerations
Endpoint Latency
Health check endpoints are designed to be lightweight:
Broker Endpoints:
/healthz: <1ms (no checks, immediate response)/readyz: <5ms (lightweight readiness validation)
Agent Endpoints:
/healthz: <1ms (no checks, immediate response)/readyz: 5-50ms (depends on Kubernetes API latency)/health: 10-100ms (multiple checks including K8s API call)
Probe Frequency Impact
With default probe configurations:
- Liveness probes: Every 10 seconds = 6 requests/minute per pod
- Readiness probes: Every 5 seconds = 12 requests/minute per pod
- Total per pod: ~18 health check requests/minute
This generates minimal load:
- CPU: <0.1% per probe
- Memory: Negligible
- Network: <1KB per probe
Recommended Probe Intervals
Production Environments:
livenessProbe:
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
High-Availability Environments (faster failure detection):
livenessProbe:
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
readinessProbe:
initialDelaySeconds: 10
periodSeconds: 3
timeoutSeconds: 2
failureThreshold: 2
Development/Testing (more forgiving):
livenessProbe:
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 5
readinessProbe:
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 5
Best Practices
-
Use all three endpoint types appropriately:
/healthzfor liveness probes only/readyzfor readiness probes only/healthfor monitoring and debugging (not for probes)
-
Set appropriate timeouts:
- Account for slow network conditions
- Consider cold start performance
- Test probe timing in staging before production
-
Monitor probe failures:
- Alert on excessive readiness probe failures
- Track liveness probe failure rate
- Use Prometheus to monitor probe success rate
-
Tune for your environment:
- Adjust
initialDelaySecondsbased on actual startup time - Increase
periodSecondsif probes cause excessive load - Increase
failureThresholdin high-latency environments
- Adjust
-
Test probe configurations:
- Simulate failures in staging
- Verify restarts work as expected
- Ensure startup timing is adequate
-
Use
/healthendpoint for operational visibility:- Monitor detailed status in dashboards
- Parse JSON response for alerting
- Track component dependencies (K8s API, broker)
-
Avoid common mistakes:
- Don’t use
/healthfor Kubernetes probes (too detailed, may cause false positives) - Don’t set timeouts shorter than actual endpoint latency
- Don’t set
initialDelaySecondstoo low for startup dependencies
- Don’t use
Related Documentation
- Monitoring & Observability - Prometheus metrics and dashboards
- Installation Guide - Helm chart installation with probe configuration
- Configuration Reference - Environment variables and advanced configuration
Container Images Reference
This reference documents Brokkr’s container images, repository locations, tag formats, and publishing commands.
Image Repositories
All Brokkr images are published to GitHub Container Registry (GHCR) under the colliery-io organization.
Available Images
| Component | Repository | Purpose |
|---|---|---|
| Broker | ghcr.io/colliery-io/brokkr-broker | Central management service |
| Agent | ghcr.io/colliery-io/brokkr-agent | Kubernetes cluster agent |
| UI | ghcr.io/colliery-io/brokkr-ui | Administrative web interface |
Supported Architectures
All images support the following platforms:
linux/amd64- x86_64 architecturelinux/arm64- ARM64/aarch64 architecture
Tag Format Specifications
Semantic Version Tags
Created when a git tag matching v*.*.* is pushed.
| Tag Format | Example | Description | Mutable |
|---|---|---|---|
{major}.{minor}.{patch} | 1.2.3 | Full semantic version | No |
{major}.{minor} | 1.2 | Latest patch in minor version | Yes |
{major} | 1 | Latest minor in major version | Yes |
latest | latest | Most recent stable release | Yes |
Example: Tagging release v1.2.3 creates:
ghcr.io/colliery-io/brokkr-broker:1.2.3
ghcr.io/colliery-io/brokkr-broker:1.2
ghcr.io/colliery-io/brokkr-broker:1
ghcr.io/colliery-io/brokkr-broker:latest
Commit SHA Tags
Created for every commit that triggers a container build.
| Tag Format | Example | Description | Mutable |
|---|---|---|---|
{branch}-sha-{short-sha} | develop-sha-abc1234 | Branch-prefixed 7-character commit SHA | No |
Example: Commit abc1234def5678 on the develop branch creates:
ghcr.io/colliery-io/brokkr-broker:develop-sha-abc1234
Branch Tags
Created for pushes to tracked branches.
| Tag Format | Example | Description | Mutable |
|---|---|---|---|
{branch-name} | main | Branch name (sanitized) | Yes |
develop | develop | Development branch | Yes |
Example: Push to develop branch creates:
ghcr.io/colliery-io/brokkr-broker:develop
Pull Request Tags
Optionally created for pull request builds.
| Tag Format | Example | Description | Mutable |
|---|---|---|---|
pr-{number} | pr-123 | Pull request number | Yes |
Example: PR #123 creates:
ghcr.io/colliery-io/brokkr-broker:pr-123
Image Digests
Every image has a unique SHA256 digest that never changes:
ghcr.io/colliery-io/brokkr-broker@sha256:9fc91fae0f07c60ccbec61d86ff93fe825f92c42e5136295552ae196200dbe86
Production recommendation: Always use digest references for deployments to ensure immutability.
Building Images
Local Build (Single Architecture)
Build for your current platform:
angreal build multi-arch <component> --tag <tag>
Parameters:
<component>:broker,agent,ui, orall--tag <tag>: Image tag (default:dev)--registry <url>: Registry URL (default:ghcr.io/colliery-io)--platforms <platforms>: Platform list (default: current platform for local builds)
Examples:
# Build broker for current platform
angreal build multi-arch broker --tag dev
# Build agent for specific platform
angreal build multi-arch agent --tag test --platforms linux/amd64
# Build all components
angreal build multi-arch all --tag v1.0.0
Publishing to Registry
Add --push to publish directly to the registry:
angreal build multi-arch <component> --tag <tag> --push
Important: When using --push, the build automatically targets both AMD64 and ARM64 unless --platforms is specified.
Examples:
# Push broker with multi-arch support
angreal build multi-arch broker --tag v1.0.0 --push
# Push all components
angreal build multi-arch all --tag v1.0.0 --push
# Push to custom registry
angreal build multi-arch broker --tag dev --registry myregistry.io/myorg --push
Pulling Images
Public Images
Images are publicly accessible and do not require authentication:
docker pull ghcr.io/colliery-io/brokkr-broker:v1.0.0
Using Specific Architectures
Docker automatically selects the appropriate architecture. To explicitly choose:
docker pull --platform linux/amd64 ghcr.io/colliery-io/brokkr-broker:v1.0.0
docker pull --platform linux/arm64 ghcr.io/colliery-io/brokkr-broker:v1.0.0
Using Digests
For immutable deployments:
docker pull ghcr.io/colliery-io/brokkr-broker@sha256:9fc91fae0f07c60ccbec61d86ff93fe825f92c42e5136295552ae196200dbe86
Authentication for Publishing
GitHub Personal Access Token
Required for manual publishing. Create a token with write:packages scope.
Set environment variable:
export GITHUB_TOKEN=ghp_yourtokenhere
Login to registry:
docker login ghcr.io -u <your-github-username> --password "$GITHUB_TOKEN"
GitHub Actions
Automated workflows use the built-in GITHUB_TOKEN secret with automatic permissions.
Inspecting Images
View Manifest
docker manifest inspect ghcr.io/colliery-io/brokkr-broker:v1.0.0
List Available Tags
Visit the package page:
https://github.com/orgs/colliery-io/packages/container/brokkr-broker
Check Image Architecture
docker image inspect ghcr.io/colliery-io/brokkr-broker:v1.0.0 | grep Architecture
Image Layer Structure
Brokkr images use multi-stage builds optimized for size and security.
Broker and Agent Images
- Planner stage: Generates cargo-chef recipe
- Cacher stage: Builds dependencies (cached layer)
- Builder stage: Compiles Rust binaries
- Final stage: Minimal Debian slim with runtime dependencies
UI Image
- Single stage: Node.js Alpine (
node:18-alpine) with npm install and application start
Kubernetes Deployment
Using Semantic Versions
apiVersion: apps/v1
kind: Deployment
metadata:
name: brokkr-broker
spec:
template:
spec:
containers:
- name: broker
image: ghcr.io/colliery-io/brokkr-broker:v1.2.3
Using Digests (Recommended)
apiVersion: apps/v1
kind: Deployment
metadata:
name: brokkr-broker
spec:
template:
spec:
containers:
- name: broker
image: ghcr.io/colliery-io/brokkr-broker@sha256:9fc91fae0f07c60ccbec61d86ff93fe825f92c42e5136295552ae196200dbe86
Image Size Reference
Approximate compressed image sizes:
| Component | AMD64 | ARM64 |
|---|---|---|
| Broker | ~60 MB | ~58 MB |
| Agent | ~65 MB | ~62 MB |
| UI | ~40 MB | ~38 MB |
Note: Sizes vary by release and dependency versions
Related Documentation
- Publishing Strategy - Understanding the tagging and distribution strategy
Monitoring and Observability
Brokkr provides comprehensive Prometheus metrics for monitoring both the broker and agent components. This guide covers available metrics, configuration options, and example dashboards.
Metrics Endpoints
Both broker and agent expose Prometheus metrics in standard text exposition format.
Broker Metrics
Endpoint: http://<broker-host>:3000/metrics
The broker exposes metrics about HTTP requests, database operations, and system state.
Agent Metrics
Endpoint: http://<agent-host>:8080/metrics
The agent exposes metrics about broker polling, Kubernetes operations, and agent health.
Broker Metrics Catalog
HTTP Request Metrics
brokkr_http_requests_total
- Type: Counter
- Description: Total number of HTTP requests by endpoint and status
- Labels:
endpoint- API endpoint pathmethod- HTTP method (GET, POST, PUT, DELETE)status- HTTP status code (200, 404, 500, etc.)
Example PromQL:
# Request rate by endpoint
rate(brokkr_http_requests_total[5m])
# Error rate (4xx and 5xx)
sum(rate(brokkr_http_requests_total{status=~"[45].."}[5m])) by (endpoint)
# Success rate percentage
100 * sum(rate(brokkr_http_requests_total{status=~"2.."}[5m])) by (endpoint)
/ sum(rate(brokkr_http_requests_total[5m])) by (endpoint)
brokkr_http_request_duration_seconds
- Type: Histogram
- Description: HTTP request latency distribution in seconds
- Labels:
endpoint- API endpoint pathmethod- HTTP method
- Buckets: 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0 seconds
Example PromQL:
# 99th percentile latency
histogram_quantile(0.99,
sum(rate(brokkr_http_request_duration_seconds_bucket[5m])) by (le, endpoint)
)
# Average latency
rate(brokkr_http_request_duration_seconds_sum[5m])
/ rate(brokkr_http_request_duration_seconds_count[5m])
Database Metrics
brokkr_database_queries_total
- Type: Counter
- Description: Total number of database queries by type
- Labels:
query_type- Type of query (select, insert, update, delete)
Example PromQL:
# Query rate by type
rate(brokkr_database_queries_total[5m])
# Total queries per second
sum(rate(brokkr_database_queries_total[5m]))
brokkr_database_query_duration_seconds
- Type: Histogram
- Description: Database query latency distribution in seconds
- Labels:
query_type- Type of query
- Buckets: 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0 seconds
Example PromQL:
# 95th percentile query latency
histogram_quantile(0.95,
sum(rate(brokkr_database_query_duration_seconds_bucket[5m])) by (le, query_type)
)
System State Metrics
brokkr_active_agents
- Type: Gauge
- Description: Number of currently active agents
- Labels: None
Example PromQL:
# Current active agents
brokkr_active_agents
# Alert if no agents connected
brokkr_active_agents == 0
brokkr_agent_heartbeat_age_seconds
- Type: Gauge
- Description: Time since last heartbeat per agent in seconds
- Labels:
agent_id- Agent UUIDagent_name- Human-readable agent name
Example PromQL:
# Agents with stale heartbeats (>5 minutes)
brokkr_agent_heartbeat_age_seconds > 300
# Maximum heartbeat age across all agents
max(brokkr_agent_heartbeat_age_seconds)
brokkr_stacks_total
- Type: Gauge
- Description: Total number of stacks
- Labels: None
brokkr_deployment_objects_total
- Type: Gauge
- Description: Total number of deployment objects
- Labels: None
Agent Metrics Catalog
Broker Polling Metrics
brokkr_agent_poll_requests_total
- Type: Counter
- Description: Total number of broker poll requests
- Labels:
status- Request status (success, error)
Example PromQL:
# Poll request rate
rate(brokkr_agent_poll_requests_total[5m])
# Error rate
rate(brokkr_agent_poll_requests_total{status="error"}[5m])
# Success rate percentage
100 * sum(rate(brokkr_agent_poll_requests_total{status="success"}[5m]))
/ sum(rate(brokkr_agent_poll_requests_total[5m]))
brokkr_agent_poll_duration_seconds
- Type: Histogram
- Description: Broker poll request latency distribution in seconds
- Labels: None
- Buckets: 0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0 seconds
Example PromQL:
# 99th percentile poll latency
histogram_quantile(0.99,
sum(rate(brokkr_agent_poll_duration_seconds_bucket[5m])) by (le)
)
Kubernetes Operation Metrics
brokkr_agent_kubernetes_operations_total
- Type: Counter
- Description: Total number of Kubernetes API operations by type
- Labels:
operation- Operation type (apply, delete, get, list)
Example PromQL:
# K8s operation rate by type
rate(brokkr_agent_kubernetes_operations_total[5m])
# Total K8s operations per second
sum(rate(brokkr_agent_kubernetes_operations_total[5m]))
brokkr_agent_kubernetes_operation_duration_seconds
- Type: Histogram
- Description: Kubernetes operation latency distribution in seconds
- Labels:
operation- Operation type
- Buckets: 0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0 seconds
Example PromQL:
# 95th percentile K8s operation latency
histogram_quantile(0.95,
sum(rate(brokkr_agent_kubernetes_operation_duration_seconds_bucket[5m])) by (le, operation)
)
Agent Health Metrics
brokkr_agent_heartbeat_sent_total
- Type: Counter
- Description: Total number of heartbeats sent to broker
- Labels: None
Example PromQL:
# Heartbeat send rate
rate(brokkr_agent_heartbeat_sent_total[5m])
brokkr_agent_last_successful_poll_timestamp
- Type: Gauge
- Description: Unix timestamp of last successful broker poll
- Labels: None
Example PromQL:
# Time since last successful poll
time() - brokkr_agent_last_successful_poll_timestamp
# Alert if no successful poll in 5 minutes
time() - brokkr_agent_last_successful_poll_timestamp > 300
Prometheus Configuration
Manual Scrape Configuration
If you’re not using the Prometheus Operator, add these scrape configs to your prometheus.yml:
scrape_configs:
# Broker metrics
- job_name: 'brokkr-broker'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
action: keep
regex: brokkr-broker
- source_labels: [__meta_kubernetes_pod_container_port_number]
action: keep
regex: "3000"
- source_labels: [__meta_kubernetes_namespace]
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: kubernetes_pod_name
metrics_path: /metrics
scrape_interval: 30s
# Agent metrics
- job_name: 'brokkr-agent'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
action: keep
regex: brokkr-agent
- source_labels: [__meta_kubernetes_pod_container_port_number]
action: keep
regex: "8080"
- source_labels: [__meta_kubernetes_namespace]
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: kubernetes_pod_name
metrics_path: /metrics
scrape_interval: 30s
ServiceMonitor Configuration (Prometheus Operator)
Both broker and agent Helm charts include optional ServiceMonitor CRDs for automatic discovery.
Enable in values.yaml:
# For broker
metrics:
enabled: true
serviceMonitor:
enabled: true
interval: 30s
additionalLabels:
prometheus: kube-prometheus
# For agent
metrics:
enabled: true
serviceMonitor:
enabled: true
interval: 30s
additionalLabels:
prometheus: kube-prometheus
Installation:
# Broker with ServiceMonitor
helm install brokkr-broker oci://ghcr.io/colliery-io/charts/brokkr-broker \
--set postgresql.enabled=true \
--set metrics.serviceMonitor.enabled=true
# Agent with ServiceMonitor
helm install brokkr-agent oci://ghcr.io/colliery-io/charts/brokkr-agent \
--set broker.url=http://brokkr-broker:3000 \
--set broker.pak="<PAK>" \
--set metrics.serviceMonitor.enabled=true
Verify ServiceMonitor:
kubectl get servicemonitor
kubectl describe servicemonitor brokkr-broker
kubectl describe servicemonitor brokkr-agent
Example Alerting Rules
Create a PrometheusRule resource for automated alerting:
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: brokkr-alerts
labels:
prometheus: kube-prometheus
spec:
groups:
- name: brokkr-broker
interval: 30s
rules:
# No active agents
- alert: BrokerNoActiveAgents
expr: brokkr_active_agents == 0
for: 5m
labels:
severity: critical
annotations:
summary: "No active agents connected to broker"
description: "Broker has no active agents for 5 minutes"
# High error rate
- alert: BrokerHighErrorRate
expr: |
sum(rate(brokkr_http_requests_total{status=~"[45].."}[5m]))
/ sum(rate(brokkr_http_requests_total[5m])) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "Broker error rate above 5%"
description: "Broker HTTP error rate is {{ $value | humanizePercentage }}"
# High request latency
- alert: BrokerHighLatency
expr: |
histogram_quantile(0.95,
sum(rate(brokkr_http_request_duration_seconds_bucket[5m])) by (le, endpoint)
) > 1.0
for: 10m
labels:
severity: warning
annotations:
summary: "Broker p95 latency above 1s"
description: "Endpoint {{ $labels.endpoint }} p95 latency is {{ $value }}s"
# Stale agent heartbeat
- alert: BrokerAgentHeartbeatStale
expr: brokkr_agent_heartbeat_age_seconds > 300
for: 5m
labels:
severity: warning
annotations:
summary: "Agent heartbeat is stale"
description: "Agent {{ $labels.agent_name }} last heartbeat {{ $value }}s ago"
- name: brokkr-agent
interval: 30s
rules:
# Poll failures
- alert: AgentPollFailures
expr: |
rate(brokkr_agent_poll_requests_total{status="error"}[5m])
/ rate(brokkr_agent_poll_requests_total[5m]) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "Agent poll failure rate above 10%"
description: "Agent poll failure rate is {{ $value | humanizePercentage }}"
# No successful polls
- alert: AgentNoSuccessfulPolls
expr: time() - brokkr_agent_last_successful_poll_timestamp > 300
for: 5m
labels:
severity: critical
annotations:
summary: "Agent has not successfully polled broker"
description: "Agent last successful poll was {{ $value }}s ago"
# High K8s operation latency
- alert: AgentHighK8sLatency
expr: |
histogram_quantile(0.95,
sum(rate(brokkr_agent_kubernetes_operation_duration_seconds_bucket[5m])) by (le, operation)
) > 5.0
for: 10m
labels:
severity: warning
annotations:
summary: "Agent K8s operation latency above 5s"
description: "Operation {{ $labels.operation }} p95 latency is {{ $value }}s"
Grafana Dashboards
Brokkr includes pre-built Grafana dashboards for both broker and agent components.
Installing Dashboards
1. Download dashboard JSONs:
# Broker dashboard
curl -O https://raw.githubusercontent.com/colliery-io/brokkr/main/docs/grafana/brokkr-broker-dashboard.json
# Agent dashboard
curl -O https://raw.githubusercontent.com/colliery-io/brokkr/main/docs/grafana/brokkr-agent-dashboard.json
2. Import into Grafana:
- Navigate to Grafana UI
- Go to Dashboards → Import
- Upload the JSON file or paste the JSON content
- Select your Prometheus datasource
- Click Import
Broker Dashboard Features
The broker dashboard includes:
- Active Agents - Current count of connected agents
- Total Stacks - Number of managed stacks
- Deployment Objects - Total deployment objects
- HTTP Request Rate - Requests per second by endpoint
- HTTP Request Latency - p50, p95, p99 latencies by endpoint
- Database Query Rate - Queries per second by type
- Database Query Latency - p50, p95, p99 query latencies
- Agent Heartbeat Age - Time since last heartbeat per agent
Agent Dashboard Features
The agent dashboard includes:
- Broker Poll Request Rate - Success/error poll rates
- Broker Poll Latency - p50, p95, p99 poll latencies
- Kubernetes Operations Rate - Operations per second by type
- Kubernetes Operation Latency - p50, p95, p99 operation latencies
- Heartbeat Send Rate - Heartbeats sent per second
- Time Since Last Successful Poll - Gauge showing polling health
Integration with Other Monitoring Systems
Datadog
Use the Datadog OpenMetrics integration to scrape Prometheus metrics:
apiVersion: v1
kind: ConfigMap
metadata:
name: datadog-checks
data:
openmetrics.yaml: |
init_config:
instances:
- prometheus_url: http://brokkr-broker:3000/metrics
namespace: "brokkr.broker"
metrics:
- brokkr_*
- prometheus_url: http://brokkr-agent:8080/metrics
namespace: "brokkr.agent"
metrics:
- brokkr_agent_*
New Relic
Use the New Relic Prometheus OpenMetrics integration:
integrations:
- name: nri-prometheus
config:
targets:
- description: Brokkr Broker
urls: ["http://brokkr-broker:3000/metrics"]
- description: Brokkr Agent
urls: ["http://brokkr-agent:8080/metrics"]
transformations:
- description: "Add cluster label"
add_attributes:
- metric_prefix: "brokkr_"
attributes:
cluster_name: "production"
Performance Impact
Metrics collection has minimal performance overhead:
- CPU: <1% per component
- Memory: ~10MB for metrics registry
- Network: ~5KB per scrape (30s intervals = ~170KB/min)
Metrics are collected lazily and only computed when scraped by Prometheus.
Troubleshooting
Metrics Not Appearing
Check endpoint accessibility:
# Broker metrics
kubectl port-forward svc/brokkr-broker 3000:3000
curl http://localhost:3000/metrics
# Agent metrics
kubectl port-forward svc/brokkr-agent 8080:8080
curl http://localhost:8080/metrics
Verify ServiceMonitor:
# Check if ServiceMonitor is created
kubectl get servicemonitor brokkr-broker
kubectl get servicemonitor brokkr-agent
# Check Prometheus targets
kubectl port-forward svc/prometheus-operated 9090:9090
# Visit http://localhost:9090/targets
Missing Labels
ServiceMonitor labels must match Prometheus ServiceMonitor selector. Check your Prometheus Operator configuration:
kubectl get prometheus -o yaml | grep serviceMonitorSelector
Update Helm values to include matching labels:
metrics:
serviceMonitor:
enabled: true
additionalLabels:
prometheus: <your-prometheus-instance-label>
Best Practices
- Use ServiceMonitors when possible for automatic discovery
- Set appropriate scrape intervals (30s is recommended)
- Configure alerting rules for critical metrics
- Monitor resource usage in high-traffic environments
- Use recording rules for frequently queried expensive PromQL expressions
- Enable grafana dashboards for operational visibility
- Test alerts in staging before production deployment
Related Documentation
- Installation Guide - Helm chart installation
- Health Check Endpoints - Liveness and readiness probes
- Configuration Guide - Configuration options
Rust API Reference
This section contains the complete Rust API documentation for all Brokkr crates, automatically generated from source code doc comments using plissken.
These docs are regenerated on every build, so they always reflect the current state of the codebase.
Crates
- brokkr-broker — The main control plane and API server
- brokkr-agent — The agent that applies resources to Kubernetes clusters
- brokkr-models — Shared data models and database schema
- brokkr-utils — Utilities, configuration, and helpers
Interactive API Documentation
For REST API endpoint documentation (request/response schemas, authentication, try-it-out), see the API Reference or access Swagger UI directly at http://<broker-url>/swagger-ui.
Looking for higher-level guides? Return to the main documentation.
brokkr-agent Rust
brokkr-agent::bin Rust
Functions
brokkr-agent::bin::main
private
async fn main () -> Result < () , Box < dyn std :: error :: Error > >
Source
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = parse_cli();
match cli.command {
Commands::Start => {
commands::start().await?;
}
}
Ok(())
}
brokkr-agent::broker Rust
Broker communication module for agent-broker interaction.
For detailed documentation, see the Brokkr Documentation.
Functions
brokkr-agent::broker::wait_for_broker_ready
pub
#![allow(unused)]
fn main() {
async fn wait_for_broker_ready (config : & Settings)
}
Waits for the broker service to become ready.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing broker configuration |
Source
#![allow(unused)]
fn main() {
pub async fn wait_for_broker_ready(config: &Settings) {
let client = Client::new();
let readyz_url = format!("{}/readyz", config.agent.broker_url);
for attempt in 1..=config.agent.max_retries {
match client.get(&readyz_url).send().await {
Ok(response) => {
if response.status().is_success() {
info!("Successfully connected to broker at {}", readyz_url);
return;
}
warn!(
"Broker at {} returned non-success status: {}",
readyz_url,
response.status()
);
}
Err(e) => {
warn!(
"Failed to connect to broker at {} (attempt {}/{}): {:?}",
readyz_url, attempt, config.agent.max_retries, e
);
}
}
if attempt < config.agent.max_retries {
info!(
"Waiting for broker to be ready at {} (attempt {}/{})",
readyz_url, attempt, config.agent.max_retries
);
sleep(Duration::from_secs(1)).await;
}
}
error!(
"Failed to connect to broker at {} after {} attempts. Exiting.",
readyz_url, config.agent.max_retries
);
std::process::exit(1);
}
}
brokkr-agent::broker::verify_agent_pak
pub
#![allow(unused)]
fn main() {
async fn verify_agent_pak (config : & Settings) -> Result < () , Box < dyn std :: error :: Error > >
}
Verifies the agent’s Personal Access Key (PAK) with the broker.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing the PAK |
Returns:
Result<(), Box<dyn std::error::Error>>- Success or error with message
Source
#![allow(unused)]
fn main() {
pub async fn verify_agent_pak(config: &Settings) -> Result<(), Box<dyn std::error::Error>> {
let url = format!("{}/api/v1/auth/pak", config.agent.broker_url);
debug!("Verifying agent PAK at {}", url);
let response = reqwest::Client::new()
.post(&url)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", config.agent.pak))
.body("{}") // Empty JSON body
.send()
.await
.map_err(|e| {
error!("Failed to send PAK verification request: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK => {
info!("Successfully verified agent PAK");
Ok(())
}
StatusCode::UNAUTHORIZED => {
error!("Agent PAK verification failed: unauthorized");
Err("Invalid agent PAK".into())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"PAK verification failed with status {}: {}",
status, error_body
);
Err(format!(
"PAK verification failed. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::broker::fetch_agent_details
pub
#![allow(unused)]
fn main() {
async fn fetch_agent_details (config : & Settings , client : & Client ,) -> Result < Agent , Box < dyn std :: error :: Error > >
}
Fetches the details of the agent from the broker.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing broker configuration |
client | - | HTTP client for making requests to the broker |
Returns:
Result<Agent, Box<dyn std::error::Error>>- Agent details or error
Source
#![allow(unused)]
fn main() {
pub async fn fetch_agent_details(
config: &Settings,
client: &Client,
) -> Result<Agent, Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/agents/?name={}&cluster_name={}",
config.agent.broker_url, config.agent.agent_name, config.agent.cluster_name
);
debug!("Fetching agent details from {}", url);
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.send()
.await
.map_err(|e| {
error!("Failed to fetch agent details: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK => {
let agent: Agent = response.json().await.map_err(|e| {
error!("Failed to deserialize agent details: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
info!(
"Successfully fetched details for agent {} in cluster {}",
agent.name, agent.cluster_name
);
Ok(agent)
}
StatusCode::NOT_FOUND => {
error!(
"Agent not found: name={}, cluster={}",
config.agent.agent_name, config.agent.cluster_name
);
Err("Agent not found".into())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Failed to fetch agent details. Status {}: {}",
status, error_body
);
Err(format!(
"Failed to fetch agent details. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::broker::fetch_and_process_deployment_objects
pub
#![allow(unused)]
fn main() {
async fn fetch_and_process_deployment_objects (config : & Settings , client : & Client , agent : & Agent ,) -> Result < Vec < DeploymentObject > , Box < dyn std :: error :: Error > >
}
Fetches and processes deployment objects from the Kubernetes cluster
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing configuration parameters |
client | - | HTTP client for making API requests |
agent | - | Agent instance containing runtime context |
Returns:
Result<Vec<DeploymentObject>>- A vector of processed deployment objects if successful
Raises:
| Exception | Description |
|---|---|
Error | Returns an error if: |
Error | Failed to fetch deployments from the cluster |
Error | Failed to process deployment objects |
Source
#![allow(unused)]
fn main() {
pub async fn fetch_and_process_deployment_objects(
config: &Settings,
client: &Client,
agent: &Agent,
) -> Result<Vec<DeploymentObject>, Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/agents/{}/target-state",
config.agent.broker_url, agent.id
);
debug!("Fetching deployment objects from {}", url);
let start = Instant::now();
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.send()
.await
.map_err(|e| {
error!("Failed to send request to broker: {}", e);
metrics::poll_requests_total()
.with_label_values(&["error"])
.inc();
metrics::poll_duration_seconds()
.with_label_values(&[])
.observe(start.elapsed().as_secs_f64());
Box::new(e) as Box<dyn std::error::Error>
})?;
let duration = start.elapsed().as_secs_f64();
match response.status() {
StatusCode::OK => {
let deployment_objects: Vec<DeploymentObject> = response.json().await.map_err(|e| {
error!("Failed to deserialize deployment objects: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
info!(
"Successfully fetched {} deployment objects for agent {}",
deployment_objects.len(),
agent.name
);
metrics::poll_requests_total()
.with_label_values(&["success"])
.inc();
metrics::poll_duration_seconds()
.with_label_values(&[])
.observe(duration);
metrics::last_successful_poll_timestamp().set(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64(),
);
Ok(deployment_objects)
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Broker request failed with status {}: {}",
status, error_body
);
metrics::poll_requests_total()
.with_label_values(&["error"])
.inc();
metrics::poll_duration_seconds()
.with_label_values(&[])
.observe(duration);
Err(format!(
"Broker request failed. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::broker::send_success_event
pub
#![allow(unused)]
fn main() {
async fn send_success_event (config : & Settings , client : & Client , agent : & Agent , deployment_object_id : Uuid , message : Option < String > ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Sends a success event to the broker for the given deployment object.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing broker configuration |
client | - | HTTP client for making requests to the broker |
agent | - | Agent details |
deployment_object_id | - | ID of the deployment object |
message | - | Optional message to include in the event |
Returns:
Result<(), Box<dyn std::error::Error>>- Success or error with message
Source
#![allow(unused)]
fn main() {
pub async fn send_success_event(
config: &Settings,
client: &Client,
agent: &Agent,
deployment_object_id: Uuid,
message: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/agents/{}/events",
config.agent.broker_url, agent.id
);
debug!(
"Sending success event for deployment {} to {}",
deployment_object_id, url
);
let event = NewAgentEvent {
agent_id: agent.id,
deployment_object_id,
event_type: "DEPLOY".to_string(),
status: "SUCCESS".to_string(),
message,
};
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.json(&event)
.send()
.await
.map_err(|e| {
error!("Failed to send success event: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK | StatusCode::CREATED => {
info!(
"Successfully reported deployment success for object {}",
deployment_object_id
);
Ok(())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Failed to send success event. Status {}: {}",
status, error_body
);
Err(format!(
"Failed to send success event. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::broker::send_failure_event
pub
#![allow(unused)]
fn main() {
async fn send_failure_event (config : & Settings , client : & Client , agent : & Agent , deployment_object_id : Uuid , error_message : String ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Sends a failure event to the broker for the given deployment object.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing broker configuration |
client | - | HTTP client for making requests to the broker |
agent | - | Agent details |
deployment_object_id | - | ID of the deployment object |
error_message | - | Message to include in the event |
Returns:
Result<(), Box<dyn std::error::Error>>- Success or error with message
Source
#![allow(unused)]
fn main() {
pub async fn send_failure_event(
config: &Settings,
client: &Client,
agent: &Agent,
deployment_object_id: Uuid,
error_message: String,
) -> Result<(), Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/agents/{}/events",
config.agent.broker_url, agent.id
);
debug!(
"Sending failure event for deployment {} to {}",
deployment_object_id, url
);
let event = NewAgentEvent {
agent_id: agent.id,
deployment_object_id,
event_type: "DEPLOY".to_string(),
status: "FAILURE".to_string(),
message: Some(error_message),
};
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.json(&event)
.send()
.await
.map_err(|e| {
error!(
"Failed to send failure event for deployment {}: {}",
deployment_object_id, e
);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK | StatusCode::CREATED => {
info!(
"Successfully reported deployment failure for object {}",
deployment_object_id
);
Ok(())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Failed to send failure event. Status {}: {}",
status, error_body
);
Err(format!(
"Failed to send failure event. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::broker::send_heartbeat
pub
#![allow(unused)]
fn main() {
async fn send_heartbeat (config : & Settings , client : & Client , agent : & Agent ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Sends a heartbeat event to the broker for the given agent.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing broker configuration |
client | - | HTTP client for making requests to the broker |
agent | - | Agent details |
Returns:
Result<(), Box<dyn std::error::Error>>- Success or error with message
Source
#![allow(unused)]
fn main() {
pub async fn send_heartbeat(
config: &Settings,
client: &Client,
agent: &Agent,
) -> Result<(), Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/agents/{}/heartbeat",
config.agent.broker_url, agent.id
);
let _start = Instant::now();
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.send()
.await
.map_err(|e| {
error!("Failed to send heartbeat for agent {}: {}", agent.name, e);
metrics::heartbeat_sent_total().inc();
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK | StatusCode::NO_CONTENT => {
trace!("Heartbeat sent successfully for agent {}", agent.name);
metrics::heartbeat_sent_total().inc();
metrics::last_successful_poll_timestamp().set(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64(),
);
Ok(())
}
StatusCode::UNAUTHORIZED => {
error!("Heartbeat unauthorized for agent {}", agent.name);
Err("Unauthorized: Invalid agent PAK".into())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Heartbeat failed for agent {}. Status {}: {}",
agent.name, status, error_body
);
Err(format!("Heartbeat failed. Status: {}, Body: {}", status, error_body).into())
}
}
}
}
brokkr-agent::broker::send_health_status
pub
#![allow(unused)]
fn main() {
async fn send_health_status (config : & Settings , client : & Client , agent : & Agent , health_updates : Vec < DeploymentObjectHealthUpdate > ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Sends health status updates for deployment objects to the broker.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing broker configuration |
client | - | HTTP client for making requests to the broker |
agent | - | Agent details |
health_updates | - | List of deployment object health updates |
Returns:
Result<(), Box<dyn std::error::Error>>- Success or error with message
Source
#![allow(unused)]
fn main() {
pub async fn send_health_status(
config: &Settings,
client: &Client,
agent: &Agent,
health_updates: Vec<DeploymentObjectHealthUpdate>,
) -> Result<(), Box<dyn std::error::Error>> {
if health_updates.is_empty() {
return Ok(());
}
let url = format!(
"{}/api/v1/agents/{}/health-status",
config.agent.broker_url, agent.id
);
debug!(
"Sending health status update for {} deployment objects for agent {}",
health_updates.len(),
agent.name
);
let update = HealthStatusUpdate {
deployment_objects: health_updates,
};
let response = client
.patch(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.json(&update)
.send()
.await
.map_err(|e| {
error!(
"Failed to send health status for agent {}: {}",
agent.name, e
);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK | StatusCode::NO_CONTENT => {
debug!(
"Successfully sent health status for {} deployment objects",
update.deployment_objects.len()
);
Ok(())
}
StatusCode::UNAUTHORIZED => {
error!("Health status update unauthorized for agent {}", agent.name);
Err("Unauthorized: Invalid agent PAK".into())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Health status update failed for agent {}. Status {}: {}",
agent.name, status, error_body
);
Err(format!(
"Health status update failed. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::broker::fetch_pending_diagnostics
pub
#![allow(unused)]
fn main() {
async fn fetch_pending_diagnostics (config : & Settings , client : & Client , agent : & Agent ,) -> Result < Vec < DiagnosticRequest > , Box < dyn std :: error :: Error > >
}
Fetches pending diagnostic requests for the agent.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing broker configuration |
client | - | HTTP client for making requests to the broker |
agent | - | Agent details |
Returns:
Result<Vec<DiagnosticRequest>, Box<dyn std::error::Error>>- Pending diagnostic requests
Source
#![allow(unused)]
fn main() {
pub async fn fetch_pending_diagnostics(
config: &Settings,
client: &Client,
agent: &Agent,
) -> Result<Vec<DiagnosticRequest>, Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/agents/{}/diagnostics/pending",
config.agent.broker_url, agent.id
);
debug!("Fetching pending diagnostics from {}", url);
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.send()
.await
.map_err(|e| {
error!("Failed to fetch pending diagnostics: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK => {
let requests: Vec<DiagnosticRequest> = response.json().await.map_err(|e| {
error!("Failed to deserialize diagnostic requests: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
if !requests.is_empty() {
debug!(
"Found {} pending diagnostic requests for agent {}",
requests.len(),
agent.name
);
}
Ok(requests)
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Failed to fetch pending diagnostics. Status {}: {}",
status, error_body
);
Err(format!(
"Failed to fetch pending diagnostics. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::broker::claim_diagnostic_request
pub
#![allow(unused)]
fn main() {
async fn claim_diagnostic_request (config : & Settings , client : & Client , request_id : Uuid ,) -> Result < DiagnosticRequest , Box < dyn std :: error :: Error > >
}
Claims a diagnostic request for processing.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing broker configuration |
client | - | HTTP client for making requests to the broker |
request_id | - | ID of the diagnostic request to claim |
Returns:
Result<DiagnosticRequest, Box<dyn std::error::Error>>- The claimed request
Source
#![allow(unused)]
fn main() {
pub async fn claim_diagnostic_request(
config: &Settings,
client: &Client,
request_id: Uuid,
) -> Result<DiagnosticRequest, Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/diagnostics/{}/claim",
config.agent.broker_url, request_id
);
debug!("Claiming diagnostic request {}", request_id);
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.send()
.await
.map_err(|e| {
error!("Failed to claim diagnostic request {}: {}", request_id, e);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK => {
let request: DiagnosticRequest = response.json().await.map_err(|e| {
error!("Failed to deserialize claimed diagnostic request: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
info!("Successfully claimed diagnostic request {}", request_id);
Ok(request)
}
StatusCode::CONFLICT => {
warn!(
"Diagnostic request {} already claimed or completed",
request_id
);
Err(format!(
"Diagnostic request {} already claimed or completed",
request_id
)
.into())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Failed to claim diagnostic request {}. Status {}: {}",
request_id, status, error_body
);
Err(format!(
"Failed to claim diagnostic request. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::broker::submit_diagnostic_result
pub
#![allow(unused)]
fn main() {
async fn submit_diagnostic_result (config : & Settings , client : & Client , request_id : Uuid , result : SubmitDiagnosticResult ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Submits diagnostic results for a request.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing broker configuration |
client | - | HTTP client for making requests to the broker |
request_id | - | ID of the diagnostic request |
result | - | The diagnostic result to submit |
Returns:
Result<(), Box<dyn std::error::Error>>- Success or error
Source
#![allow(unused)]
fn main() {
pub async fn submit_diagnostic_result(
config: &Settings,
client: &Client,
request_id: Uuid,
result: SubmitDiagnosticResult,
) -> Result<(), Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/diagnostics/{}/result",
config.agent.broker_url, request_id
);
debug!("Submitting diagnostic result for request {}", request_id);
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.json(&result)
.send()
.await
.map_err(|e| {
error!(
"Failed to submit diagnostic result for request {}: {}",
request_id, e
);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK | StatusCode::CREATED => {
info!(
"Successfully submitted diagnostic result for request {}",
request_id
);
Ok(())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Failed to submit diagnostic result for request {}. Status {}: {}",
request_id, status, error_body
);
Err(format!(
"Failed to submit diagnostic result. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::cli Rust
Structs
brokkr-agent::cli::Cli
pub
Derives: Parser
CLI configuration structure.
Fields
| Name | Type | Description |
|---|---|---|
command | Commands | Command to execute |
Enums
brokkr-agent::cli::Commands pub
Available CLI commands.
Variants
Start- Start the Brokkr agent
Functions
brokkr-agent::cli::parse_cli
pub
#![allow(unused)]
fn main() {
fn parse_cli () -> Cli
}
Parses command-line arguments into the Cli structure.
Returns:
Cli- Parsed CLI configuration
Source
#![allow(unused)]
fn main() {
pub fn parse_cli() -> Cli {
Cli::parse()
}
}
brokkr-agent::cli::commands Rust
Functions
brokkr-agent::cli::commands::start
pub
#![allow(unused)]
fn main() {
async fn start () -> Result < () , Box < dyn std :: error :: Error > >
}
Source
#![allow(unused)]
fn main() {
pub async fn start() -> Result<(), Box<dyn std::error::Error>> {
let config = Settings::new(None).expect("Failed to load configuration");
// Initialize telemetry (includes tracing/logging setup)
let telemetry_config = config.telemetry.for_agent();
brokkr_utils::telemetry::init(&telemetry_config, &config.log.level, &config.log.format)
.expect("Failed to initialize telemetry");
info!("Starting Brokkr Agent");
info!("Waiting for broker to be ready");
broker::wait_for_broker_ready(&config).await;
info!("Verifying agent PAK");
broker::verify_agent_pak(&config).await?;
info!("Agent PAK verified successfully");
let client = Client::new();
info!("HTTP client created");
info!("Fetching agent details");
let mut agent = broker::fetch_agent_details(&config, &client).await?;
info!(
"Agent details fetched successfully for agent: {}",
agent.name
);
// Initialize Kubernetes client
info!("Initializing Kubernetes client");
let k8s_client = k8s::api::create_k8s_client(config.agent.kubeconfig_path.as_deref())
.await
.expect("Failed to create Kubernetes client");
// Initialize health state for health endpoints
let broker_status = Arc::new(RwLock::new(health::BrokerStatus {
connected: true,
last_heartbeat: None,
}));
let health_state = health::HealthState {
k8s_client: k8s_client.clone(),
broker_status: broker_status.clone(),
start_time: SystemTime::now(),
};
// Start health check HTTP server
let health_port = config.agent.health_port.unwrap_or(8080);
info!("Starting health check server on port {}", health_port);
let health_router = health::configure_health_routes(health_state);
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", health_port))
.await
.expect("Failed to bind health check server");
let _health_server = tokio::spawn(async move {
axum::serve(listener, health_router)
.await
.expect("Health check server failed");
});
info!("Starting main control loop");
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
// Create channels for shutdown coordination
let (shutdown_tx, mut shutdown_rx) = tokio::sync::broadcast::channel(1);
// Set up ctrl-c handler
tokio::spawn(async move {
if let Ok(()) = ctrl_c().await {
info!("Received shutdown signal");
let _ = shutdown_tx.send(());
r.store(false, Ordering::SeqCst);
}
});
// Create interval timers for periodic tasks
let mut heartbeat_interval = interval(Duration::from_secs(config.agent.polling_interval));
let mut deployment_check_interval =
interval(Duration::from_secs(config.agent.polling_interval));
let mut work_order_interval = interval(Duration::from_secs(config.agent.polling_interval));
// Health checking configuration
let health_check_enabled = config.agent.deployment_health_enabled.unwrap_or(true);
let health_check_interval_secs = config.agent.deployment_health_interval.unwrap_or(60);
let mut health_check_interval = interval(Duration::from_secs(health_check_interval_secs));
// Track deployment objects we've applied for health checking
let tracked_deployment_objects: Arc<RwLock<HashSet<Uuid>>> =
Arc::new(RwLock::new(HashSet::new()));
// Create health checker
let health_checker = deployment_health::HealthChecker::new(k8s_client.clone());
if health_check_enabled {
info!(
"Deployment health checking enabled with {}s interval",
health_check_interval_secs
);
} else {
info!("Deployment health checking is disabled");
}
// Diagnostics configuration - poll every 10 seconds for diagnostic requests
let mut diagnostics_interval = interval(Duration::from_secs(10));
let diagnostics_handler = diagnostics::DiagnosticsHandler::new(k8s_client.clone());
// Webhook delivery configuration - poll every 10 seconds for pending webhooks
let mut webhook_interval = interval(Duration::from_secs(10));
// Main control loop
while running.load(Ordering::SeqCst) {
select! {
_ = heartbeat_interval.tick() => {
match broker::send_heartbeat(&config, &client, &agent).await {
Ok(_) => {
debug!("Successfully sent heartbeat for agent '{}' (id: {})", agent.name, agent.id);
// Update broker status for health endpoints
{
let mut status = broker_status.write().await;
status.connected = true;
status.last_heartbeat = Some(chrono::Utc::now().to_rfc3339());
}
// Fetch updated agent details after heartbeat
match broker::fetch_agent_details(&config, &client).await {
Ok(updated_agent) => {
debug!("Successfully fetched updated agent details. Status: {}", updated_agent.status);
agent = updated_agent;
}
Err(e) => error!("Failed to fetch updated agent details: {}", e),
}
},
Err(e) => {
error!("Failed to send heartbeat for agent '{}' (id: {}): {}", agent.name, agent.id, e);
// Update broker status for health endpoints
let mut status = broker_status.write().await;
status.connected = false;
}
}
}
_ = deployment_check_interval.tick() => {
// Skip deployment object requests if agent is inactive
if agent.status != "ACTIVE" {
debug!("Agent '{}' (id: {}) is not active (status: {}), skipping deployment object requests",
agent.name, agent.id, agent.status);
continue;
}
match broker::fetch_and_process_deployment_objects(&config, &client, &agent).await {
Ok(objects) => {
for obj in objects {
let k8s_objects = k8s::objects::create_k8s_objects(obj.clone(),agent.id)?;
match k8s::api::reconcile_target_state(
&k8s_objects,
k8s_client.clone(),
&obj.stack_id.to_string(),
&obj.yaml_checksum,
).await {
Ok(_) => {
info!("Successfully applied {} Kubernetes objects for deployment object {} in agent '{}' (id: {})",
k8s_objects.len(), obj.id, agent.name, agent.id);
// Track this deployment object for health checking
{
let mut tracked = tracked_deployment_objects.write().await;
tracked.insert(obj.id);
}
if let Err(e) = broker::send_success_event(
&config,
&client,
&agent,
obj.id,
None,
).await {
error!("Failed to send success event for deployment {} in agent '{}' (id: {}): {}",
obj.id, agent.name, agent.id, e);
}
}
Err(e) => {
error!("Failed to apply Kubernetes objects for deployment {} in agent '{}' (id: {}). Error: {}",
obj.id, agent.name, agent.id, e);
if let Err(send_err) = broker::send_failure_event(
&config,
&client,
&agent,
obj.id,
e.to_string(),
).await {
error!("Failed to send failure event for deployment {} in agent '{}' (id: {}): {}",
obj.id, agent.name, agent.id, send_err);
}
}
}
}
}
Err(e) => error!("Failed to fetch deployment objects for agent '{}' (id: {}): {}",
agent.name, agent.id, e),
}
}
_ = work_order_interval.tick() => {
// Skip work order processing if agent is inactive
if agent.status != "ACTIVE" {
debug!("Agent '{}' (id: {}) is not active (status: {}), skipping work order processing",
agent.name, agent.id, agent.status);
continue;
}
// Process pending work orders
match work_orders::process_pending_work_orders(&config, &client, &k8s_client, &agent).await {
Ok(count) => {
if count > 0 {
info!("Processed {} work orders for agent '{}' (id: {})",
count, agent.name, agent.id);
}
}
Err(e) => {
error!("Failed to process work orders for agent '{}' (id: {}): {}",
agent.name, agent.id, e);
}
}
}
_ = health_check_interval.tick(), if health_check_enabled => {
// Skip health checking if agent is inactive
if agent.status != "ACTIVE" {
debug!("Agent '{}' (id: {}) is not active, skipping health check",
agent.name, agent.id);
continue;
}
// Get the list of tracked deployment objects
let deployment_ids: Vec<Uuid> = {
let tracked = tracked_deployment_objects.read().await;
tracked.iter().cloned().collect()
};
if deployment_ids.is_empty() {
debug!("No deployment objects to check health for");
continue;
}
debug!("Checking health for {} deployment objects", deployment_ids.len());
// Check health of all tracked deployment objects
let health_statuses = health_checker
.check_deployment_objects(&deployment_ids)
.await;
// Convert to health updates for broker
let health_updates: Vec<deployment_health::DeploymentObjectHealthUpdate> =
health_statuses.into_iter().map(|s| s.into()).collect();
// Send health status to broker
if let Err(e) = broker::send_health_status(&config, &client, &agent, health_updates).await {
error!("Failed to send health status for agent '{}': {}", agent.name, e);
} else {
debug!("Successfully sent health status for {} deployment objects",
deployment_ids.len());
}
}
_ = diagnostics_interval.tick() => {
// Skip diagnostics processing if agent is inactive
if agent.status != "ACTIVE" {
debug!("Agent '{}' (id: {}) is not active, skipping diagnostics",
agent.name, agent.id);
continue;
}
// Fetch pending diagnostic requests
match broker::fetch_pending_diagnostics(&config, &client, &agent).await {
Ok(requests) => {
for request in requests {
info!("Processing diagnostic request {} for deployment object {}",
request.id, request.deployment_object_id);
// Claim the request
match broker::claim_diagnostic_request(&config, &client, request.id).await {
Ok(_claimed) => {
// Collect diagnostics
// For now, use a default namespace and label selector
// In production, this should be derived from the deployment object
let namespace = "default";
let label_selector = format!("brokkr.io/deployment-object-id={}", request.deployment_object_id);
match diagnostics_handler.collect_diagnostics(namespace, &label_selector).await {
Ok(result) => {
// Submit the result
if let Err(e) = broker::submit_diagnostic_result(
&config,
&client,
request.id,
result,
).await {
error!("Failed to submit diagnostic result for request {}: {}",
request.id, e);
} else {
info!("Successfully submitted diagnostic result for request {}",
request.id);
}
}
Err(e) => {
error!("Failed to collect diagnostics for request {}: {}",
request.id, e);
// Submit an error result
let error_result = diagnostics::SubmitDiagnosticResult {
pod_statuses: "[]".to_string(),
events: format!("[{{\"error\": \"{}\"}}]", e),
log_tails: None,
collected_at: chrono::Utc::now(),
};
let _ = broker::submit_diagnostic_result(
&config,
&client,
request.id,
error_result,
).await;
}
}
}
Err(e) => {
warn!("Failed to claim diagnostic request {}: {}",
request.id, e);
}
}
}
}
Err(e) => {
debug!("Failed to fetch pending diagnostics: {}", e);
}
}
}
_ = webhook_interval.tick() => {
// Skip webhook processing if agent is inactive
if agent.status != "ACTIVE" {
debug!("Agent '{}' (id: {}) is not active, skipping webhook delivery",
agent.name, agent.id);
continue;
}
// Process pending webhook deliveries
match webhooks::process_pending_webhooks(&config, &client, &agent).await {
Ok(count) => {
if count > 0 {
info!("Processed {} webhook deliveries for agent '{}' (id: {})",
count, agent.name, agent.id);
}
}
Err(e) => {
debug!("Failed to process webhook deliveries: {}", e);
}
}
}
_ = shutdown_rx.recv() => {
info!("Initiating shutdown for agent '{}' (id: {})...", agent.name, agent.id);
break;
}
}
}
info!(
"Shutdown complete for agent '{}' (id: {})",
agent.name, agent.id
);
// Shutdown telemetry, flushing any pending traces
brokkr_utils::telemetry::shutdown();
Ok(())
}
}
brokkr-agent::deployment_health Rust
Deployment Health Checker Module
Monitors the health of deployed Kubernetes resources and reports status to the broker. Detects common issues like ImagePullBackOff, CrashLoopBackOff, OOMKilled, and other problematic conditions.
Structs
brokkr-agent::deployment_health::DeploymentHealthStatus
pub
Derives: Debug, Clone, Serialize, Deserialize
Health status for a deployment object
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | The deployment object ID |
status | String | Overall health status: healthy, degraded, failing, unknown |
summary | HealthSummary | Structured health summary |
checked_at | DateTime < Utc > | When the health was checked |
brokkr-agent::deployment_health::HealthSummary
pub
Derives: Debug, Clone, Default, Serialize, Deserialize
Summary of health information for a deployment
Fields
| Name | Type | Description |
|---|---|---|
pods_ready | usize | Number of pods in ready state |
pods_total | usize | Total number of pods |
conditions | Vec < String > | List of detected problematic conditions |
resources | Vec < ResourceHealth > | Per-resource health details |
brokkr-agent::deployment_health::ResourceHealth
pub
Derives: Debug, Clone, Serialize, Deserialize
Health status of an individual resource
Fields
| Name | Type | Description |
|---|---|---|
kind | String | Kind of the resource (e.g., “Pod”, “Deployment”) |
name | String | Name of the resource |
namespace | String | Namespace of the resource |
ready | bool | Whether the resource is ready |
message | Option < String > | Human-readable status message |
brokkr-agent::deployment_health::HealthChecker
pub
Checks deployment health for Kubernetes resources
Fields
| Name | Type | Description |
|---|---|---|
k8s_client | Client |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (k8s_client : Client) -> Self
}
Creates a new HealthChecker instance
Source
#![allow(unused)]
fn main() {
pub fn new(k8s_client: Client) -> Self {
Self { k8s_client }
}
}
check_deployment_object pub
async
#![allow(unused)]
fn main() {
async fn check_deployment_object (& self , deployment_object_id : Uuid ,) -> Result < DeploymentHealthStatus , Box < dyn std :: error :: Error + Send + Sync > >
}
Checks the health of a specific deployment object by ID
Finds all pods labeled with the deployment object ID and analyzes their status to determine overall health.
Source
#![allow(unused)]
fn main() {
pub async fn check_deployment_object(
&self,
deployment_object_id: Uuid,
) -> Result<DeploymentHealthStatus, Box<dyn std::error::Error + Send + Sync>> {
let checked_at = Utc::now();
// Find pods matching this deployment object
let pods = self.find_pods_for_deployment(deployment_object_id).await?;
let mut summary = HealthSummary::default();
let mut overall_status = "healthy";
let mut conditions_set: std::collections::HashSet<String> =
std::collections::HashSet::new();
summary.pods_total = pods.len();
for pod in &pods {
let pod_name = pod.metadata.name.clone().unwrap_or_default();
let pod_namespace = pod.metadata.namespace.clone().unwrap_or_default();
// Check if pod is ready
let pod_ready = is_pod_ready(pod);
if pod_ready {
summary.pods_ready += 1;
}
// Analyze pod status for issues
if let Some(pod_status) = &pod.status {
// Check container statuses for waiting/terminated issues
if let Some(container_statuses) = &pod_status.container_statuses {
for cs in container_statuses {
if let Some(state) = &cs.state {
// Check waiting state
if let Some(waiting) = &state.waiting {
if let Some(reason) = &waiting.reason {
if DEGRADED_CONDITIONS.contains(&reason.as_str()) {
conditions_set.insert(reason.clone());
overall_status = "degraded";
summary.resources.push(ResourceHealth {
kind: "Pod".to_string(),
name: pod_name.clone(),
namespace: pod_namespace.clone(),
ready: false,
message: waiting.message.clone(),
});
}
}
}
// Check terminated state for issues
if let Some(terminated) = &state.terminated {
if let Some(reason) = &terminated.reason {
if TERMINATED_ISSUES.contains(&reason.as_str()) {
conditions_set.insert(reason.clone());
overall_status = "degraded";
}
}
}
}
// Check last terminated state for recent crashes
if let Some(last_state) = &cs.last_state {
if let Some(terminated) = &last_state.terminated {
if let Some(reason) = &terminated.reason {
if reason == "OOMKilled" {
conditions_set.insert("OOMKilled".to_string());
overall_status = "degraded";
}
}
}
}
}
}
// Check init container statuses
if let Some(init_statuses) = &pod_status.init_container_statuses {
for cs in init_statuses {
if let Some(state) = &cs.state {
if let Some(waiting) = &state.waiting {
if let Some(reason) = &waiting.reason {
if DEGRADED_CONDITIONS.contains(&reason.as_str()) {
conditions_set.insert(format!("InitContainer:{}", reason));
overall_status = "degraded";
}
}
}
}
}
}
// Check pod phase
if let Some(phase) = &pod_status.phase {
match phase.as_str() {
"Failed" => {
overall_status = "failing";
conditions_set.insert("PodFailed".to_string());
}
"Unknown" => {
if overall_status != "failing" && overall_status != "degraded" {
overall_status = "unknown";
}
}
"Pending" => {
// Check if pending for too long might indicate an issue
// For now, we just note it's pending
if overall_status == "healthy" {
// Could add logic to check if pending too long
}
}
_ => {}
}
}
}
}
summary.conditions = conditions_set.into_iter().collect();
// If no pods found and we expected some, mark as unknown
if summary.pods_total == 0 {
overall_status = "unknown";
}
Ok(DeploymentHealthStatus {
id: deployment_object_id,
status: overall_status.to_string(),
summary,
checked_at,
})
}
}
find_pods_for_deployment private
async
#![allow(unused)]
fn main() {
async fn find_pods_for_deployment (& self , deployment_object_id : Uuid ,) -> Result < Vec < Pod > , Box < dyn std :: error :: Error + Send + Sync > >
}
Finds all pods labeled with the given deployment object ID
Source
#![allow(unused)]
fn main() {
async fn find_pods_for_deployment(
&self,
deployment_object_id: Uuid,
) -> Result<Vec<Pod>, Box<dyn std::error::Error + Send + Sync>> {
// Query pods across all namespaces with the deployment object label
let pods_api: Api<Pod> = Api::all(self.k8s_client.clone());
let label_selector = format!("{}={}", DEPLOYMENT_OBJECT_ID_LABEL, deployment_object_id);
let lp = ListParams::default().labels(&label_selector);
let pod_list = pods_api.list(&lp).await?;
Ok(pod_list.items)
}
}
check_deployment_objects pub
async
#![allow(unused)]
fn main() {
async fn check_deployment_objects (& self , deployment_object_ids : & [Uuid] ,) -> Vec < DeploymentHealthStatus >
}
Checks health for multiple deployment objects
Source
#![allow(unused)]
fn main() {
pub async fn check_deployment_objects(
&self,
deployment_object_ids: &[Uuid],
) -> Vec<DeploymentHealthStatus> {
let mut results = Vec::new();
for &id in deployment_object_ids {
match self.check_deployment_object(id).await {
Ok(status) => results.push(status),
Err(e) => {
warn!("Failed to check health for deployment object {}: {}", id, e);
// Report as unknown on error
results.push(DeploymentHealthStatus {
id,
status: "unknown".to_string(),
summary: HealthSummary::default(),
checked_at: Utc::now(),
});
}
}
}
results
}
}
brokkr-agent::deployment_health::HealthStatusUpdate
pub
Derives: Debug, Clone, Serialize, Deserialize
Request body for sending health status updates to the broker
Fields
| Name | Type | Description |
|---|---|---|
deployment_objects | Vec < DeploymentObjectHealthUpdate > | List of deployment object health updates |
brokkr-agent::deployment_health::DeploymentObjectHealthUpdate
pub
Derives: Debug, Clone, Serialize, Deserialize
Health update for a single deployment object (matches broker API)
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | The deployment object ID |
status | String | Health status: healthy, degraded, failing, or unknown |
summary | Option < HealthSummary > | Structured health summary |
checked_at | DateTime < Utc > | When the health was checked |
Functions
brokkr-agent::deployment_health::is_pod_ready
private
#![allow(unused)]
fn main() {
fn is_pod_ready (pod : & Pod) -> bool
}
Checks if a pod is in ready state
Source
#![allow(unused)]
fn main() {
fn is_pod_ready(pod: &Pod) -> bool {
pod.status
.as_ref()
.and_then(|s| s.conditions.as_ref())
.map(|conditions| {
conditions
.iter()
.any(|c| c.type_ == "Ready" && c.status == "True")
})
.unwrap_or(false)
}
}
brokkr-agent::diagnostics Rust
Diagnostics handler for on-demand diagnostic collection.
This module provides functionality to collect detailed diagnostic information about Kubernetes resources, including pod statuses, events, and log tails.
Structs
brokkr-agent::diagnostics::DiagnosticRequest
pub
Derives: Debug, Clone, Serialize, Deserialize
Diagnostic request received from the broker.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the diagnostic request. |
agent_id | Uuid | The agent that should handle this request. |
deployment_object_id | Uuid | The deployment object to gather diagnostics for. |
status | String | Status: pending, claimed, completed, failed, expired. |
requested_by | Option < String > | Who requested the diagnostics. |
created_at | DateTime < Utc > | When the request was created. |
claimed_at | Option < DateTime < Utc > > | When the agent claimed the request. |
completed_at | Option < DateTime < Utc > > | When the request was completed. |
expires_at | DateTime < Utc > | When the request expires. |
brokkr-agent::diagnostics::SubmitDiagnosticResult
pub
Derives: Debug, Clone, Serialize, Deserialize
Result to submit back to the broker.
Fields
| Name | Type | Description |
|---|---|---|
pod_statuses | String | JSON-encoded pod statuses. |
events | String | JSON-encoded Kubernetes events. |
log_tails | Option < String > | JSON-encoded log tails (optional). |
collected_at | DateTime < Utc > | When the diagnostics were collected. |
brokkr-agent::diagnostics::PodStatus
pub
Derives: Debug, Clone, Serialize, Deserialize
Pod status information for diagnostics.
Fields
| Name | Type | Description |
|---|---|---|
name | String | Pod name. |
namespace | String | Pod namespace. |
phase | String | Pod phase (Pending, Running, Succeeded, Failed, Unknown). |
conditions | Vec < PodCondition > | Pod conditions. |
containers | Vec < ContainerStatus > | Container statuses. |
brokkr-agent::diagnostics::PodCondition
pub
Derives: Debug, Clone, Serialize, Deserialize
Pod condition information.
Fields
| Name | Type | Description |
|---|---|---|
condition_type | String | Condition type. |
status | String | Condition status (True, False, Unknown). |
reason | Option < String > | Reason for the condition. |
message | Option < String > | Human-readable message. |
brokkr-agent::diagnostics::ContainerStatus
pub
Derives: Debug, Clone, Serialize, Deserialize
Container status information.
Fields
| Name | Type | Description |
|---|---|---|
name | String | Container name. |
ready | bool | Whether the container is ready. |
restart_count | i32 | Number of restarts. |
state | String | Current state of the container. |
state_reason | Option < String > | Reason for current state. |
state_message | Option < String > | Message for current state. |
brokkr-agent::diagnostics::EventInfo
pub
Derives: Debug, Clone, Serialize, Deserialize
Kubernetes event information.
Fields
| Name | Type | Description |
|---|---|---|
event_type | Option < String > | Event type (Normal, Warning). |
reason | Option < String > | Event reason. |
message | Option < String > | Event message. |
involved_object | String | Object involved. |
first_timestamp | Option < DateTime < Utc > > | First timestamp. |
last_timestamp | Option < DateTime < Utc > > | Last timestamp. |
count | Option < i32 > | Event count. |
brokkr-agent::diagnostics::DiagnosticsHandler
pub
Diagnostics handler for collecting Kubernetes diagnostics.
Fields
| Name | Type | Description |
|---|---|---|
client | Client | Kubernetes client. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (client : Client) -> Self
}
Creates a new DiagnosticsHandler.
Source
#![allow(unused)]
fn main() {
pub fn new(client: Client) -> Self {
Self { client }
}
}
collect_diagnostics pub
async
#![allow(unused)]
fn main() {
async fn collect_diagnostics (& self , namespace : & str , label_selector : & str ,) -> Result < SubmitDiagnosticResult , Box < dyn std :: error :: Error + Send + Sync > >
}
Collects diagnostics for resources matching the given labels in the namespace.
Parameters:
| Name | Type | Description |
|---|---|---|
namespace | - | The Kubernetes namespace |
label_selector | - | Label selector to find the resources |
Returns:
A SubmitDiagnosticResult containing collected diagnostics
Source
#![allow(unused)]
fn main() {
pub async fn collect_diagnostics(
&self,
namespace: &str,
label_selector: &str,
) -> Result<SubmitDiagnosticResult, Box<dyn std::error::Error + Send + Sync>> {
info!(
"Collecting diagnostics for namespace={}, labels={}",
namespace, label_selector
);
// Collect pod statuses
let pod_statuses = self.collect_pod_statuses(namespace, label_selector).await?;
// Collect events
let events = self.collect_events(namespace, label_selector).await?;
// Collect log tails
let log_tails = self.collect_log_tails(namespace, label_selector).await.ok();
Ok(SubmitDiagnosticResult {
pod_statuses: serde_json::to_string(&pod_statuses)?,
events: serde_json::to_string(&events)?,
log_tails: log_tails.map(|l| serde_json::to_string(&l)).transpose()?,
collected_at: Utc::now(),
})
}
}
collect_pod_statuses private
async
#![allow(unused)]
fn main() {
async fn collect_pod_statuses (& self , namespace : & str , label_selector : & str ,) -> Result < Vec < PodStatus > , Box < dyn std :: error :: Error + Send + Sync > >
}
Collects pod statuses for matching pods.
Source
#![allow(unused)]
fn main() {
async fn collect_pod_statuses(
&self,
namespace: &str,
label_selector: &str,
) -> Result<Vec<PodStatus>, Box<dyn std::error::Error + Send + Sync>> {
let pods: Api<Pod> = Api::namespaced(self.client.clone(), namespace);
let lp = ListParams::default().labels(label_selector);
let pod_list = pods.list(&lp).await?;
let mut statuses = Vec::new();
for pod in pod_list.items {
let name = pod.metadata.name.clone().unwrap_or_default();
let pod_namespace = pod.metadata.namespace.clone().unwrap_or_default();
let status = if let Some(status) = &pod.status {
let phase = status
.phase
.clone()
.unwrap_or_else(|| "Unknown".to_string());
let conditions: Vec<PodCondition> = status
.conditions
.as_ref()
.map(|conds| {
conds
.iter()
.map(|c| PodCondition {
condition_type: c.type_.clone(),
status: c.status.clone(),
reason: c.reason.clone(),
message: c.message.clone(),
})
.collect()
})
.unwrap_or_default();
let containers: Vec<ContainerStatus> = status
.container_statuses
.as_ref()
.map(|cs| {
cs.iter()
.map(|c| {
let (state, state_reason, state_message) =
if let Some(state) = &c.state {
if let Some(running) = &state.running {
(
"Running".to_string(),
None,
running
.started_at
.as_ref()
.map(|t| format!("Started at {}", t.0)),
)
} else if let Some(waiting) = &state.waiting {
(
"Waiting".to_string(),
waiting.reason.clone(),
waiting.message.clone(),
)
} else if let Some(terminated) = &state.terminated {
(
"Terminated".to_string(),
terminated.reason.clone(),
terminated.message.clone(),
)
} else {
("Unknown".to_string(), None, None)
}
} else {
("Unknown".to_string(), None, None)
};
ContainerStatus {
name: c.name.clone(),
ready: c.ready,
restart_count: c.restart_count,
state,
state_reason,
state_message,
}
})
.collect()
})
.unwrap_or_default();
PodStatus {
name,
namespace: pod_namespace,
phase,
conditions,
containers,
}
} else {
PodStatus {
name,
namespace: pod_namespace,
phase: "Unknown".to_string(),
conditions: vec![],
containers: vec![],
}
};
statuses.push(status);
}
debug!("Collected status for {} pods", statuses.len());
Ok(statuses)
}
}
collect_events private
async
#![allow(unused)]
fn main() {
async fn collect_events (& self , namespace : & str , _label_selector : & str ,) -> Result < Vec < EventInfo > , Box < dyn std :: error :: Error + Send + Sync > >
}
Collects events for matching resources.
Source
#![allow(unused)]
fn main() {
async fn collect_events(
&self,
namespace: &str,
_label_selector: &str,
) -> Result<Vec<EventInfo>, Box<dyn std::error::Error + Send + Sync>> {
let events: Api<Event> = Api::namespaced(self.client.clone(), namespace);
let lp = ListParams::default();
let event_list = events.list(&lp).await?;
let mut event_infos = Vec::new();
for event in event_list.items {
let involved_object = event
.involved_object
.name
.clone()
.unwrap_or_else(|| "unknown".to_string());
event_infos.push(EventInfo {
event_type: event.type_.clone(),
reason: event.reason.clone(),
message: event.message.clone(),
involved_object,
first_timestamp: event.first_timestamp.map(|t| t.0),
last_timestamp: event.last_timestamp.map(|t| t.0),
count: event.count,
});
}
// Sort by last_timestamp descending and take recent events
event_infos.sort_by(|a, b| {
b.last_timestamp
.unwrap_or(DateTime::<Utc>::MIN_UTC)
.cmp(&a.last_timestamp.unwrap_or(DateTime::<Utc>::MIN_UTC))
});
event_infos.truncate(50);
debug!("Collected {} events", event_infos.len());
Ok(event_infos)
}
}
collect_log_tails private
async
#![allow(unused)]
fn main() {
async fn collect_log_tails (& self , namespace : & str , label_selector : & str ,) -> Result < HashMap < String , String > , Box < dyn std :: error :: Error + Send + Sync > >
}
Collects log tails for matching pods.
Source
#![allow(unused)]
fn main() {
async fn collect_log_tails(
&self,
namespace: &str,
label_selector: &str,
) -> Result<HashMap<String, String>, Box<dyn std::error::Error + Send + Sync>> {
let pods: Api<Pod> = Api::namespaced(self.client.clone(), namespace);
let lp = ListParams::default().labels(label_selector);
let pod_list = pods.list(&lp).await?;
let mut log_tails = HashMap::new();
for pod in pod_list.items {
let pod_name = pod.metadata.name.clone().unwrap_or_default();
// Get containers from the spec
if let Some(spec) = &pod.spec {
for container in &spec.containers {
let container_name = &container.name;
let key = format!("{}/{}", pod_name, container_name);
match self
.get_container_logs(namespace, &pod_name, container_name)
.await
{
Ok(logs) => {
log_tails.insert(key, logs);
}
Err(e) => {
debug!(
"Failed to get logs for {}/{}: {}",
pod_name, container_name, e
);
log_tails.insert(key, format!("Error: {}", e));
}
}
}
}
}
debug!("Collected logs for {} containers", log_tails.len());
Ok(log_tails)
}
}
get_container_logs private
async
#![allow(unused)]
fn main() {
async fn get_container_logs (& self , namespace : & str , pod_name : & str , container_name : & str ,) -> Result < String , Box < dyn std :: error :: Error + Send + Sync > >
}
Gets logs for a specific container.
Source
#![allow(unused)]
fn main() {
async fn get_container_logs(
&self,
namespace: &str,
pod_name: &str,
container_name: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let pods: Api<Pod> = Api::namespaced(self.client.clone(), namespace);
let logs = pods
.logs(
pod_name,
&kube::api::LogParams {
container: Some(container_name.to_string()),
tail_lines: Some(MAX_LOG_LINES),
..Default::default()
},
)
.await?;
Ok(logs)
}
}
brokkr-agent::health Rust
Structs
brokkr-agent::health::HealthState
pub
Derives: Clone
Shared state for health endpoints
Fields
| Name | Type | Description |
|---|---|---|
k8s_client | Client | |
broker_status | Arc < RwLock < BrokerStatus > > | |
start_time | SystemTime |
brokkr-agent::health::BrokerStatus
pub
Derives: Clone
Broker connection status
Fields
| Name | Type | Description |
|---|---|---|
connected | bool | |
last_heartbeat | Option < String > |
brokkr-agent::health::HealthStatus
private
Derives: Serialize
Health status response structure
Fields
| Name | Type | Description |
|---|---|---|
status | String | |
kubernetes | KubernetesStatus | |
broker | BrokerStatusResponse | |
uptime_seconds | u64 | |
version | String | |
timestamp | String |
brokkr-agent::health::KubernetesStatus
private
Derives: Serialize
Kubernetes health status
Fields
| Name | Type | Description |
|---|---|---|
connected | bool | |
error | Option < String > |
brokkr-agent::health::BrokerStatusResponse
private
Derives: Serialize
Broker health status for response
Fields
| Name | Type | Description |
|---|---|---|
connected | bool | |
last_heartbeat | Option < String > |
Functions
brokkr-agent::health::configure_health_routes
pub
#![allow(unused)]
fn main() {
fn configure_health_routes (state : HealthState) -> Router
}
Configures and returns the health check router
Source
#![allow(unused)]
fn main() {
pub fn configure_health_routes(state: HealthState) -> Router {
Router::new()
.route("/healthz", get(healthz))
.route("/readyz", get(readyz))
.route("/health", get(health))
.route("/metrics", get(metrics_handler))
.with_state(state)
}
}
brokkr-agent::health::healthz
private
#![allow(unused)]
fn main() {
async fn healthz () -> impl IntoResponse
}
Simple liveness check endpoint
Returns 200 OK if the process is running. This is used for Kubernetes liveness probes.
Source
#![allow(unused)]
fn main() {
async fn healthz() -> impl IntoResponse {
(StatusCode::OK, "OK")
}
}
brokkr-agent::health::readyz
private
#![allow(unused)]
fn main() {
async fn readyz (State (state) : State < HealthState >) -> impl IntoResponse
}
Readiness check endpoint
Validates Kubernetes API connectivity. Returns 200 OK if K8s API is accessible, 503 if not.
Source
#![allow(unused)]
fn main() {
async fn readyz(State(state): State<HealthState>) -> impl IntoResponse {
// Test Kubernetes API connectivity by checking API health
match state.k8s_client.apiserver_version().await {
Ok(_) => (StatusCode::OK, "Ready"),
Err(e) => {
error!("Kubernetes API connectivity check failed: {:?}", e);
(
StatusCode::SERVICE_UNAVAILABLE,
"Kubernetes API unavailable",
)
}
}
}
}
brokkr-agent::health::health
private
#![allow(unused)]
fn main() {
async fn health (State (state) : State < HealthState >) -> impl IntoResponse
}
Detailed health check endpoint
Provides comprehensive JSON status including:
- Kubernetes API connectivity
- Broker connection status
- Service uptime
- Application version
- Timestamp Returns 200 OK if all checks pass, 503 if any check fails.
Source
#![allow(unused)]
fn main() {
async fn health(State(state): State<HealthState>) -> impl IntoResponse {
// Get current timestamp
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
let timestamp = chrono::Utc::now().to_rfc3339();
// Calculate uptime
let uptime = now.as_secs().saturating_sub(
state
.start_time
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs(),
);
// Check Kubernetes API connectivity
let (k8s_connected, k8s_error) = match state.k8s_client.apiserver_version().await {
Ok(_) => (true, None),
Err(e) => {
error!("Kubernetes API connectivity check failed: {:?}", e);
(false, Some(format!("{:?}", e)))
}
};
// Get broker status
let broker_status = state.broker_status.read().await;
let broker_connected = broker_status.connected;
let broker_last_heartbeat = broker_status.last_heartbeat.clone();
// Determine overall status
let overall_status = if k8s_connected && broker_connected {
"healthy"
} else {
"unhealthy"
};
let status_code = if k8s_connected && broker_connected {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
};
let health_status = HealthStatus {
status: overall_status.to_string(),
kubernetes: KubernetesStatus {
connected: k8s_connected,
error: k8s_error,
},
broker: BrokerStatusResponse {
connected: broker_connected,
last_heartbeat: broker_last_heartbeat,
},
uptime_seconds: uptime,
version: env!("CARGO_PKG_VERSION").to_string(),
timestamp,
};
(status_code, Json(health_status))
}
}
brokkr-agent::health::metrics_handler
private
#![allow(unused)]
fn main() {
async fn metrics_handler () -> impl IntoResponse
}
Prometheus metrics endpoint
Returns Prometheus metrics in text exposition format. Metrics include broker polling, Kubernetes operations, and agent health.
Source
#![allow(unused)]
fn main() {
async fn metrics_handler() -> impl IntoResponse {
let metrics_data = metrics::encode_metrics();
(
StatusCode::OK,
[("Content-Type", "text/plain; version=0.0.4")],
metrics_data,
)
}
}
brokkr-agent::k8s Rust
brokkr-agent::k8s::api Rust
Structs
brokkr-agent::k8s::api::RetryConfig
private
Retry configuration for Kubernetes operations
Fields
| Name | Type | Description |
|---|---|---|
max_elapsed_time | Duration | |
initial_interval | Duration | |
max_interval | Duration | |
multiplier | f64 |
Functions
brokkr-agent::k8s::api::is_retryable_error
private
#![allow(unused)]
fn main() {
fn is_retryable_error (error : & KubeError) -> bool
}
Determines if a Kubernetes error is retryable
Source
#![allow(unused)]
fn main() {
fn is_retryable_error(error: &KubeError) -> bool {
match error {
KubeError::Api(api_err) => {
matches!(api_err.code, 429 | 500 | 503 | 504)
|| matches!(
api_err.reason.as_str(),
"ServiceUnavailable" | "InternalError" | "Timeout"
)
}
_ => false,
}
}
}
brokkr-agent::k8s::api::with_retries
private
#![allow(unused)]
fn main() {
async fn with_retries < F , Fut , T > (operation : F , config : RetryConfig ,) -> Result < T , Box < dyn std :: error :: Error > > where F : Fn () -> Fut , Fut : std :: future :: Future < Output = Result < T , KubeError > > ,
}
Executes a Kubernetes operation with retries
Source
#![allow(unused)]
fn main() {
async fn with_retries<F, Fut, T>(
operation: F,
config: RetryConfig,
) -> Result<T, Box<dyn std::error::Error>>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<T, KubeError>>,
{
let backoff = ExponentialBackoffBuilder::new()
.with_initial_interval(config.initial_interval)
.with_max_interval(config.max_interval)
.with_multiplier(config.multiplier)
.with_max_elapsed_time(Some(config.max_elapsed_time))
.build();
let operation_with_backoff = || async {
match operation().await {
Ok(value) => Ok(value),
Err(error) => {
if is_retryable_error(&error) {
warn!("Retryable error encountered: {}", error);
Err(backoff::Error::Transient {
err: error,
retry_after: None,
})
} else {
error!("Non-retryable error encountered: {}", error);
Err(backoff::Error::Permanent(error))
}
}
}
};
backoff::future::retry(backoff, operation_with_backoff)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
}
brokkr-agent::k8s::api::apply_k8s_objects
pub
#![allow(unused)]
fn main() {
async fn apply_k8s_objects (k8s_objects : & [DynamicObject] , k8s_client : K8sClient , patch_params : PatchParams ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Applies a list of Kubernetes objects to the cluster using server-side apply.
Parameters:
| Name | Type | Description |
|---|---|---|
k8s_objects | - | List of DynamicObjects to apply |
discovery | - | Kubernetes Discovery client for API resource resolution |
k8s_client | - | Kubernetes client for API interactions |
patch_params | - | Parameters for the patch operation |
Returns:
Result<(), Box<dyn std::error::Error>>- Success or error with message
Source
#![allow(unused)]
fn main() {
pub async fn apply_k8s_objects(
k8s_objects: &[DynamicObject],
k8s_client: K8sClient,
patch_params: PatchParams,
) -> Result<(), Box<dyn std::error::Error>> {
info!("Applying {} Kubernetes objects", k8s_objects.len());
let start = Instant::now();
let discovery = Discovery::new(k8s_client.clone())
.run()
.await
.map_err(|e| {
error!("Failed to create Kubernetes discovery client: {}", e);
metrics::kubernetes_operations_total()
.with_label_values(&["apply"])
.inc();
metrics::kubernetes_operation_duration_seconds()
.with_label_values(&["apply"])
.observe(start.elapsed().as_secs_f64());
e
})?;
for k8s_object in k8s_objects {
let default_namespace = &"default".to_string();
let namespace = k8s_object
.metadata
.namespace
.as_deref()
.unwrap_or(default_namespace);
let gvk = if let Some(tm) = &k8s_object.types {
GroupVersionKind::try_from(tm)?
} else {
error!(
"Cannot apply object without valid TypeMeta for object named '{}'",
k8s_object.name_any()
);
return Err(format!(
"Cannot apply object without valid TypeMeta for object named '{}'",
k8s_object.name_any()
)
.into());
};
if let Some((ar, caps)) = discovery.resolve_gvk(&gvk) {
let api = dynamic_api(ar, caps, k8s_client.clone(), Some(namespace), false);
info!(
"Applying {} '{}' in namespace '{}'",
gvk.kind,
k8s_object.name_any(),
namespace
);
debug!("Object content:\n{}", serde_yaml::to_string(&k8s_object)?);
let data = serde_json::to_value(k8s_object)?;
let name = k8s_object.name_any();
let name_for_error = name.clone();
let patch_params = patch_params.clone();
with_retries(
move || {
let api = api.clone();
let name = name.clone();
let data = data.clone();
let patch_params = patch_params.clone();
async move { api.patch(&name, &patch_params, &Patch::Apply(data)).await }
},
RetryConfig::default(),
)
.await
.map_err(|e| {
error!(
"Failed to apply {} '{}' in namespace '{}': {}",
gvk.kind, name_for_error, namespace, e
);
e
})?;
info!(
"Successfully applied {} '{}' in namespace '{}'",
gvk.kind, name_for_error, namespace
);
} else {
error!(
"Failed to resolve GroupVersionKind for {} '{}' in namespace '{}'",
gvk.kind,
k8s_object.name_any(),
namespace
);
return Err(format!(
"Failed to resolve GroupVersionKind for {} '{}' in namespace '{}'",
gvk.kind,
k8s_object.name_any(),
namespace
)
.into());
}
}
info!(
"Successfully applied all {} Kubernetes objects",
k8s_objects.len()
);
// Record metrics for successful apply
metrics::kubernetes_operations_total()
.with_label_values(&["apply"])
.inc();
metrics::kubernetes_operation_duration_seconds()
.with_label_values(&["apply"])
.observe(start.elapsed().as_secs_f64());
Ok(())
}
}
brokkr-agent::k8s::api::dynamic_api
pub
#![allow(unused)]
fn main() {
fn dynamic_api (ar : ApiResource , caps : ApiCapabilities , client : K8sClient , namespace : Option < & str > , all_namespaces : bool ,) -> Api < DynamicObject >
}
Creates a dynamic Kubernetes API client for a specific resource type
Parameters:
| Name | Type | Description |
|---|---|---|
ar | - | ApiResource describing the Kubernetes resource type |
caps | - | Capabilities of the API (e.g., list, watch, etc.) |
client | - | Kubernetes client instance |
namespace | - | Optional namespace to scope the API to |
all_namespaces | - | Whether to operate across all namespaces |
Returns:
An Api
Source
#![allow(unused)]
fn main() {
pub fn dynamic_api(
ar: ApiResource,
caps: ApiCapabilities,
client: K8sClient,
namespace: Option<&str>,
all_namespaces: bool,
) -> Api<DynamicObject> {
if caps.scope == Scope::Cluster || all_namespaces {
Api::all_with(client, &ar)
} else if let Some(namespace) = namespace {
Api::namespaced_with(client, namespace, &ar)
} else {
Api::default_namespaced_with(client, &ar)
}
}
}
brokkr-agent::k8s::api::get_all_objects_by_annotation
pub
#![allow(unused)]
fn main() {
async fn get_all_objects_by_annotation (k8s_client : & K8sClient , annotation_key : & str , annotation_value : & str ,) -> Result < Vec < DynamicObject > , Box < dyn std :: error :: Error > >
}
Retrieves all Kubernetes objects with a specific annotation key-value pair.
Parameters:
| Name | Type | Description |
|---|---|---|
k8s_client | - | Kubernetes client |
discovery | - | Kubernetes Discovery client |
annotation_key | - | Annotation key to filter by |
annotation_value | - | Annotation value to filter by |
Returns:
Result<Vec<DynamicObject>, Box<dyn std::error::Error>>- List of matching objects or error
Source
#![allow(unused)]
fn main() {
pub async fn get_all_objects_by_annotation(
k8s_client: &K8sClient,
annotation_key: &str,
annotation_value: &str,
) -> Result<Vec<DynamicObject>, Box<dyn std::error::Error>> {
let mut results = Vec::new();
let discovery = Discovery::new(k8s_client.clone())
.run()
.await
.expect("Failed to create discovery client");
// Search through all API groups and resources
for group in discovery.groups() {
for (ar, caps) in group.recommended_resources() {
let api: Api<DynamicObject> =
dynamic_api(ar.clone(), caps.clone(), k8s_client.clone(), None, true);
match api.list(&Default::default()).await {
Ok(list) => {
let matching_objects = list
.items
.into_iter()
.filter(|obj| {
obj.metadata
.annotations
.as_ref()
.and_then(|annotations| annotations.get(annotation_key))
.is_some_and(|value| value == annotation_value)
})
.map(|mut obj| {
// Set TypeMeta directly
obj.types = Some(TypeMeta {
api_version: if ar.group.is_empty() {
ar.version.clone()
} else {
format!("{}/{}", ar.group, ar.version)
},
kind: ar.kind.clone(),
});
obj
});
results.extend(matching_objects);
}
Err(e) => warn!("Error listing resources for {:?}: {:?}", ar, e),
}
}
}
Ok(results)
}
}
brokkr-agent::k8s::api::delete_k8s_objects
pub
#![allow(unused)]
fn main() {
async fn delete_k8s_objects (k8s_objects : & [DynamicObject] , k8s_client : K8sClient , agent_id : & Uuid ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Deletes a list of Kubernetes objects from the cluster.
Parameters:
| Name | Type | Description |
|---|---|---|
k8s_objects | - | List of DynamicObjects to delete |
discovery | - | Kubernetes Discovery client for API resource resolution |
k8s_client | - | Kubernetes client for API interactions |
Returns:
Result<(), Box<dyn std::error::Error>>- Success or error with message
Source
#![allow(unused)]
fn main() {
pub async fn delete_k8s_objects(
k8s_objects: &[DynamicObject],
k8s_client: K8sClient,
agent_id: &Uuid,
) -> Result<(), Box<dyn std::error::Error>> {
info!(
"Starting deletion of {} Kubernetes objects",
k8s_objects.len()
);
let discovery = Discovery::new(k8s_client.clone())
.run()
.await
.expect("Failed to create discovery client");
for k8s_object in k8s_objects {
// Verify ownership before attempting deletion
if !verify_object_ownership(k8s_object, agent_id) {
error!(
"Cannot delete object '{}' (kind: {}) as it is not owned by agent {}",
k8s_object.name_any(),
k8s_object.types.as_ref().map_or("unknown", |t| &t.kind),
agent_id
);
return Err(format!(
"Cannot delete object '{}' as it is not owned by this agent",
k8s_object.name_any()
)
.into());
}
debug!("Processing k8s object for deletion: {:?}", k8s_object);
let default_namespace = &"default".to_string();
let namespace = k8s_object
.metadata
.namespace
.as_ref()
.unwrap_or(default_namespace);
let gvk = if let Some(tm) = &k8s_object.types {
GroupVersionKind::try_from(tm)?
} else {
error!(
"Cannot delete object '{}' without valid TypeMeta",
k8s_object.name_any()
);
return Err(format!(
"Cannot delete object without valid TypeMeta {:?}",
k8s_object
)
.into());
};
if let Some((ar, caps)) = discovery.resolve_gvk(&gvk) {
let api = dynamic_api(ar, caps, k8s_client.clone(), Some(namespace), false);
let name = k8s_object.name_any();
let name_for_error = name.clone();
info!(
"Deleting {} '{}' in namespace '{}'",
gvk.kind, name, namespace
);
with_retries(
move || {
let api = api.clone();
let name = name.clone();
async move { api.delete(&name, &Default::default()).await }
},
RetryConfig::default(),
)
.await
.map_err(|e| {
error!(
"Failed to delete {} '{}' in namespace '{}': {}",
gvk.kind, name_for_error, namespace, e
);
e
})?;
info!(
"Successfully deleted {} '{}' in namespace '{}'",
gvk.kind, name_for_error, namespace
);
}
}
info!(
"Successfully deleted all {} Kubernetes objects",
k8s_objects.len()
);
Ok(())
}
}
brokkr-agent::k8s::api::validate_k8s_objects
pub
#![allow(unused)]
fn main() {
async fn validate_k8s_objects (k8s_objects : & [DynamicObject] , k8s_client : K8sClient ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Validates Kubernetes objects against the API server without applying them.
Parameters:
| Name | Type | Description |
|---|---|---|
k8s_objects | - | List of DynamicObjects to validate |
k8s_client | - | Kubernetes client for API interactions |
Returns:
Result<(), Box<dyn std::error::Error>>- Success or error with validation message
Source
#![allow(unused)]
fn main() {
pub async fn validate_k8s_objects(
k8s_objects: &[DynamicObject],
k8s_client: K8sClient,
) -> Result<(), Box<dyn std::error::Error>> {
let mut validation_errors = Vec::new();
let discovery = Discovery::new(k8s_client.clone())
.run()
.await
.expect("Failed to create discovery client");
for k8s_object in k8s_objects {
let default_namespace = &"default".to_string();
let namespace = k8s_object
.metadata
.namespace
.as_deref()
.unwrap_or(default_namespace);
let gvk = if let Some(tm) = &k8s_object.types {
match GroupVersionKind::try_from(tm) {
Ok(gvk) => gvk,
Err(e) => {
validation_errors.push(format!(
"Invalid TypeMeta for object '{}': {}",
k8s_object.name_any(),
e
));
continue;
}
}
} else {
validation_errors.push(format!(
"Missing TypeMeta for object '{}'",
k8s_object.name_any()
));
continue;
};
if let Some((ar, caps)) = discovery.resolve_gvk(&gvk) {
let api = dynamic_api(ar, caps, k8s_client.clone(), Some(namespace), false);
match serde_json::to_value(k8s_object) {
Ok(data) => {
let mut patch_params = PatchParams::apply("validation");
patch_params = patch_params.dry_run();
patch_params.force = true;
match api
.patch(&k8s_object.name_any(), &patch_params, &Patch::Apply(data))
.await
{
Ok(_) => {
info!(
"Validation successful for {:?} '{}'",
gvk.kind,
k8s_object.name_any()
);
}
Err(e) => {
error!(
"Validation failed for {:?} '{}': {:?}",
gvk.kind,
k8s_object.name_any(),
e
);
validation_errors.push(format!(
"Validation failed for {} '{}': {}",
gvk.kind,
k8s_object.name_any(),
e
));
}
}
}
Err(e) => {
validation_errors.push(format!(
"Failed to serialize object '{}': {}",
k8s_object.name_any(),
e
));
}
}
} else {
validation_errors.push(format!(
"Unable to resolve GVK {:?} for object '{}'",
gvk,
k8s_object.name_any()
));
}
}
if validation_errors.is_empty() {
Ok(())
} else {
Err(validation_errors.join("\n").into())
}
}
}
brokkr-agent::k8s::api::apply_single_object
private
#![allow(unused)]
fn main() {
async fn apply_single_object (object : & DynamicObject , client : & Client , stack_id : & str , checksum : & str ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Applies a single Kubernetes object with proper annotations.
Parameters:
| Name | Type | Description |
|---|---|---|
object | - | The DynamicObject to apply |
client | - | Kubernetes client |
stack_id | - | Stack ID for annotation |
checksum | - | Checksum for annotation |
Source
#![allow(unused)]
fn main() {
async fn apply_single_object(
object: &DynamicObject,
client: &Client,
stack_id: &str,
checksum: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let kind = object
.types
.as_ref()
.map(|t| t.kind.clone())
.unwrap_or_default();
let namespace = object
.metadata
.namespace
.as_deref()
.unwrap_or("default")
.to_string();
let name = object.metadata.name.as_deref().unwrap_or("").to_string();
let key = format!("{}:{}@{}", kind, name, namespace);
debug!(
"Applying priority object: kind={}, namespace={}, name={}",
kind, namespace, name
);
// Prepare object with annotations
let mut object = object.clone();
let annotations = object
.metadata
.annotations
.get_or_insert_with(BTreeMap::new);
annotations.insert(STACK_LABEL.to_string(), stack_id.to_string());
annotations.insert(CHECKSUM_ANNOTATION.to_string(), checksum.to_string());
let mut params = PatchParams::apply("brokkr-controller");
params.force = true;
if let Some(gvk) = object.types.as_ref() {
let gvk = GroupVersionKind::try_from(gvk)?;
if let Some((ar, caps)) = Discovery::new(client.clone())
.run()
.await?
.resolve_gvk(&gvk)
{
let api = dynamic_api(ar, caps, client.clone(), Some(&namespace), false);
let patch = Patch::Apply(&object);
match api.patch(&name, ¶ms, &patch).await {
Ok(_) => {
debug!("Successfully applied priority object {}", key);
Ok(())
}
Err(e) => {
error!("Failed to apply priority object {}: {}", key, e);
Err(Box::new(e))
}
}
} else {
Err(format!("Failed to resolve GVK for {}", key).into())
}
} else {
Err(format!("Missing TypeMeta for {}", key).into())
}
}
}
brokkr-agent::k8s::api::rollback_namespaces
private
#![allow(unused)]
fn main() {
async fn rollback_namespaces (client : & Client , namespaces : & [String])
}
Rolls back namespaces that were created during a failed reconciliation.
Parameters:
| Name | Type | Description |
|---|---|---|
client | - | Kubernetes client |
namespaces | - | List of namespace names to delete |
Source
#![allow(unused)]
fn main() {
async fn rollback_namespaces(client: &Client, namespaces: &[String]) {
if namespaces.is_empty() {
return;
}
warn!(
"Rolling back {} namespace(s) due to reconciliation failure",
namespaces.len()
);
for ns_name in namespaces {
info!("Deleting namespace '{}' as part of rollback", ns_name);
// Create a namespace API
let ns_api: Api<Namespace> = Api::all(client.clone());
match ns_api.delete(ns_name, &DeleteParams::default()).await {
Ok(_) => {
info!(
"Successfully deleted namespace '{}' during rollback",
ns_name
);
}
Err(e) => {
// Log but don't fail - best effort cleanup
warn!(
"Failed to delete namespace '{}' during rollback: {}",
ns_name, e
);
}
}
}
}
}
brokkr-agent::k8s::api::reconcile_target_state
pub
#![allow(unused)]
fn main() {
async fn reconcile_target_state (objects : & [DynamicObject] , client : Client , stack_id : & str , checksum : & str ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Reconciles the target state of Kubernetes objects for a stack.
This function:
- Applies priority resources (Namespaces, CRDs) first to ensure dependencies exist
- Validates remaining objects against the API server
- Applies all resources with server-side apply
- Prunes any objects that are no longer part of the desired state but belong to the same stack
- Rolls back namespace creation if any part of the reconciliation fails
Parameters:
| Name | Type | Description |
|---|---|---|
k8s_objects | - | List of DynamicObjects representing the desired state |
k8s_client | - | Kubernetes client for API interactions |
Returns:
Result<(), Box<dyn std::error::Error>>- Success or error with message
Source
#![allow(unused)]
fn main() {
pub async fn reconcile_target_state(
objects: &[DynamicObject],
client: Client,
stack_id: &str,
checksum: &str,
) -> Result<(), Box<dyn std::error::Error>> {
info!(
"Starting reconciliation with stack_id={}, checksum={}",
stack_id, checksum
);
// If we have objects to apply, handle them in dependency order
if !objects.is_empty() {
// Separate priority objects (Namespaces, CRDs) from regular objects
// Priority objects must be applied first before we can validate namespaced resources
let (priority_objects, regular_objects): (Vec<_>, Vec<_>) =
objects.iter().partition(|obj| {
obj.types
.as_ref()
.map(|t| t.kind == "Namespace" || t.kind == "CustomResourceDefinition")
.unwrap_or(false)
});
// Track namespaces we create so we can roll them back on failure
let mut created_namespaces: Vec<String> = Vec::new();
// Apply priority objects (Namespaces, CRDs) first without validation
// These are cluster-scoped and don't have namespace dependencies
if !priority_objects.is_empty() {
info!(
"Applying {} priority resources (Namespaces/CRDs) first",
priority_objects.len()
);
for object in &priority_objects {
// Track namespace names for potential rollback
if object
.types
.as_ref()
.map(|t| t.kind == "Namespace")
.unwrap_or(false)
{
if let Some(name) = &object.metadata.name {
created_namespaces.push(name.clone());
}
}
if let Err(e) = apply_single_object(object, &client, stack_id, checksum).await {
// Rollback: delete any namespaces we created
rollback_namespaces(&client, &created_namespaces).await;
return Err(e);
}
}
}
// Now validate remaining objects (namespaces exist at this point)
if !regular_objects.is_empty() {
debug!("Validating {} regular objects", regular_objects.len());
let regular_refs: Vec<DynamicObject> =
regular_objects.iter().map(|o| (*o).clone()).collect();
if let Err(e) = validate_k8s_objects(®ular_refs, client.clone()).await {
error!("Validation failed: {}", e);
// Rollback: delete any namespaces we created
rollback_namespaces(&client, &created_namespaces).await;
return Err(e);
}
debug!("All objects validated successfully");
}
// Apply all resources with server-side apply
// (Priority objects were already applied, but applying again is idempotent)
info!("Applying {} resources", objects.len());
for object in objects {
let kind = object
.types
.as_ref()
.map(|t| t.kind.clone())
.unwrap_or_default();
let namespace = object
.metadata
.namespace
.as_deref()
.unwrap_or("default")
.to_string();
let name = object.metadata.name.as_deref().unwrap_or("").to_string();
let key = format!("{}:{}@{}", kind, name, namespace);
debug!(
"Processing object: kind={}, namespace={}, name={}",
kind, namespace, name
);
// Prepare object with annotations
let mut object = object.clone();
let annotations = object
.metadata
.annotations
.get_or_insert_with(BTreeMap::new);
annotations.insert(STACK_LABEL.to_string(), stack_id.to_string());
annotations.insert(CHECKSUM_ANNOTATION.to_string(), checksum.to_string());
let mut params = PatchParams::apply("brokkr-controller");
params.force = true;
if let Some(gvk) = object.types.as_ref() {
let gvk = GroupVersionKind::try_from(gvk)?;
if let Some((ar, caps)) = Discovery::new(client.clone())
.run()
.await?
.resolve_gvk(&gvk)
{
let api = dynamic_api(ar, caps, client.clone(), Some(&namespace), false);
let patch = Patch::Apply(&object);
match api.patch(&name, ¶ms, &patch).await {
Ok(_) => debug!("Successfully applied {}", key),
Err(e) => {
error!("Failed to apply {}: {}", key, e);
// Rollback: delete any namespaces we created
rollback_namespaces(&client, &created_namespaces).await;
return Err(Box::new(e));
}
}
}
}
}
} else {
info!("No objects in desired state, will remove all existing objects in stack");
}
// Get existing resources with this stack ID after applying changes
debug!("Fetching existing resources for stack {}", stack_id);
let existing = get_all_objects_by_annotation(&client, STACK_LABEL, stack_id).await?;
debug!("Found {} existing resources", existing.len());
// Prune objects that are no longer in the desired state
for existing_obj in existing {
let kind = existing_obj
.types
.as_ref()
.map(|t| t.kind.clone())
.unwrap_or_default();
let namespace = existing_obj
.metadata
.namespace
.as_deref()
.unwrap_or("default")
.to_string();
let name = existing_obj
.metadata
.name
.as_deref()
.unwrap_or("")
.to_string();
let key = format!("{}:{}@{}", kind, name, namespace);
// Skip if object has owner references
if let Some(owner_refs) = &existing_obj.metadata.owner_references {
if !owner_refs.is_empty() {
debug!("Skipping object {} with owner references", key);
continue;
}
}
// Delete if checksum doesn't match the new checksum
let existing_checksum = existing_obj
.metadata
.annotations
.as_ref()
.and_then(|anns| anns.get(CHECKSUM_ANNOTATION))
.map_or("".to_string(), |v| v.to_string());
if existing_checksum != checksum {
info!(
"Deleting object {} (checksum mismatch: {} != {})",
key, existing_checksum, checksum
);
if let Some(gvk) = existing_obj.types.as_ref() {
let gvk = GroupVersionKind::try_from(gvk)?;
if let Some((ar, caps)) = Discovery::new(client.clone())
.run()
.await?
.resolve_gvk(&gvk)
{
let api = dynamic_api(ar, caps, client.clone(), Some(&namespace), false);
match api.delete(&name, &DeleteParams::default()).await {
Ok(_) => debug!("Successfully deleted {}", key),
Err(e) => {
error!("Failed to delete {}: {}", key, e);
return Err(Box::new(e));
}
}
}
}
} else {
debug!("Keeping object {} (checksum matches: {})", key, checksum);
}
}
info!("Reconciliation completed successfully");
Ok(())
}
}
brokkr-agent::k8s::api::create_k8s_client
pub
#![allow(unused)]
fn main() {
async fn create_k8s_client (kubeconfig_path : Option < & str > ,) -> Result < K8sClient , Box < dyn std :: error :: Error > >
}
Creates a Kubernetes client using either a provided kubeconfig path or default configuration.
Parameters:
| Name | Type | Description |
|---|---|---|
kubeconfig_path | - | Optional path to kubeconfig file |
Returns:
Result<K8sClient, Box<dyn std::error::Error>>- Kubernetes client or error
Source
#![allow(unused)]
fn main() {
pub async fn create_k8s_client(
kubeconfig_path: Option<&str>,
) -> Result<K8sClient, Box<dyn std::error::Error>> {
// Set KUBECONFIG environment variable if path is provided
if let Some(path) = kubeconfig_path {
info!("Setting KUBECONFIG environment variable to: {}", path);
std::env::set_var("KUBECONFIG", path);
} else {
info!("Using default Kubernetes configuration");
}
let client = K8sClient::try_default()
.await
.map_err(|e| format!("Failed to create Kubernetes client: {}", e))?;
// Verify cluster connectivity using API server version (doesn't require RBAC permissions)
match client.apiserver_version().await {
Ok(version) => {
info!(
"Successfully connected to Kubernetes cluster (version: {}.{})",
version.major, version.minor
);
}
Err(e) => {
error!("Failed to verify Kubernetes cluster connectivity: {}", e);
return Err(format!("Failed to connect to Kubernetes cluster: {}", e).into());
}
}
Ok(client)
}
}
brokkr-agent::k8s::objects Rust
Functions
brokkr-agent::k8s::objects::create_k8s_objects
pub
#![allow(unused)]
fn main() {
fn create_k8s_objects (deployment_object : DeploymentObject , agent_id : Uuid ,) -> Result < Vec < DynamicObject > , Box < dyn std :: error :: Error > >
}
Creates Kubernetes objects from a brokkr deployment object’s YAML content.
Parameters:
| Name | Type | Description |
|---|---|---|
deployment_object | - | The deployment object containing YAML content |
Returns:
Result<Vec<DynamicObject>, Box<dyn std::error::Error>>- List of created K8s objects or error
Source
#![allow(unused)]
fn main() {
pub fn create_k8s_objects(
deployment_object: DeploymentObject,
agent_id: Uuid,
) -> Result<Vec<DynamicObject>, Box<dyn std::error::Error>> {
let mut k8s_objects = Vec::new();
let yaml_docs = utils::multidoc_deserialize(&deployment_object.yaml_content)?;
for yaml_doc in yaml_docs {
// Skip null documents
if yaml_doc.is_null() {
continue;
}
let mut obj: DynamicObject = serde_yaml::from_value(yaml_doc)?;
let mut annotations = BTreeMap::new();
annotations.insert(
STACK_LABEL.to_string(),
deployment_object.stack_id.to_string(),
);
annotations.insert(
CHECKSUM_ANNOTATION.to_string(),
deployment_object.yaml_checksum.to_string(),
);
annotations.insert(
DEPLOYMENT_OBJECT_ID_LABEL.to_string(),
deployment_object.id.to_string(),
);
annotations.insert(LAST_CONFIG_ANNOTATION.to_string(), format!("{:?}", obj));
annotations.insert(
BROKKR_AGENT_OWNER_ANNOTATION.to_string(),
agent_id.to_string(),
);
obj.metadata.annotations = Some(annotations);
let kind = obj
.types
.as_ref()
.map(|t| t.kind.clone())
.unwrap_or_default();
// Move namespace and CRDs to the front of objects list for apply
if kind == "Namespace" || kind == "CustomResourceDefinition" {
k8s_objects.insert(0, obj);
} else {
k8s_objects.push(obj);
}
}
Ok(k8s_objects)
}
}
brokkr-agent::k8s::objects::verify_object_ownership
pub
#![allow(unused)]
fn main() {
fn verify_object_ownership (object : & DynamicObject , agent_id : & Uuid) -> bool
}
Source
#![allow(unused)]
fn main() {
pub fn verify_object_ownership(object: &DynamicObject, agent_id: &Uuid) -> bool {
object
.metadata
.annotations
.as_ref()
.and_then(|annotations| annotations.get(BROKKR_AGENT_OWNER_ANNOTATION))
.map(|value| value == &agent_id.to_string())
.unwrap_or(false)
}
}
brokkr-agent::metrics Rust
Functions
brokkr-agent::metrics::registry
private
#![allow(unused)]
fn main() {
fn registry () -> & 'static Registry
}
Source
#![allow(unused)]
fn main() {
fn registry() -> &'static Registry {
REGISTRY.get_or_init(Registry::new)
}
}
brokkr-agent::metrics::poll_requests_total
pub
#![allow(unused)]
fn main() {
fn poll_requests_total () -> & 'static CounterVec
}
Broker poll request counter Labels: status (success/error)
Source
#![allow(unused)]
fn main() {
pub fn poll_requests_total() -> &'static CounterVec {
static COUNTER: OnceLock<CounterVec> = OnceLock::new();
COUNTER.get_or_init(|| {
let opts = Opts::new(
"brokkr_agent_poll_requests_total",
"Total number of broker poll requests",
);
let counter =
CounterVec::new(opts, &["status"]).expect("Failed to create poll requests counter");
registry()
.register(Box::new(counter.clone()))
.expect("Failed to register poll requests counter");
counter
})
}
}
brokkr-agent::metrics::poll_duration_seconds
pub
#![allow(unused)]
fn main() {
fn poll_duration_seconds () -> & 'static HistogramVec
}
Broker poll duration histogram
Source
#![allow(unused)]
fn main() {
pub fn poll_duration_seconds() -> &'static HistogramVec {
static HISTOGRAM: OnceLock<HistogramVec> = OnceLock::new();
HISTOGRAM.get_or_init(|| {
let opts = HistogramOpts::new(
"brokkr_agent_poll_duration_seconds",
"Broker poll request latency distribution in seconds",
)
.buckets(vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0]);
let histogram =
HistogramVec::new(opts, &[]).expect("Failed to create poll duration histogram");
registry()
.register(Box::new(histogram.clone()))
.expect("Failed to register poll duration histogram");
histogram
})
}
}
brokkr-agent::metrics::kubernetes_operations_total
pub
#![allow(unused)]
fn main() {
fn kubernetes_operations_total () -> & 'static CounterVec
}
Kubernetes operations counter Labels: operation (apply/delete/get/list)
Source
#![allow(unused)]
fn main() {
pub fn kubernetes_operations_total() -> &'static CounterVec {
static COUNTER: OnceLock<CounterVec> = OnceLock::new();
COUNTER.get_or_init(|| {
let opts = Opts::new(
"brokkr_agent_kubernetes_operations_total",
"Total number of Kubernetes API operations by type",
);
let counter = CounterVec::new(opts, &["operation"])
.expect("Failed to create Kubernetes operations counter");
registry()
.register(Box::new(counter.clone()))
.expect("Failed to register Kubernetes operations counter");
counter
})
}
}
brokkr-agent::metrics::kubernetes_operation_duration_seconds
pub
#![allow(unused)]
fn main() {
fn kubernetes_operation_duration_seconds () -> & 'static HistogramVec
}
Kubernetes operation duration histogram Labels: operation (apply/delete/get/list)
Source
#![allow(unused)]
fn main() {
pub fn kubernetes_operation_duration_seconds() -> &'static HistogramVec {
static HISTOGRAM: OnceLock<HistogramVec> = OnceLock::new();
HISTOGRAM.get_or_init(|| {
let opts = HistogramOpts::new(
"brokkr_agent_kubernetes_operation_duration_seconds",
"Kubernetes operation latency distribution in seconds",
)
.buckets(vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]);
let histogram = HistogramVec::new(opts, &["operation"])
.expect("Failed to create Kubernetes operation duration histogram");
registry()
.register(Box::new(histogram.clone()))
.expect("Failed to register Kubernetes operation duration histogram");
histogram
})
}
}
brokkr-agent::metrics::heartbeat_sent_total
pub
#![allow(unused)]
fn main() {
fn heartbeat_sent_total () -> & 'static IntCounter
}
Heartbeat sent counter
Source
#![allow(unused)]
fn main() {
pub fn heartbeat_sent_total() -> &'static IntCounter {
static COUNTER: OnceLock<IntCounter> = OnceLock::new();
COUNTER.get_or_init(|| {
let opts = Opts::new(
"brokkr_agent_heartbeat_sent_total",
"Total number of heartbeats sent to broker",
);
let counter = IntCounter::with_opts(opts).expect("Failed to create heartbeat counter");
registry()
.register(Box::new(counter.clone()))
.expect("Failed to register heartbeat counter");
counter
})
}
}
brokkr-agent::metrics::last_successful_poll_timestamp
pub
#![allow(unused)]
fn main() {
fn last_successful_poll_timestamp () -> & 'static Gauge
}
Last successful poll timestamp (Unix timestamp)
Source
#![allow(unused)]
fn main() {
pub fn last_successful_poll_timestamp() -> &'static Gauge {
static GAUGE: OnceLock<Gauge> = OnceLock::new();
GAUGE.get_or_init(|| {
let opts = Opts::new(
"brokkr_agent_last_successful_poll_timestamp",
"Unix timestamp of last successful broker poll",
);
let gauge = Gauge::with_opts(opts).expect("Failed to create last poll timestamp gauge");
registry()
.register(Box::new(gauge.clone()))
.expect("Failed to register last poll timestamp gauge");
gauge
})
}
}
brokkr-agent::metrics::encode_metrics
pub
#![allow(unused)]
fn main() {
fn encode_metrics () -> String
}
Encodes all registered metrics in Prometheus text format
Returns:
Returns a String containing all metrics in Prometheus exposition format
Source
#![allow(unused)]
fn main() {
pub fn encode_metrics() -> String {
let encoder = TextEncoder::new();
let metric_families = registry().gather();
let mut buffer = vec![];
encoder
.encode(&metric_families, &mut buffer)
.expect("Failed to encode metrics");
String::from_utf8(buffer).expect("Failed to convert metrics to UTF-8")
}
}
brokkr-agent::utils Rust
Functions
brokkr-agent::utils::multidoc_deserialize
pub
#![allow(unused)]
fn main() {
fn multidoc_deserialize (multi_doc_str : & str) -> Result < Vec < serde_yaml :: Value > , Box < dyn Error > >
}
Deserializes a multi-document YAML string into a vector of YAML values.
Parameters:
| Name | Type | Description |
|---|---|---|
multi_doc_str | - | String containing multiple YAML documents |
Returns:
Result<Vec<serde_yaml::Value>, Box<dyn Error>>- Vector of parsed YAML values or error
Source
#![allow(unused)]
fn main() {
pub fn multidoc_deserialize(multi_doc_str: &str) -> Result<Vec<serde_yaml::Value>, Box<dyn Error>> {
let mut docs = vec![];
for d in serde_yaml::Deserializer::from_str(multi_doc_str) {
docs.push(serde_yaml::Value::deserialize(d)?);
}
Ok(docs)
}
}
brokkr-agent::webhooks Rust
Webhook delivery module for agent-side webhook processing.
This module provides functionality for agents to poll for pending webhooks assigned to them, deliver them via HTTP, and report results back to the broker.
Structs
brokkr-agent::webhooks::PendingWebhookDelivery
pub
Derives: Debug, Clone, Deserialize
Pending webhook delivery from the broker. Contains decrypted URL and auth header for delivery.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Delivery ID. |
subscription_id | Uuid | Subscription ID. |
event_type | String | Event type being delivered. |
payload | String | JSON-encoded event payload. |
url | String | Decrypted webhook URL. |
auth_header | Option < String > | Decrypted Authorization header (if configured). |
timeout_seconds | i32 | HTTP timeout in seconds. |
max_retries | i32 | Maximum retries for this subscription. |
attempts | i32 | Current attempt number. |
brokkr-agent::webhooks::DeliveryResultRequest
pub
Derives: Debug, Clone, Serialize
Request body for reporting delivery result to broker.
Fields
| Name | Type | Description |
|---|---|---|
success | bool | Whether delivery succeeded. |
status_code | Option < i32 > | HTTP status code (if available). |
error | Option < String > | Error message (if failed). |
duration_ms | Option < i64 > | Delivery duration in milliseconds. |
brokkr-agent::webhooks::DeliveryResult
pub
Derives: Debug
Result of a webhook delivery attempt.
Fields
| Name | Type | Description |
|---|---|---|
success | bool | Whether delivery succeeded. |
status_code | Option < i32 > | HTTP status code (if available). |
error | Option < String > | Error message (if failed). |
duration_ms | i64 | Delivery duration in milliseconds. |
Functions
brokkr-agent::webhooks::fetch_pending_webhooks
pub
#![allow(unused)]
fn main() {
async fn fetch_pending_webhooks (config : & Settings , client : & Client , agent : & Agent ,) -> Result < Vec < PendingWebhookDelivery > , Box < dyn std :: error :: Error > >
}
Fetches pending webhook deliveries for this agent from the broker.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing broker configuration |
client | - | HTTP client for making requests |
agent | - | Agent details |
Returns:
Pending webhook deliveries or error
Source
#![allow(unused)]
fn main() {
pub async fn fetch_pending_webhooks(
config: &Settings,
client: &Client,
agent: &Agent,
) -> Result<Vec<PendingWebhookDelivery>, Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/agents/{}/webhooks/pending",
config.agent.broker_url, agent.id
);
debug!("Fetching pending webhooks from {}", url);
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.send()
.await
.map_err(|e| {
error!("Failed to fetch pending webhooks: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK => {
let deliveries: Vec<PendingWebhookDelivery> = response.json().await.map_err(|e| {
error!("Failed to deserialize pending webhooks: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
if !deliveries.is_empty() {
debug!(
"Fetched {} pending webhook deliveries for agent {}",
deliveries.len(),
agent.name
);
}
Ok(deliveries)
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Failed to fetch pending webhooks. Status {}: {}",
status, error_body
);
Err(format!(
"Failed to fetch pending webhooks. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::webhooks::report_delivery_result
pub
#![allow(unused)]
fn main() {
async fn report_delivery_result (config : & Settings , client : & Client , delivery_id : Uuid , result : & DeliveryResult ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Reports the result of a webhook delivery attempt to the broker.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings containing broker configuration |
client | - | HTTP client for making requests |
delivery_id | - | ID of the delivery being reported |
result | - | The delivery result |
Returns:
Success or error
Source
#![allow(unused)]
fn main() {
pub async fn report_delivery_result(
config: &Settings,
client: &Client,
delivery_id: Uuid,
result: &DeliveryResult,
) -> Result<(), Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/webhook-deliveries/{}/result",
config.agent.broker_url, delivery_id
);
debug!("Reporting delivery result for {} to {}", delivery_id, url);
let request_body = DeliveryResultRequest {
success: result.success,
status_code: result.status_code,
error: result.error.clone(),
duration_ms: Some(result.duration_ms),
};
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.json(&request_body)
.send()
.await
.map_err(|e| {
error!("Failed to report delivery result: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK => {
debug!("Successfully reported delivery result for {}", delivery_id);
Ok(())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Failed to report delivery result for {}. Status {}: {}",
delivery_id, status, error_body
);
Err(format!(
"Failed to report delivery result. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::webhooks::deliver_webhook
pub
#![allow(unused)]
fn main() {
async fn deliver_webhook (delivery : & PendingWebhookDelivery) -> DeliveryResult
}
Delivers a webhook via HTTP POST.
Parameters:
| Name | Type | Description |
|---|---|---|
delivery | - | The pending webhook delivery with URL and payload |
Returns:
DeliveryResult with success/failure info and timing
Source
#![allow(unused)]
fn main() {
pub async fn deliver_webhook(delivery: &PendingWebhookDelivery) -> DeliveryResult {
let start = Instant::now();
// Build HTTP client with timeout
let client = match reqwest::Client::builder()
.timeout(Duration::from_secs(delivery.timeout_seconds as u64))
.build()
{
Ok(c) => c,
Err(e) => {
return DeliveryResult {
success: false,
status_code: None,
error: Some(format!("Failed to create HTTP client: {}", e)),
duration_ms: start.elapsed().as_millis() as i64,
};
}
};
// Build the request
let mut request = client
.post(&delivery.url)
.header("Content-Type", "application/json")
.header("X-Brokkr-Event-Type", &delivery.event_type)
.header("X-Brokkr-Delivery-Id", delivery.id.to_string())
.body(delivery.payload.clone());
// Add authorization header if present
if let Some(ref auth) = delivery.auth_header {
request = request.header("Authorization", auth);
}
// Send the request
match request.send().await {
Ok(response) => {
let status_code = response.status().as_u16() as i32;
let duration_ms = start.elapsed().as_millis() as i64;
if response.status().is_success() {
debug!(
"Webhook delivery {} succeeded with status {} in {}ms",
delivery.id, status_code, duration_ms
);
DeliveryResult {
success: true,
status_code: Some(status_code),
error: None,
duration_ms,
}
} else {
// Get error body for context (limit to 500 chars)
let error_body = response
.text()
.await
.unwrap_or_else(|_| "Failed to read response body".to_string());
let error_preview: String = error_body.chars().take(500).collect();
warn!(
"Webhook delivery {} failed with status {}: {}",
delivery.id, status_code, error_preview
);
DeliveryResult {
success: false,
status_code: Some(status_code),
error: Some(format!("HTTP {}: {}", status_code, error_preview)),
duration_ms,
}
}
}
Err(e) => {
let duration_ms = start.elapsed().as_millis() as i64;
let error_msg = classify_error(&e);
warn!(
"Webhook delivery {} failed after {}ms: {}",
delivery.id, duration_ms, error_msg
);
DeliveryResult {
success: false,
status_code: None,
error: Some(error_msg),
duration_ms,
}
}
}
}
}
brokkr-agent::webhooks::classify_error
private
#![allow(unused)]
fn main() {
fn classify_error (error : & reqwest :: Error) -> String
}
Classifies request errors for logging and retry decisions.
Source
#![allow(unused)]
fn main() {
fn classify_error(error: &reqwest::Error) -> String {
if error.is_timeout() {
"Request timed out".to_string()
} else if error.is_connect() {
"Connection failed".to_string()
} else if error.is_request() {
format!("Request error: {}", error)
} else {
format!("Error: {}", error)
}
}
}
brokkr-agent::webhooks::process_pending_webhooks
pub
#![allow(unused)]
fn main() {
async fn process_pending_webhooks (config : & Settings , client : & Client , agent : & Agent ,) -> Result < usize , Box < dyn std :: error :: Error > >
}
Processes all pending webhook deliveries for this agent.
This function:
- Fetches pending webhooks from the broker
- Delivers each webhook via HTTP
- Reports results back to the broker
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings |
client | - | HTTP client for broker communication |
agent | - | Agent details |
Returns:
Number of webhooks processed or error
Source
#![allow(unused)]
fn main() {
pub async fn process_pending_webhooks(
config: &Settings,
client: &Client,
agent: &Agent,
) -> Result<usize, Box<dyn std::error::Error>> {
// Fetch pending deliveries from broker
let deliveries = fetch_pending_webhooks(config, client, agent).await?;
if deliveries.is_empty() {
return Ok(0);
}
info!(
"Processing {} pending webhook deliveries for agent {}",
deliveries.len(),
agent.name
);
let mut processed = 0;
for delivery in deliveries {
debug!(
"Delivering webhook {} (event: {}, attempt: {})",
delivery.id,
delivery.event_type,
delivery.attempts + 1
);
// Deliver the webhook
let result = deliver_webhook(&delivery).await;
// Report result to broker
if let Err(e) = report_delivery_result(config, client, delivery.id, &result).await {
error!(
"Failed to report delivery result for {}: {}",
delivery.id, e
);
// Continue processing other deliveries even if reporting fails
}
processed += 1;
if result.success {
info!(
"Webhook delivery {} succeeded in {}ms",
delivery.id, result.duration_ms
);
} else {
warn!(
"Webhook delivery {} failed: {:?}",
delivery.id,
result.error.as_deref().unwrap_or("unknown error")
);
}
}
Ok(processed)
}
}
brokkr-agent::work_orders Rust
Functions
brokkr-agent::work_orders::is_error_retryable
private
#![allow(unused)]
fn main() {
fn is_error_retryable (error : & dyn std :: error :: Error) -> bool
}
Determines if an error is retryable by inspecting the error message.
Non-retryable errors include:
- 404 NotFound (resource doesn’t exist)
- 403 Forbidden (permission denied)
- 400 BadRequest (malformed request)
- Validation errors Retryable errors include:
- 429 TooManyRequests
- 500 InternalServerError
- 503 ServiceUnavailable
- 504 GatewayTimeout
- Network/connectivity errors
Source
#![allow(unused)]
fn main() {
fn is_error_retryable(error: &dyn std::error::Error) -> bool {
let error_str = error.to_string().to_lowercase();
// Non-retryable patterns (permanent failures)
let non_retryable_patterns = [
"notfound",
"not found",
"forbidden",
"unauthorized",
"badrequest",
"bad request",
"invalid",
"unprocessable",
"conflict",
];
for pattern in &non_retryable_patterns {
if error_str.contains(pattern) {
debug!(
"Error classified as non-retryable (matched '{}'): {}",
pattern, error
);
return false;
}
}
// Retryable patterns (transient failures)
let retryable_patterns = [
"timeout",
"unavailable",
"connection",
"network",
"internal",
"too many requests",
"throttl",
];
for pattern in &retryable_patterns {
if error_str.contains(pattern) {
debug!(
"Error classified as retryable (matched '{}'): {}",
pattern, error
);
return true;
}
}
// Default to non-retryable for unknown errors
// This prevents infinite retry loops for unhandled cases
debug!(
"Error classified as non-retryable (no pattern match): {}",
error
);
false
}
}
brokkr-agent::work_orders::process_pending_work_orders
pub
#![allow(unused)]
fn main() {
async fn process_pending_work_orders (config : & Settings , http_client : & Client , k8s_client : & K8sClient , agent : & Agent ,) -> Result < usize , Box < dyn std :: error :: Error > >
}
Processes pending work orders for the agent.
This function:
- Fetches pending work orders from the broker
- Claims the first available work order
- Executes the work based on work type
- Reports completion to the broker
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings |
http_client | - | HTTP client for broker communication |
k8s_client | - | Kubernetes client for resource operations |
agent | - | Agent details |
Returns:
Number of work orders processed
Source
#![allow(unused)]
fn main() {
pub async fn process_pending_work_orders(
config: &Settings,
http_client: &Client,
k8s_client: &K8sClient,
agent: &Agent,
) -> Result<usize, Box<dyn std::error::Error>> {
// Fetch pending work orders
let pending = broker::fetch_pending_work_orders(config, http_client, agent, None).await?;
if pending.is_empty() {
trace!("No pending work orders for agent {}", agent.name);
return Ok(0);
}
info!(
"Found {} pending work orders for agent {}",
pending.len(),
agent.name
);
let mut processed = 0;
// Process one work order at a time
// In the future, we could parallelize this based on work type
for work_order in pending.iter().take(1) {
match process_single_work_order(config, http_client, k8s_client, agent, work_order).await {
Ok(_) => {
processed += 1;
info!(
"Successfully processed work order {} (type: {})",
work_order.id, work_order.work_type
);
}
Err(e) => {
error!(
"Failed to process work order {} (type: {}): {}",
work_order.id, work_order.work_type, e
);
// Continue with next work order instead of failing completely
}
}
}
Ok(processed)
}
}
brokkr-agent::work_orders::process_single_work_order
private
#![allow(unused)]
fn main() {
async fn process_single_work_order (config : & Settings , http_client : & Client , k8s_client : & K8sClient , agent : & Agent , work_order : & WorkOrder ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Processes a single work order through its complete lifecycle.
Source
#![allow(unused)]
fn main() {
async fn process_single_work_order(
config: &Settings,
http_client: &Client,
k8s_client: &K8sClient,
agent: &Agent,
work_order: &WorkOrder,
) -> Result<(), Box<dyn std::error::Error>> {
info!(
"Processing work order {} (type: {}, status: {})",
work_order.id, work_order.work_type, work_order.status
);
// Claim the work order
let claimed = broker::claim_work_order(config, http_client, agent, work_order.id).await?;
info!("Successfully claimed work order {}", claimed.id);
// Execute based on work type
let result = match work_order.work_type.as_str() {
"build" => execute_build_work_order(config, http_client, k8s_client, agent, &claimed).await,
"custom" => execute_custom_work_order(k8s_client, agent, &claimed).await,
unknown => Err(format!("Unknown work type: {}", unknown).into()),
};
// Report completion
match result {
Ok(message) => {
broker::complete_work_order(config, http_client, work_order.id, true, message, true)
.await?;
info!("Work order {} completed successfully", work_order.id);
}
Err(e) => {
let error_msg = e.to_string();
let retryable = is_error_retryable(e.as_ref());
if retryable {
warn!(
"Work order {} failed with retryable error: {}",
work_order.id, e
);
} else {
error!(
"Work order {} failed with non-retryable error: {}",
work_order.id, e
);
}
broker::complete_work_order(
config,
http_client,
work_order.id,
false,
Some(error_msg),
retryable,
)
.await?;
return Err(e);
}
}
Ok(())
}
}
brokkr-agent::work_orders::execute_build_work_order
private
#![allow(unused)]
fn main() {
async fn execute_build_work_order (_config : & Settings , _http_client : & Client , k8s_client : & K8sClient , agent : & Agent , work_order : & WorkOrder ,) -> Result < Option < String > , Box < dyn std :: error :: Error > >
}
Executes a build work order using Shipwright.
Source
#![allow(unused)]
fn main() {
async fn execute_build_work_order(
_config: &Settings,
_http_client: &Client,
k8s_client: &K8sClient,
agent: &Agent,
work_order: &WorkOrder,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
info!(
"Executing build work order {} for agent {}",
work_order.id, agent.name
);
// Parse the YAML content to extract Build and WorkOrder resources
let yaml_docs = crate::utils::multidoc_deserialize(&work_order.yaml_content)?;
if yaml_docs.is_empty() {
return Err("Work order YAML content is empty".into());
}
// Apply all K8s resources from the YAML
// The YAML should contain Shipwright Build + brokkr WorkOrder CRD
for _doc in &yaml_docs {
debug!("Applying K8s resource from work order YAML");
// We'll implement the actual application in the build module
}
// Execute the build using the build handler
let result = build::execute_build(
k8s_client,
&work_order.yaml_content,
&work_order.id.to_string(),
)
.await?;
Ok(result)
}
}
brokkr-agent::work_orders::execute_custom_work_order
private
#![allow(unused)]
fn main() {
async fn execute_custom_work_order (k8s_client : & K8sClient , agent : & Agent , work_order : & WorkOrder ,) -> Result < Option < String > , Box < dyn std :: error :: Error > >
}
Executes a custom work order by applying YAML resources to the cluster.
Source
#![allow(unused)]
fn main() {
async fn execute_custom_work_order(
k8s_client: &K8sClient,
agent: &Agent,
work_order: &WorkOrder,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
use kube::api::{DynamicObject, PatchParams};
info!(
"Executing custom work order {} for agent {}",
work_order.id, agent.name
);
// Parse the YAML content
let yaml_docs = crate::utils::multidoc_deserialize(&work_order.yaml_content)?;
if yaml_docs.is_empty() {
return Err("Work order YAML content is empty".into());
}
// Convert YAML docs to DynamicObjects
let mut objects: Vec<DynamicObject> = Vec::new();
for yaml_doc in &yaml_docs {
// Skip null documents
if yaml_doc.is_null() {
continue;
}
let object: DynamicObject = serde_yaml::from_value(yaml_doc.clone())?;
let gvk = object
.types
.as_ref()
.ok_or("Object missing type metadata")?;
debug!(
"Parsed {} {}/{}",
gvk.kind,
object.metadata.namespace.as_deref().unwrap_or("cluster"),
object.metadata.name.as_deref().unwrap_or("unnamed")
);
objects.push(object);
}
if objects.is_empty() {
return Err("No valid Kubernetes objects found in YAML".into());
}
info!(
"Applying {} resource(s) from custom work order {}",
objects.len(),
work_order.id
);
// Apply all resources using server-side apply
let patch_params = PatchParams::apply("brokkr-agent").force();
crate::k8s::api::apply_k8s_objects(&objects, k8s_client.clone(), patch_params).await?;
Ok(Some(format!(
"Successfully applied {} resource(s)",
objects.len()
)))
}
}
brokkr-agent::work_orders::broker Rust
Broker communication for work order operations.
This module handles all HTTP communication with the broker for work orders:
- Fetching pending work orders
- Claiming work orders
- Reporting completion (success/failure)
Structs
brokkr-agent::work_orders::broker::ClaimRequest
private
Derives: Debug, Serialize
Request body for claiming a work order.
Fields
| Name | Type | Description |
|---|---|---|
agent_id | Uuid |
brokkr-agent::work_orders::broker::CompleteRequest
private
Derives: Debug, Serialize
Request body for completing a work order.
Fields
| Name | Type | Description |
|---|---|---|
success | bool | |
message | Option < String > | |
retryable | bool | Whether the error is retryable. Only meaningful when success=false. |
| If false, the broker will immediately fail the work order without retry. |
brokkr-agent::work_orders::broker::RetryResponse
private
Derives: Debug, Deserialize
Response for retry scheduling.
Fields
| Name | Type | Description |
|---|---|---|
status | String |
Functions
brokkr-agent::work_orders::broker::fetch_pending_work_orders
pub
#![allow(unused)]
fn main() {
async fn fetch_pending_work_orders (config : & Settings , client : & Client , agent : & Agent , work_type : Option < & str > ,) -> Result < Vec < WorkOrder > , Box < dyn std :: error :: Error > >
}
Fetches pending work orders for the agent from the broker.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings |
client | - | HTTP client |
agent | - | Agent details |
work_type | - | Optional filter by work type |
Returns:
Vector of pending work orders that can be claimed by this agent
Source
#![allow(unused)]
fn main() {
pub async fn fetch_pending_work_orders(
config: &Settings,
client: &Client,
agent: &Agent,
work_type: Option<&str>,
) -> Result<Vec<WorkOrder>, Box<dyn std::error::Error>> {
let mut url = format!(
"{}/api/v1/agents/{}/work-orders/pending",
config.agent.broker_url, agent.id
);
if let Some(wt) = work_type {
url.push_str(&format!("?work_type={}", wt));
}
debug!("Fetching pending work orders from {}", url);
let response = client
.get(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.send()
.await
.map_err(|e| {
error!("Failed to fetch pending work orders: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK => {
let work_orders: Vec<WorkOrder> = response.json().await.map_err(|e| {
error!("Failed to deserialize work orders: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
debug!(
"Successfully fetched {} pending work orders for agent {}",
work_orders.len(),
agent.name
);
Ok(work_orders)
}
StatusCode::FORBIDDEN => {
error!(
"Access denied when fetching pending work orders for agent {}",
agent.id
);
Err("Access denied".into())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Failed to fetch pending work orders. Status {}: {}",
status, error_body
);
Err(format!(
"Failed to fetch pending work orders. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::work_orders::broker::claim_work_order
pub
#![allow(unused)]
fn main() {
async fn claim_work_order (config : & Settings , client : & Client , agent : & Agent , work_order_id : Uuid ,) -> Result < WorkOrder , Box < dyn std :: error :: Error > >
}
Claims a work order for the agent.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings |
client | - | HTTP client |
agent | - | Agent details |
work_order_id | - | ID of the work order to claim |
Returns:
The claimed work order with updated status
Source
#![allow(unused)]
fn main() {
pub async fn claim_work_order(
config: &Settings,
client: &Client,
agent: &Agent,
work_order_id: Uuid,
) -> Result<WorkOrder, Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/work-orders/{}/claim",
config.agent.broker_url, work_order_id
);
debug!("Claiming work order {} at {}", work_order_id, url);
let request = ClaimRequest { agent_id: agent.id };
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.json(&request)
.send()
.await
.map_err(|e| {
error!("Failed to claim work order {}: {}", work_order_id, e);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK => {
let work_order: WorkOrder = response.json().await.map_err(|e| {
error!("Failed to deserialize claimed work order: {}", e);
Box::new(e) as Box<dyn std::error::Error>
})?;
info!(
"Successfully claimed work order {} for agent {}",
work_order_id, agent.name
);
Ok(work_order)
}
StatusCode::NOT_FOUND => {
warn!(
"Work order {} not found or not claimable by agent {}",
work_order_id, agent.id
);
Err("Work order not found or not claimable".into())
}
StatusCode::CONFLICT => {
warn!("Work order {} already claimed", work_order_id);
Err("Work order already claimed".into())
}
StatusCode::FORBIDDEN => {
error!(
"Access denied when claiming work order {} for agent {}",
work_order_id, agent.id
);
Err("Access denied".into())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Failed to claim work order {}. Status {}: {}",
work_order_id, status, error_body
);
Err(format!(
"Failed to claim work order. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::work_orders::broker::complete_work_order
pub
#![allow(unused)]
fn main() {
async fn complete_work_order (config : & Settings , client : & Client , work_order_id : Uuid , success : bool , message : Option < String > , retryable : bool ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Reports work order completion to the broker.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Application settings |
client | - | HTTP client |
work_order_id | - | ID of the work order |
success | - | Whether the work completed successfully |
message | - | Optional result message (image digest on success, error on failure) |
retryable | - | Whether a failure is retryable (ignored on success) |
Returns:
Ok(()) on success, Err on failure
Source
#![allow(unused)]
fn main() {
pub async fn complete_work_order(
config: &Settings,
client: &Client,
work_order_id: Uuid,
success: bool,
message: Option<String>,
retryable: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let url = format!(
"{}/api/v1/work-orders/{}/complete",
config.agent.broker_url, work_order_id
);
debug!(
"Completing work order {} (success: {}, retryable: {}) at {}",
work_order_id, success, retryable, url
);
let request = CompleteRequest {
success,
message,
retryable,
};
let response = client
.post(&url)
.header("Authorization", format!("Bearer {}", config.agent.pak))
.json(&request)
.send()
.await
.map_err(|e| {
error!("Failed to complete work order {}: {}", work_order_id, e);
Box::new(e) as Box<dyn std::error::Error>
})?;
match response.status() {
StatusCode::OK => {
info!(
"Successfully reported work order {} completion (success: {})",
work_order_id, success
);
Ok(())
}
StatusCode::ACCEPTED => {
// Work order scheduled for retry
info!(
"Work order {} scheduled for retry after failure",
work_order_id
);
Ok(())
}
StatusCode::NOT_FOUND => {
warn!(
"Work order {} not found when reporting completion",
work_order_id
);
Err("Work order not found".into())
}
StatusCode::FORBIDDEN => {
error!("Access denied when completing work order {}", work_order_id);
Err("Access denied".into())
}
status => {
let error_body = response.text().await.unwrap_or_default();
error!(
"Failed to complete work order {}. Status {}: {}",
work_order_id, status, error_body
);
Err(format!(
"Failed to complete work order. Status: {}, Body: {}",
status, error_body
)
.into())
}
}
}
}
brokkr-agent::work_orders::build Rust
Build handler for Shipwright Build integration.
This module handles the execution of build work orders using Shipwright:
- Parsing Build and WorkOrder resources from YAML
- Applying Build resources to the cluster
- Creating BuildRun resources
- Watching BuildRun status until completion
- Extracting results (image digest, errors)
Structs
brokkr-agent::work_orders::build::BuildRunStatus
pub(crate)
Derives: Debug, Deserialize
BuildRun status for watching completion
Fields
| Name | Type | Description |
|---|---|---|
conditions | Vec < Condition > | |
output | Option < BuildRunOutput > | |
failure_details | Option < FailureDetails > |
brokkr-agent::work_orders::build::Condition
pub(crate)
Derives: Debug, Deserialize
Fields
| Name | Type | Description |
|---|---|---|
condition_type | String | |
status | String | |
reason | Option < String > | |
message | Option < String > |
brokkr-agent::work_orders::build::BuildRunOutput
pub(crate)
Derives: Debug, Deserialize
Fields
| Name | Type | Description |
|---|---|---|
digest | Option < String > | |
size | Option < i64 > |
brokkr-agent::work_orders::build::FailureDetails
pub(crate)
Derives: Debug, Deserialize
Fields
| Name | Type | Description |
|---|---|---|
reason | Option < String > | |
message | Option < String > |
brokkr-agent::work_orders::build::ParsedBuildInfo
pub(crate)
Derives: Debug, Clone, PartialEq
Result of parsing build YAML content
Fields
| Name | Type | Description |
|---|---|---|
build_name | String | |
build_namespace | String | |
build_docs | Vec < serde_yaml :: Value > |
Functions
brokkr-agent::work_orders::build::execute_build
pub
#![allow(unused)]
fn main() {
async fn execute_build (k8s_client : & K8sClient , yaml_content : & str , work_order_id : & str ,) -> Result < Option < String > , Box < dyn std :: error :: Error > >
}
Executes a build using Shipwright.
This function:
- Parses the YAML content to find Build resources
- Applies Build resources to the cluster
- Creates a BuildRun
- Watches the BuildRun until completion
- Returns the image digest on success or error details on failure
Parameters:
| Name | Type | Description |
|---|---|---|
k8s_client | - | Kubernetes client |
yaml_content | - | Multi-document YAML containing Build and WorkOrder |
work_order_id | - | Work order ID for labeling resources |
Returns:
Optional result message (image digest on success)
Source
#![allow(unused)]
fn main() {
pub async fn execute_build(
k8s_client: &K8sClient,
yaml_content: &str,
work_order_id: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
info!("Starting build execution for work order {}", work_order_id);
// Parse YAML documents
let docs: Vec<serde_yaml::Value> = serde_yaml::Deserializer::from_str(yaml_content)
.map(serde_yaml::Value::deserialize)
.collect::<Result<Vec<_>, _>>()?;
if docs.is_empty() {
return Err("No YAML documents found in work order content".into());
}
// Find Build resources and apply them
let mut build_name: Option<String> = None;
let mut build_namespace = String::from("default");
for doc in &docs {
let api_version = doc.get("apiVersion").and_then(|v| v.as_str());
let kind = doc.get("kind").and_then(|v| v.as_str());
match (api_version, kind) {
(Some(av), Some("Build")) if av.starts_with(SHIPWRIGHT_API_GROUP) => {
// Apply the Build resource
let metadata = doc.get("metadata");
if let Some(name) = metadata
.and_then(|m| m.get("name"))
.and_then(|n| n.as_str())
{
build_name = Some(name.to_string());
}
if let Some(ns) = metadata
.and_then(|m| m.get("namespace"))
.and_then(|n| n.as_str())
{
build_namespace = ns.to_string();
}
info!("Applying Shipwright Build resource");
apply_shipwright_resource(k8s_client, doc).await?;
}
(Some(av), Some("WorkOrder")) if av.starts_with("brokkr.io") => {
// Extract buildRef from WorkOrder if present
if let Some(spec) = doc.get("spec") {
if let Some(build_ref) = spec
.get("buildRef")
.and_then(|b| b.get("name"))
.and_then(|n| n.as_str())
{
if build_name.is_none() {
build_name = Some(build_ref.to_string());
}
}
}
debug!("Found brokkr WorkOrder resource, skipping apply (handled separately)");
}
_ => {
debug!("Skipping non-build resource: {:?}/{:?}", api_version, kind);
}
}
}
let build_name = build_name.ok_or("No Build resource or buildRef found in YAML content")?;
info!(
"Using Build '{}' in namespace '{}'",
build_name, build_namespace
);
// Create BuildRun
let buildrun_name = format!(
"{}-{}",
build_name,
&work_order_id[..8.min(work_order_id.len())]
);
info!("Creating BuildRun '{}'", buildrun_name);
let _buildrun = create_buildrun(
k8s_client,
&buildrun_name,
&build_name,
&build_namespace,
work_order_id,
)
.await?;
info!(
"BuildRun '{}' created, waiting for completion",
buildrun_name
);
// Watch BuildRun until completion
let result = watch_buildrun_completion(k8s_client, &buildrun_name, &build_namespace).await?;
Ok(result)
}
}
brokkr-agent::work_orders::build::apply_shipwright_resource
private
#![allow(unused)]
fn main() {
async fn apply_shipwright_resource (k8s_client : & K8sClient , resource : & serde_yaml :: Value ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Applies a Shipwright resource (Build) to the cluster using the core k8s apply logic.
Source
#![allow(unused)]
fn main() {
async fn apply_shipwright_resource(
k8s_client: &K8sClient,
resource: &serde_yaml::Value,
) -> Result<(), Box<dyn std::error::Error>> {
// Convert YAML to DynamicObject
let k8s_object: DynamicObject = serde_yaml::from_value(resource.clone())?;
// Use the existing apply_k8s_objects function which has proper retry logic
let patch_params = PatchParams::apply("brokkr-agent").force();
k8s::api::apply_k8s_objects(&[k8s_object], k8s_client.clone(), patch_params).await
}
}
brokkr-agent::work_orders::build::create_buildrun
private
#![allow(unused)]
fn main() {
async fn create_buildrun (k8s_client : & K8sClient , name : & str , build_name : & str , namespace : & str , work_order_id : & str ,) -> Result < DynamicObject , Box < dyn std :: error :: Error > >
}
Creates a BuildRun resource.
Source
#![allow(unused)]
fn main() {
async fn create_buildrun(
k8s_client: &K8sClient,
name: &str,
build_name: &str,
namespace: &str,
work_order_id: &str,
) -> Result<DynamicObject, Box<dyn std::error::Error>> {
// Discover the BuildRun API
let discovery = Discovery::new(k8s_client.clone()).run().await?;
let ar = discovery
.groups()
.flat_map(|g| g.recommended_resources())
.find(|(ar, _)| ar.group == SHIPWRIGHT_API_GROUP && ar.kind == "BuildRun")
.map(|(ar, _)| ar)
.ok_or("Shipwright BuildRun CRD not found in cluster")?;
let api: Api<DynamicObject> = Api::namespaced_with(k8s_client.clone(), namespace, &ar);
let buildrun_data = json!({
"apiVersion": format!("{}/{}", SHIPWRIGHT_API_GROUP, SHIPWRIGHT_API_VERSION),
"kind": "BuildRun",
"metadata": {
"name": name,
"namespace": namespace,
"labels": {
"brokkr.io/work-order-id": work_order_id,
"shipwright.io/build": build_name
}
},
"spec": {
"build": {
"name": build_name
}
}
});
let buildrun: DynamicObject = serde_json::from_value(buildrun_data)?;
let result = api.create(&PostParams::default(), &buildrun).await?;
Ok(result)
}
}
brokkr-agent::work_orders::build::watch_buildrun_completion
private
#![allow(unused)]
fn main() {
async fn watch_buildrun_completion (k8s_client : & K8sClient , name : & str , namespace : & str ,) -> Result < Option < String > , Box < dyn std :: error :: Error > >
}
Watches a BuildRun until it completes (success or failure).
Source
#![allow(unused)]
fn main() {
async fn watch_buildrun_completion(
k8s_client: &K8sClient,
name: &str,
namespace: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
// Discover the BuildRun API
let discovery = Discovery::new(k8s_client.clone()).run().await?;
let ar = discovery
.groups()
.flat_map(|g| g.recommended_resources())
.find(|(ar, _)| ar.group == SHIPWRIGHT_API_GROUP && ar.kind == "BuildRun")
.map(|(ar, _)| ar)
.ok_or("Shipwright BuildRun CRD not found in cluster")?;
let api: Api<DynamicObject> = Api::namespaced_with(k8s_client.clone(), namespace, &ar);
let start_time = std::time::Instant::now();
let timeout = Duration::from_secs(BUILD_TIMEOUT_SECS);
loop {
if start_time.elapsed() > timeout {
return Err(format!(
"BuildRun '{}' timed out after {} seconds",
name, BUILD_TIMEOUT_SECS
)
.into());
}
let buildrun = api.get(name).await?;
// Check status
if let Some(status_value) = buildrun.data.get("status") {
let status: BuildRunStatus = serde_json::from_value(status_value.clone())?;
// Find the Succeeded condition
for condition in &status.conditions {
if condition.condition_type == CONDITION_SUCCEEDED {
match condition.status.as_str() {
"True" => {
// Build succeeded
let digest = status.output.as_ref().and_then(|o| o.digest.clone());
info!(
"BuildRun '{}' completed successfully. Digest: {:?}",
name, digest
);
return Ok(digest);
}
"False" => {
// Build failed
let error_msg = status
.failure_details
.as_ref()
.map(|f| {
format!(
"{}: {}",
f.reason.as_deref().unwrap_or("Unknown"),
f.message.as_deref().unwrap_or("No message")
)
})
.or_else(|| condition.message.clone())
.unwrap_or_else(|| "Build failed".to_string());
error!("BuildRun '{}' failed: {}", name, error_msg);
return Err(error_msg.into());
}
_ => {
// Still running
debug!(
"BuildRun '{}' still in progress: {:?}",
name, condition.reason
);
}
}
}
}
}
// Wait before next check
sleep(Duration::from_secs(STATUS_POLL_INTERVAL_SECS)).await;
}
}
}
brokkr-agent::work_orders::build::parse_build_yaml
pub(crate)
#![allow(unused)]
fn main() {
fn parse_build_yaml (yaml_content : & str ,) -> Result < ParsedBuildInfo , Box < dyn std :: error :: Error > >
}
Parses YAML content to extract Build resource information.
This function finds Shipwright Build resources in the YAML and extracts:
- The build name (from Build metadata or WorkOrder buildRef)
- The namespace (defaulting to “default”)
- The Build documents to be applied
Parameters:
| Name | Type | Description |
|---|---|---|
yaml_content | - | Multi-document YAML string |
Returns:
ParsedBuildInfo with extracted build details
Source
#![allow(unused)]
fn main() {
pub(crate) fn parse_build_yaml(
yaml_content: &str,
) -> Result<ParsedBuildInfo, Box<dyn std::error::Error>> {
let docs: Vec<serde_yaml::Value> = serde_yaml::Deserializer::from_str(yaml_content)
.map(serde_yaml::Value::deserialize)
.collect::<Result<Vec<_>, _>>()?;
if docs.is_empty() {
return Err("No YAML documents found in work order content".into());
}
let mut build_name: Option<String> = None;
let mut build_namespace = String::from("default");
let mut build_docs = Vec::new();
for doc in &docs {
let api_version = doc.get("apiVersion").and_then(|v| v.as_str());
let kind = doc.get("kind").and_then(|v| v.as_str());
match (api_version, kind) {
(Some(av), Some("Build")) if av.starts_with(SHIPWRIGHT_API_GROUP) => {
let metadata = doc.get("metadata");
if let Some(name) = metadata
.and_then(|m| m.get("name"))
.and_then(|n| n.as_str())
{
build_name = Some(name.to_string());
}
if let Some(ns) = metadata
.and_then(|m| m.get("namespace"))
.and_then(|n| n.as_str())
{
build_namespace = ns.to_string();
}
build_docs.push(doc.clone());
}
(Some(av), Some("WorkOrder")) if av.starts_with("brokkr.io") => {
// Extract buildRef from WorkOrder if present
if let Some(spec) = doc.get("spec") {
if let Some(build_ref) = spec
.get("buildRef")
.and_then(|b| b.get("name"))
.and_then(|n| n.as_str())
{
if build_name.is_none() {
build_name = Some(build_ref.to_string());
}
}
}
}
_ => {
// Skip non-build resources
}
}
}
let build_name = build_name.ok_or("No Build resource or buildRef found in YAML content")?;
Ok(ParsedBuildInfo {
build_name,
build_namespace,
build_docs,
})
}
}
brokkr-agent::work_orders::build::interpret_buildrun_status
pub(crate)
#![allow(unused)]
fn main() {
fn interpret_buildrun_status (status : & BuildRunStatus) -> Result < Option < String > , String >
}
Interprets a BuildRun status to determine completion state.
Returns:
Ok(Some(digest))if the build succeeded -Err(message)if the build failed -Ok(None)if the build is still in progress
Source
#![allow(unused)]
fn main() {
pub(crate) fn interpret_buildrun_status(status: &BuildRunStatus) -> Result<Option<String>, String> {
for condition in &status.conditions {
if condition.condition_type == CONDITION_SUCCEEDED {
match condition.status.as_str() {
"True" => {
// Build succeeded - extract digest
let digest = status.output.as_ref().and_then(|o| o.digest.clone());
return Ok(digest);
}
"False" => {
// Build failed - extract error message
let error_msg = status
.failure_details
.as_ref()
.map(|f| {
format!(
"{}: {}",
f.reason.as_deref().unwrap_or("Unknown"),
f.message.as_deref().unwrap_or("No message")
)
})
.or_else(|| condition.message.clone())
.unwrap_or_else(|| "Build failed".to_string());
return Err(error_msg);
}
_ => {
// Still running (Unknown status)
return Ok(None);
}
}
}
}
// No Succeeded condition found yet - still initializing
Ok(None)
}
}
brokkr-broker Rust
brokkr-broker::api Rust
Functions
brokkr-broker::api::configure_api_routes
pub
#![allow(unused)]
fn main() {
fn configure_api_routes (dal : DAL , cors_config : & Cors , reloadable_config : Option < ReloadableConfig > ,) -> Router < DAL >
}
Configures and returns the main application router with all API routes
This function is responsible for setting up the entire API structure of the application. It merges routes from all submodules and adds a health check endpoint.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | An instance of the Data Access Layer |
cors_config | - | CORS configuration settings |
reloadable_config | - | Optional reloadable configuration for hot-reload support |
Returns:
Returns a configured Router instance that includes all API routes and middleware.
Source
#![allow(unused)]
fn main() {
pub fn configure_api_routes(
dal: DAL,
cors_config: &Cors,
reloadable_config: Option<ReloadableConfig>,
) -> Router<DAL> {
// Build a permissive CORS layer for health/metrics endpoints
let root_cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
Router::new()
.merge(v1::routes(dal.clone(), cors_config, reloadable_config))
.route("/healthz", get(healthz))
.route("/readyz", get(readyz))
.route("/metrics", get(metrics_handler))
.layer(root_cors)
.layer(middleware::from_fn(metrics_middleware))
.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &hyper::Request<_>| {
tracing::span!(
Level::INFO,
"http_request",
method = %request.method(),
uri = %request.uri(),
version = ?request.version(),
)
})
.on_response(
|response: &hyper::Response<_>,
latency: std::time::Duration,
_span: &tracing::Span| {
tracing::info!(
status = %response.status(),
latency_ms = latency.as_millis(),
"response"
);
},
),
)
}
}
brokkr-broker::api::healthz
private
#![allow(unused)]
fn main() {
async fn healthz () -> impl IntoResponse
}
Health check endpoint handler
This handler responds to GET requests at the “/healthz” endpoint. It’s used to verify that the API is up and running.
Returns:
Returns a 200 OK status code with “OK” in the body.
Source
#![allow(unused)]
fn main() {
async fn healthz() -> impl IntoResponse {
(StatusCode::OK, "OK")
}
}
brokkr-broker::api::readyz
private
#![allow(unused)]
fn main() {
async fn readyz () -> impl IntoResponse
}
Ready check endpoint handler
This handler responds to GET requests at the “/readyz” endpoint. It’s used to verify that the API is ready for use.
Returns:
Returns a 200 OK status code with “Ready” in the body.
Source
#![allow(unused)]
fn main() {
async fn readyz() -> impl IntoResponse {
(StatusCode::OK, "Ready")
}
}
brokkr-broker::api::metrics_handler
private
#![allow(unused)]
fn main() {
async fn metrics_handler () -> impl IntoResponse
}
Metrics endpoint handler
This handler responds to GET requests at the “/metrics” endpoint. It’s used to provide Prometheus metrics about the broker’s operation.
Returns:
Returns a 200 OK status code with Prometheus metrics in text format.
Source
#![allow(unused)]
fn main() {
async fn metrics_handler() -> impl IntoResponse {
let metrics_data = metrics::encode_metrics();
(
StatusCode::OK,
[("Content-Type", "text/plain; version=0.0.4")],
metrics_data,
)
}
}
brokkr-broker::api::metrics_middleware
private
#![allow(unused)]
fn main() {
async fn metrics_middleware (request : Request < Body > , next : Next) -> Response
}
Middleware to record HTTP request metrics
Records request count and duration for each HTTP request.
Source
#![allow(unused)]
fn main() {
async fn metrics_middleware(request: Request<Body>, next: Next) -> Response {
let start = Instant::now();
let method = request.method().to_string();
let path = request.uri().path().to_string();
// Process the request
let response = next.run(request).await;
// Record metrics (skip the /metrics endpoint itself to avoid recursion)
if path != "/metrics" {
let duration = start.elapsed().as_secs_f64();
let status = response.status().as_u16();
metrics::record_http_request(&path, &method, status, duration);
}
response
}
}
brokkr-broker::api::v1 Rust
API v1 module for the Brokkr broker.
This module defines the structure and routes for version 1 of the Brokkr API. It includes submodules for various API functionalities and sets up the main router with authentication middleware.
Functions
brokkr-broker::api::v1::routes
pub
#![allow(unused)]
fn main() {
fn routes (dal : DAL , cors_config : & Cors , reloadable_config : Option < ReloadableConfig > ,) -> Router < DAL >
}
Constructs and returns the main router for API v1.
This function combines all the route handlers from different modules and applies the authentication middleware.
Source
#![allow(unused)]
fn main() {
pub fn routes(
dal: DAL,
cors_config: &Cors,
reloadable_config: Option<ReloadableConfig>,
) -> Router<DAL> {
// Configure CORS from settings
let cors = build_cors_layer(cors_config);
let mut api_routes = Router::new()
.merge(agent_events::routes())
.merge(agents::routes())
.merge(auth::routes())
.merge(deployment_objects::routes())
.merge(diagnostics::routes())
.merge(generators::routes())
.merge(health::routes())
.merge(stacks::routes())
.merge(templates::routes())
.merge(webhooks::routes())
.merge(work_orders::routes())
.merge(work_orders::agent_routes())
.merge(admin::routes())
.layer(from_fn_with_state(
dal.clone(),
middleware::auth_middleware::<axum::body::Body>,
))
.layer(cors);
// Add ReloadableConfig as an extension if available
if let Some(config) = reloadable_config {
api_routes = api_routes.layer(axum::Extension(config));
}
Router::new()
.nest("/api/v1", api_routes)
.merge(openapi::configure_openapi())
}
}
brokkr-broker::api::v1::build_cors_layer
private
#![allow(unused)]
fn main() {
fn build_cors_layer (config : & Cors) -> CorsLayer
}
Builds a CORS layer from configuration.
If “*” is in the allowed_origins list, allows all origins. Otherwise, restricts to the configured origins.
Source
#![allow(unused)]
fn main() {
fn build_cors_layer(config: &Cors) -> CorsLayer {
let mut cors = CorsLayer::new();
// Handle allowed origins
if config.allowed_origins.iter().any(|o| o == "*") {
info!("CORS: Allowing all origins (not recommended for production)");
cors = cors.allow_origin(tower_http::cors::Any);
} else {
let origins: Vec<_> = config
.allowed_origins
.iter()
.filter_map(|o| o.parse().ok())
.collect();
info!("CORS: Allowing origins: {:?}", config.allowed_origins);
cors = cors.allow_origin(origins);
}
// Handle allowed methods
let methods: Vec<Method> = config
.allowed_methods
.iter()
.filter_map(|m| m.parse().ok())
.collect();
cors = cors.allow_methods(methods);
// Handle allowed headers
let headers: Vec<HeaderName> = config
.allowed_headers
.iter()
.filter_map(|h| h.parse().ok())
.collect();
cors = cors.allow_headers(headers);
// Set max age
cors = cors.max_age(Duration::from_secs(config.max_age_seconds));
cors
}
}
brokkr-broker::api::v1::admin Rust
Admin API endpoints for the Brokkr broker.
This module provides administrative endpoints for managing the broker, including configuration hot-reload functionality.
Structs
brokkr-broker::api::v1::admin::ConfigReloadResponse
pub
Derives: Debug, Serialize, ToSchema
Response structure for configuration reload operations.
Fields
| Name | Type | Description |
|---|---|---|
reloaded_at | DateTime < Utc > | Timestamp when the configuration was reloaded. |
changes | Vec < ConfigChangeInfo > | List of configuration changes detected during reload. |
success | bool | Indicates whether the reload was successful. |
message | Option < String > | Optional message providing additional context. |
brokkr-broker::api::v1::admin::ConfigChangeInfo
pub
Derives: Debug, Serialize, ToSchema
Information about a single configuration change.
Fields
| Name | Type | Description |
|---|---|---|
key | String | The configuration key that changed. |
old_value | String | The previous value (as a string representation). |
new_value | String | The new value (as a string representation). |
brokkr-broker::api::v1::admin::AuditLogQueryParams
pub
Derives: Debug, Deserialize, IntoParams
Query parameters for listing audit logs.
Fields
| Name | Type | Description |
|---|---|---|
actor_type | Option < String > | Filter by actor type (admin, agent, generator, system). |
actor_id | Option < Uuid > | Filter by actor ID. |
action | Option < String > | Filter by action (exact match or prefix with *). |
resource_type | Option < String > | Filter by resource type. |
resource_id | Option < Uuid > | Filter by resource ID. |
from | Option < DateTime < Utc > > | Filter by start time (inclusive, ISO 8601). |
to | Option < DateTime < Utc > > | Filter by end time (exclusive, ISO 8601). |
limit | Option < i64 > | Maximum number of results (default 100, max 1000). |
offset | Option < i64 > | Number of results to skip. |
brokkr-broker::api::v1::admin::AuditLogListResponse
pub
Derives: Debug, Serialize, ToSchema
Response structure for audit log list operations.
Fields
| Name | Type | Description |
|---|---|---|
logs | Vec < AuditLog > | The audit log entries. |
total | i64 | Total count of matching entries (for pagination). |
count | usize | Number of entries returned. |
limit | i64 | Limit used for this query. |
offset | i64 | Offset used for this query. |
Functions
brokkr-broker::api::v1::admin::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Constructs and returns the admin routes.
These routes require admin PAK authentication.
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
info!("Setting up admin routes");
Router::new()
.route("/admin/config/reload", post(reload_config))
.route("/admin/audit-logs", get(list_audit_logs))
}
}
brokkr-broker::api::v1::admin::reload_config
private
#![allow(unused)]
fn main() {
async fn reload_config (Extension (auth) : Extension < AuthPayload > , Extension (config) : Extension < ReloadableConfig > ,) -> Result < impl IntoResponse , StatusCode >
}
Reloads the broker configuration from disk.
This endpoint triggers a hot-reload of configurable settings without requiring a broker restart. Only settings marked as “dynamic” can be reloaded; static settings (like database URL) require a restart.
Returns:
200 OK: Configuration reloaded successfully with list of changes. -401 UNAUTHORIZED: Missing or invalid authentication. -403 FORBIDDEN: Authenticated but not an admin. -500 INTERNAL_SERVER_ERROR: Failed to reload configuration.
Source
#![allow(unused)]
fn main() {
async fn reload_config(
Extension(auth): Extension<AuthPayload>,
Extension(config): Extension<ReloadableConfig>,
) -> Result<impl IntoResponse, StatusCode> {
// Verify admin authorization
if !auth.admin {
warn!("Non-admin attempted to reload configuration");
return Err(StatusCode::FORBIDDEN);
}
info!("Admin initiated configuration reload");
// Attempt to reload configuration
match config.reload() {
Ok(changes) => {
let change_count = changes.len();
let change_infos: Vec<ConfigChangeInfo> = changes
.into_iter()
.map(|c| ConfigChangeInfo {
key: c.key,
old_value: c.old_value,
new_value: c.new_value,
})
.collect();
if change_count > 0 {
info!(
"Configuration reloaded with {} change(s): {:?}",
change_count,
change_infos.iter().map(|c| &c.key).collect::<Vec<_>>()
);
} else {
info!("Configuration reloaded with no changes detected");
}
Ok(Json(ConfigReloadResponse {
reloaded_at: Utc::now(),
changes: change_infos,
success: true,
message: if change_count > 0 {
Some(format!("{} setting(s) updated", change_count))
} else {
Some("No changes detected".to_string())
},
}))
}
Err(e) => {
error!("Failed to reload configuration: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
}
brokkr-broker::api::v1::admin::list_audit_logs
private
#![allow(unused)]
fn main() {
async fn list_audit_logs (State (dal) : State < DAL > , Extension (auth) : Extension < AuthPayload > , Query (params) : Query < AuditLogQueryParams > ,) -> Result < impl IntoResponse , (StatusCode , Json < serde_json :: Value >) >
}
Lists audit logs with optional filtering and pagination.
Returns audit log entries matching the specified filters, ordered by timestamp descending (most recent first).
Returns:
200 OK: List of audit logs with pagination info. -401 UNAUTHORIZED: Missing or invalid authentication. -403 FORBIDDEN: Authenticated but not an admin. -500 INTERNAL_SERVER_ERROR: Database error.
Source
#![allow(unused)]
fn main() {
async fn list_audit_logs(
State(dal): State<DAL>,
Extension(auth): Extension<AuthPayload>,
Query(params): Query<AuditLogQueryParams>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
// Verify admin authorization
if !auth.admin {
warn!("Non-admin attempted to access audit logs");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
let limit = params.limit.unwrap_or(100).min(1000);
let offset = params.offset.unwrap_or(0);
let filter: AuditLogFilter = params.into();
// Get total count for pagination
let total = match dal.audit_logs().count(Some(&filter)) {
Ok(count) => count,
Err(e) => {
error!("Failed to count audit logs: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to query audit logs"})),
));
}
};
// Get the logs
let logs = match dal
.audit_logs()
.list(Some(&filter), Some(limit), Some(offset))
{
Ok(logs) => logs,
Err(e) => {
error!("Failed to list audit logs: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to query audit logs"})),
));
}
};
let count = logs.len();
Ok(Json(AuditLogListResponse {
logs,
total,
count,
limit,
offset,
}))
}
}
brokkr-broker::api::v1::agent_events Rust
Handles API routes and logic for agent events.
This module provides functionality to list and retrieve agent events through HTTP endpoints.
Functions
brokkr-broker::api::v1::agent_events::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Creates and returns a router for agent event-related endpoints.
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
Router::new()
.route("/agent-events", get(list_agent_events))
.route("/agent-events/:id", get(get_agent_event))
}
}
brokkr-broker::api::v1::agent_events::list_agent_events
private
#![allow(unused)]
fn main() {
async fn list_agent_events (State (dal) : State < DAL > , Extension (_auth_payload) : Extension < crate :: api :: v1 :: middleware :: AuthPayload > ,) -> Result < Json < Vec < AgentEvent > > , (axum :: http :: StatusCode , Json < serde_json :: Value >) >
}
Retrieves a list of all agent events.
Parameters:
| Name | Type | Description |
|---|---|---|
State(dal) | - | The data access layer state. |
Extension(_auth_payload) | - | Authentication payload (unused but required). |
Returns:
A JSON response containing a vector of AgentEvents or an error.
Source
#![allow(unused)]
fn main() {
async fn list_agent_events(
State(dal): State<DAL>,
Extension(_auth_payload): Extension<crate::api::v1::middleware::AuthPayload>,
) -> Result<Json<Vec<AgentEvent>>, (axum::http::StatusCode, Json<serde_json::Value>)> {
info!("Handling request to list agent events");
match dal.agent_events().list() {
Ok(events) => {
info!("Successfully retrieved {} agent events", events.len());
Ok(Json(events))
}
Err(e) => {
error!("Failed to fetch agent events: {:?}", e);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent events"})),
))
}
}
}
}
brokkr-broker::api::v1::agent_events::get_agent_event
private
#![allow(unused)]
fn main() {
async fn get_agent_event (State (dal) : State < DAL > , Extension (_auth_payload) : Extension < crate :: api :: v1 :: middleware :: AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < AgentEvent > , (axum :: http :: StatusCode , Json < serde_json :: Value >) >
}
Retrieves a specific agent event by its ID.
Parameters:
| Name | Type | Description |
|---|---|---|
State(dal) | - | The data access layer state. |
Extension(_auth_payload) | - | Authentication payload (unused but required). |
Path(id) | - | The UUID of the agent event to retrieve. |
Returns:
A JSON response containing the requested AgentEvent or an error.
Source
#![allow(unused)]
fn main() {
async fn get_agent_event(
State(dal): State<DAL>,
Extension(_auth_payload): Extension<crate::api::v1::middleware::AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<AgentEvent>, (axum::http::StatusCode, Json<serde_json::Value>)> {
info!("Handling request to get agent event with ID: {}", id);
match dal.agent_events().get(id) {
Ok(Some(event)) => {
info!("Successfully retrieved agent event with ID: {}", id);
Ok(Json(event))
}
Ok(None) => {
warn!("Agent event with ID {} not found", id);
Err((
axum::http::StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Agent event not found"})),
))
}
Err(e) => {
error!("Error fetching agent event with ID {}: {:?}", id, e);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent event"})),
))
}
}
}
}
brokkr-broker::api::v1::agents Rust
Agent management API endpoints.
This module provides routes and handlers for managing agents, including CRUD operations, event logging, label management, annotation management, target management, and heartbeat recording.
Structs
brokkr-broker::api::v1::agents::AgentQuery
private
Derives: Deserialize
Fields
| Name | Type | Description |
|---|---|---|
name | Option < String > | |
cluster_name | Option < String > |
brokkr-broker::api::v1::agents::TargetStateParams
private
Derives: Deserialize, Default
Defines query parameters for the target state endpoint
Fields
| Name | Type | Description |
|---|---|---|
mode | Option < String > | Mode of operation: “incremental” (default) or “full” |
Functions
brokkr-broker::api::v1::agents::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Creates and returns the router for agent-related endpoints.
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
info!("Setting up agent routes");
Router::new()
.route("/agents", get(list_agents).post(create_agent))
.route("/agents/", get(search_agent))
.route(
"/agents/:id",
get(get_agent).put(update_agent).delete(delete_agent),
)
.route("/agents/:id/events", get(list_events).post(create_event))
.route("/agents/:id/labels", get(list_labels).post(add_label))
.route("/agents/:id/labels/:label", delete(remove_label))
.route(
"/agents/:id/annotations",
get(list_annotations).post(add_annotation),
)
.route("/agents/:id/annotations/:key", delete(remove_annotation))
.route("/agents/:id/targets", get(list_targets).post(add_target))
.route("/agents/:id/targets/:stack_id", delete(remove_target))
.route("/agents/:id/heartbeat", post(record_heartbeat))
.route("/agents/:id/target-state", get(get_target_state))
.route("/agents/:id/stacks", get(get_associated_stacks))
.route("/agents/:id/rotate-pak", post(rotate_agent_pak))
}
}
brokkr-broker::api::v1::agents::list_agents
private
#![allow(unused)]
fn main() {
async fn list_agents (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > ,) -> Result < Json < Vec < Agent > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists all agents.
Source
#![allow(unused)]
fn main() {
async fn list_agents(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
) -> Result<Json<Vec<Agent>>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to list agents");
if !auth_payload.admin {
warn!("Unauthorized attempt to list agents");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agents().list() {
Ok(agents) => {
info!("Successfully retrieved {} agents", agents.len());
// Update active agents metric
let active_count = agents.iter().filter(|a| a.status == "ACTIVE").count();
metrics::set_active_agents(active_count as i64);
// Update heartbeat age metrics for all agents
let now = chrono::Utc::now();
for agent in &agents {
if let Some(last_hb) = agent.last_heartbeat {
let age_seconds = (now - last_hb).num_seconds().max(0) as f64;
metrics::set_agent_heartbeat_age(
&agent.id.to_string(),
&agent.name,
age_seconds,
);
}
}
Ok(Json(agents))
}
Err(e) => {
error!("Failed to fetch agents: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agents"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::create_agent
private
#![allow(unused)]
fn main() {
async fn create_agent (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Json (new_agent) : Json < NewAgent > ,) -> Result < Json < Value > , (StatusCode , Json < serde_json :: Value >) >
}
Creates a new agent.
Source
#![allow(unused)]
fn main() {
async fn create_agent(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Json(new_agent): Json<NewAgent>,
) -> Result<Json<Value>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to create a new agent");
if !auth_payload.admin {
warn!("Unauthorized attempt to create an agent");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agents().create(&new_agent) {
Ok(agent) => {
info!("Successfully created agent with ID: {}", agent.id);
// Generate initial PAK and set PAK hash
let (pak, pak_hash) = pak::create_pak().map_err(|e| {
error!("Failed to create PAK: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create PAK"})),
)
})?;
match dal.agents().update_pak_hash(agent.id, pak_hash) {
Ok(updated_agent) => {
info!("Successfully updated PAK hash for agent ID: {}", agent.id);
// Log audit entry for agent creation
audit::log_action(
ACTOR_TYPE_ADMIN,
None,
ACTION_AGENT_CREATED,
RESOURCE_TYPE_AGENT,
Some(agent.id),
Some(serde_json::json!({
"name": updated_agent.name,
"cluster_name": updated_agent.cluster_name,
})),
None,
None,
);
let response = serde_json::json!({
"agent": updated_agent,
"initial_pak": pak
});
Ok(Json(response))
}
Err(e) => {
error!("Failed to update agent PAK hash: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update agent PAK hash"})),
))
}
}
}
Err(e) => {
error!("Failed to create agent: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create agent"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::get_agent
private
#![allow(unused)]
fn main() {
async fn get_agent (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < Agent > , (StatusCode , Json < serde_json :: Value >) >
}
Retrieves a specific agent by ID.
Source
#![allow(unused)]
fn main() {
async fn get_agent(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<Agent>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to get agent by ID: {}", id);
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!("Unauthorized attempt to get agent with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agents().get(id) {
Ok(Some(agent)) => {
info!("Successfully retrieved agent with ID: {}", id);
Ok(Json(agent))
}
Ok(None) => {
warn!("Agent not found with ID: {}", id);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Agent not found"})),
))
}
Err(e) => {
error!("Failed to fetch agent with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::search_agent
private
#![allow(unused)]
fn main() {
async fn search_agent (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Query (query) : Query < AgentQuery > ,) -> Result < Json < Agent > , (StatusCode , Json < serde_json :: Value >) >
}
Searches for an agent by name and cluster name.
Source
#![allow(unused)]
fn main() {
async fn search_agent(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Query(query): Query<AgentQuery>,
) -> Result<Json<Agent>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to search for agent");
if let (Some(name), Some(cluster_name)) = (query.name.clone(), query.cluster_name.clone()) {
info!(
"Searching for agent with name: {} and cluster_name: {}",
name, cluster_name
);
match dal
.agents()
.get_by_name_and_cluster_name(name, cluster_name)
{
Ok(Some(agent)) => {
if auth_payload.admin || auth_payload.agent == Some(agent.id) {
info!("Successfully found agent with ID: {}", agent.id);
Ok(Json(agent))
} else {
warn!("Unauthorized attempt to access agent with ID: {}", agent.id);
Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
))
}
}
Ok(None) => {
warn!("Agent not found with provided name and cluster_name");
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Agent not found"})),
))
}
Err(e) => {
error!("Failed to fetch agent: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent"})),
))
}
}
} else {
warn!("Invalid request: missing name or cluster_name");
Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid request"})),
))
}
}
}
brokkr-broker::api::v1::agents::update_agent
private
#![allow(unused)]
fn main() {
async fn update_agent (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Json (update_payload) : Json < serde_json :: Value > ,) -> Result < Json < Agent > , (StatusCode , Json < serde_json :: Value >) >
}
Updates an existing agent.
Source
#![allow(unused)]
fn main() {
async fn update_agent(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Json(update_payload): Json<serde_json::Value>,
) -> Result<Json<Agent>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to update agent with ID: {}", id);
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!("Unauthorized attempt to update agent with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
let mut agent = match dal.agents().get(id) {
Ok(Some(a)) => a,
Ok(None) => {
warn!("Agent not found with ID: {}", id);
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Agent not found"})),
));
}
Err(e) => {
error!("Failed to fetch agent with ID {}: {:?}", id, e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent"})),
));
}
};
if let Some(name) = update_payload.get("name").and_then(|v| v.as_str()) {
agent.name = name.to_string();
}
if let Some(cluster_name) = update_payload.get("cluster_name").and_then(|v| v.as_str()) {
agent.cluster_name = cluster_name.to_string();
}
if let Some(status) = update_payload.get("status").and_then(|v| v.as_str()) {
agent.status = status.to_string();
}
match dal.agents().update(id, &agent) {
Ok(updated_agent) => {
info!("Successfully updated agent with ID: {}", id);
// Log audit entry for agent update
audit::log_action(
ACTOR_TYPE_ADMIN,
None,
ACTION_AGENT_UPDATED,
RESOURCE_TYPE_AGENT,
Some(id),
Some(serde_json::json!({
"name": updated_agent.name,
"cluster_name": updated_agent.cluster_name,
"status": updated_agent.status,
})),
None,
None,
);
Ok(Json(updated_agent))
}
Err(e) => {
error!("Failed to update agent with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update agent"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::delete_agent
private
#![allow(unused)]
fn main() {
async fn delete_agent (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Soft deletes an agent.
Source
#![allow(unused)]
fn main() {
async fn delete_agent(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to delete agent with ID: {}", id);
if !auth_payload.admin {
warn!("Unauthorized attempt to delete agent with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
// Capture old PAK hash for cache invalidation before deletion
let old_pak_hash = dal.agents().get(id).ok().flatten().map(|a| a.pak_hash);
match dal.agents().soft_delete(id) {
Ok(_) => {
info!("Successfully deleted agent with ID: {}", id);
// Invalidate old PAK hash from auth cache
if let Some(ref hash) = old_pak_hash {
dal.invalidate_auth_cache(hash);
}
// Log audit entry for agent deletion
audit::log_action(
ACTOR_TYPE_ADMIN,
None,
ACTION_AGENT_DELETED,
RESOURCE_TYPE_AGENT,
Some(id),
None,
None,
None,
);
Ok(StatusCode::NO_CONTENT)
}
Err(e) => {
error!("Failed to delete agent with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to delete agent"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::list_events
private
#![allow(unused)]
fn main() {
async fn list_events (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < Vec < Value > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists events for a specific agent.
Source
#![allow(unused)]
fn main() {
async fn list_events(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<Vec<Value>>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to list events for agent with ID: {}", id);
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!(
"Unauthorized attempt to list events for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agent_events().get_events(None, Some(id)) {
Ok(events) => {
info!(
"Successfully retrieved {} events for agent with ID: {}",
events.len(),
id
);
Ok(Json(
events
.into_iter()
.map(|e| serde_json::to_value(e).unwrap())
.collect(),
))
}
Err(e) => {
error!("Failed to fetch events for agent with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent events"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::create_event
private
#![allow(unused)]
fn main() {
async fn create_event (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Json (new_event) : Json < NewAgentEvent > ,) -> Result < Json < AgentEvent > , (StatusCode , Json < serde_json :: Value >) >
}
Creates a new event for a specific agent.
Source
#![allow(unused)]
fn main() {
async fn create_event(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Json(new_event): Json<NewAgentEvent>,
) -> Result<Json<AgentEvent>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to create event for agent with ID: {}", id);
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!(
"Unauthorized attempt to create event for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agent_events().create(&new_event) {
Ok(event) => {
info!("Successfully created event for agent with ID: {}", id);
// Emit deployment webhook event based on status
let webhook_event_type = if new_event.status.to_uppercase() == "SUCCESS" {
EVENT_DEPLOYMENT_APPLIED
} else {
EVENT_DEPLOYMENT_FAILED
};
let event_data = serde_json::json!({
"agent_event_id": event.id,
"agent_id": event.agent_id,
"deployment_object_id": event.deployment_object_id,
"event_type": event.event_type,
"status": event.status,
"message": event.message,
"created_at": event.created_at,
});
event_bus::emit_event(&dal, &BrokkrEvent::new(webhook_event_type, event_data));
Ok(Json(event))
}
Err(e) => {
error!("Failed to create event for agent with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create agent event"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::list_labels
private
#![allow(unused)]
fn main() {
async fn list_labels (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < Vec < AgentLabel > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists labels for a specific agent.
Source
#![allow(unused)]
fn main() {
async fn list_labels(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<Vec<AgentLabel>>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to list labels for agent with ID: {}", id);
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!(
"Unauthorized attempt to list labels for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agent_labels().list_for_agent(id) {
Ok(labels) => {
info!(
"Successfully retrieved {} labels for agent with ID: {}",
labels.len(),
id
);
Ok(Json(labels))
}
Err(e) => {
error!("Failed to fetch labels for agent with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent labels"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::add_label
private
#![allow(unused)]
fn main() {
async fn add_label (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Json (new_label) : Json < NewAgentLabel > ,) -> Result < Json < AgentLabel > , (StatusCode , Json < serde_json :: Value >) >
}
Adds a new label to a specific agent.
Source
#![allow(unused)]
fn main() {
async fn add_label(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Json(new_label): Json<NewAgentLabel>,
) -> Result<Json<AgentLabel>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to add label for agent with ID: {}", id);
if !auth_payload.admin {
warn!(
"Unauthorized attempt to add label for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agent_labels().create(&new_label) {
Ok(label) => {
info!("Successfully added label for agent with ID: {}", id);
Ok(Json(label))
}
Err(e) => {
error!("Failed to add label for agent with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to add agent label"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::remove_label
private
#![allow(unused)]
fn main() {
async fn remove_label (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path ((id , label)) : Path < (Uuid , String) > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Removes a label from a specific agent.
Source
#![allow(unused)]
fn main() {
async fn remove_label(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path((id, label)): Path<(Uuid, String)>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to remove label '{}' from agent with ID: {}",
label, id
);
if !auth_payload.admin {
warn!(
"Unauthorized attempt to remove label from agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agent_labels().delete_by_agent_and_label(id, &label) {
Ok(deleted_count) => {
if deleted_count > 0 {
info!(
"Successfully removed label '{}' from agent with ID: {}",
label, id
);
Ok(StatusCode::NO_CONTENT)
} else {
warn!("Label '{}' not found for agent with ID: {}", label, id);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Label not found"})),
))
}
}
Err(e) => {
error!(
"Failed to remove label '{}' from agent with ID {}: {:?}",
label, id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to remove agent label"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::list_annotations
private
#![allow(unused)]
fn main() {
async fn list_annotations (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < Vec < AgentAnnotation > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists annotations for a specific agent.
Source
#![allow(unused)]
fn main() {
async fn list_annotations(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<Vec<AgentAnnotation>>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to list annotations for agent with ID: {}",
id
);
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!(
"Unauthorized attempt to list annotations for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agent_annotations().list_for_agent(id) {
Ok(annotations) => {
info!(
"Successfully retrieved {} annotations for agent with ID: {}",
annotations.len(),
id
);
Ok(Json(annotations))
}
Err(e) => {
error!(
"Failed to fetch annotations for agent with ID {}: {:?}",
id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent annotations"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::add_annotation
private
#![allow(unused)]
fn main() {
async fn add_annotation (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Json (new_annotation) : Json < NewAgentAnnotation > ,) -> Result < Json < AgentAnnotation > , (StatusCode , Json < serde_json :: Value >) >
}
Adds a new annotation to a specific agent.
Source
#![allow(unused)]
fn main() {
async fn add_annotation(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Json(new_annotation): Json<NewAgentAnnotation>,
) -> Result<Json<AgentAnnotation>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to add annotation for agent with ID: {}",
id
);
if !auth_payload.admin {
warn!(
"Unauthorized attempt to add annotation for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agent_annotations().create(&new_annotation) {
Ok(annotation) => {
info!("Successfully added annotation for agent with ID: {}", id);
Ok(Json(annotation))
}
Err(e) => {
error!("Failed to add annotation for agent with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to add agent annotation"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::remove_annotation
private
#![allow(unused)]
fn main() {
async fn remove_annotation (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path ((id , key)) : Path < (Uuid , String) > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Removes an annotation from a specific agent.
Source
#![allow(unused)]
fn main() {
async fn remove_annotation(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path((id, key)): Path<(Uuid, String)>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to remove annotation '{}' from agent with ID: {}",
key, id
);
if !auth_payload.admin {
warn!(
"Unauthorized attempt to remove annotation from agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agent_annotations().delete_by_agent_and_key(id, &key) {
Ok(deleted_count) => {
if deleted_count > 0 {
info!(
"Successfully removed annotation '{}' from agent with ID: {}",
key, id
);
Ok(StatusCode::NO_CONTENT)
} else {
warn!("Annotation '{}' not found for agent with ID: {}", key, id);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Annotation not found"})),
))
}
}
Err(e) => {
error!(
"Failed to remove annotation '{}' from agent with ID {}: {:?}",
key, id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to remove agent annotation"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::list_targets
private
#![allow(unused)]
fn main() {
async fn list_targets (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < Vec < AgentTarget > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists targets for a specific agent.
Source
#![allow(unused)]
fn main() {
async fn list_targets(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<Vec<AgentTarget>>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to list targets for agent with ID: {}", id);
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!(
"Unauthorized attempt to list targets for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agent_targets().list_for_agent(id) {
Ok(targets) => {
info!(
"Successfully retrieved {} targets for agent with ID: {}",
targets.len(),
id
);
Ok(Json(targets))
}
Err(e) => {
error!("Failed to fetch targets for agent with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent targets"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::add_target
private
#![allow(unused)]
fn main() {
async fn add_target (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Json (new_target) : Json < NewAgentTarget > ,) -> Result < Json < AgentTarget > , (StatusCode , Json < serde_json :: Value >) >
}
Adds a new target to a specific agent.
Source
#![allow(unused)]
fn main() {
async fn add_target(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Json(new_target): Json<NewAgentTarget>,
) -> Result<Json<AgentTarget>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to add target for agent with ID: {}", id);
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!(
"Unauthorized attempt to add target for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agent_targets().create(&new_target) {
Ok(target) => {
info!("Successfully added target for agent with ID: {}", id);
Ok(Json(target))
}
Err(e) => {
error!("Failed to add target for agent with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to add agent target"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::remove_target
private
#![allow(unused)]
fn main() {
async fn remove_target (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path ((id , stack_id)) : Path < (Uuid , Uuid) > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Removes a target from a specific agent.
Source
#![allow(unused)]
fn main() {
async fn remove_target(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path((id, stack_id)): Path<(Uuid, Uuid)>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to remove target for stack {} from agent with ID: {}",
stack_id, id
);
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!(
"Unauthorized attempt to remove target from agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agent_targets().delete_by_agent_and_stack(id, stack_id) {
Ok(deleted_count) => {
if deleted_count > 0 {
info!(
"Successfully removed target for stack {} from agent with ID: {}",
stack_id, id
);
Ok(StatusCode::NO_CONTENT)
} else {
warn!(
"Target for stack {} not found for agent with ID: {}",
stack_id, id
);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Target not found"})),
))
}
}
Err(e) => {
error!(
"Failed to remove target for stack {} from agent with ID {}: {:?}",
stack_id, id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to remove agent target"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::record_heartbeat
private
#![allow(unused)]
fn main() {
async fn record_heartbeat (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Records a heartbeat for a specific agent.
Source
#![allow(unused)]
fn main() {
async fn record_heartbeat(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to record heartbeat for agent with ID: {}",
id
);
if auth_payload.agent != Some(id) {
warn!(
"Unauthorized attempt to record heartbeat for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.agents().record_heartbeat(id) {
Ok(_) => {
info!("Successfully recorded heartbeat for agent with ID: {}", id);
// Update heartbeat age metric (age is now 0 since we just recorded it)
// Also get agent name for the metric label
if let Ok(Some(agent)) = dal.agents().get(id) {
metrics::set_agent_heartbeat_age(&id.to_string(), &agent.name, 0.0);
}
Ok(StatusCode::NO_CONTENT)
}
Err(e) => {
error!(
"Failed to record heartbeat for agent with ID {}: {:?}",
id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to record agent heartbeat"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::get_target_state
private
#![allow(unused)]
fn main() {
async fn get_target_state (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Query (params) : Query < TargetStateParams > ,) -> Result < Json < Vec < DeploymentObject > > , (StatusCode , Json < serde_json :: Value >) >
}
Retrieves the target state (deployment objects that should be applied) for a specific agent.
Source
#![allow(unused)]
fn main() {
async fn get_target_state(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Query(params): Query<TargetStateParams>,
) -> Result<Json<Vec<DeploymentObject>>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to get target state for agent with ID: {}",
id
);
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!(
"Unauthorized attempt to get target state for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
// Determine if we should include deployed objects based on query parameter
let include_deployed = params.mode.as_deref() == Some("full");
info!(
"Target state request mode is '{}', include_deployed={}",
params.mode.unwrap_or_else(|| "incremental".to_string()),
include_deployed
);
match dal
.deployment_objects()
.get_target_state_for_agent(id, include_deployed)
{
Ok(objects) => {
info!(
"Successfully retrieved {} objects in target state for agent with ID: {}",
objects.len(),
id
);
Ok(Json(objects))
}
Err(e) => {
error!(
"Failed to fetch target state for agent with ID {}: {:?}",
id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch target state"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::get_associated_stacks
private
#![allow(unused)]
fn main() {
async fn get_associated_stacks (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < Vec < Stack > > , (StatusCode , Json < serde_json :: Value >) >
}
Retrieves all stacks associated with a specific agent based on targets, labels, and annotations.
Source
#![allow(unused)]
fn main() {
async fn get_associated_stacks(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<Vec<Stack>>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to get associated stacks for agent with ID: {}",
id
);
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!(
"Unauthorized attempt to get associated stacks for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.stacks().get_associated_stacks(id) {
Ok(stacks) => {
info!(
"Successfully retrieved {} associated stacks for agent with ID: {}",
stacks.len(),
id
);
Ok(Json(stacks))
}
Err(e) => {
error!(
"Failed to fetch associated stacks for agent with ID {}: {:?}",
id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch associated stacks"})),
))
}
}
}
}
brokkr-broker::api::v1::agents::rotate_agent_pak
private
#![allow(unused)]
fn main() {
async fn rotate_agent_pak (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < serde_json :: Value > , (StatusCode , Json < serde_json :: Value >) >
}
Rotates the PAK for a specific agent.
Source
#![allow(unused)]
fn main() {
async fn rotate_agent_pak(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to rotate PAK for agent with ID: {}", id);
// Check authorization - must be admin or the agent itself
if !auth_payload.admin && auth_payload.agent != Some(id) {
warn!(
"Unauthorized attempt to rotate PAK for agent with ID: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized access"})),
));
}
// Verify agent exists and capture old PAK hash for cache invalidation
let old_pak_hash = match dal.agents().get(id) {
Ok(Some(agent)) => agent.pak_hash,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Agent not found"})),
));
}
Err(e) => {
error!("Failed to fetch agent with ID {}: {:?}", id, e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent"})),
));
}
};
// Generate new PAK and hash
let (pak, pak_hash) = match crate::utils::pak::create_pak() {
Ok((pak, hash)) => (pak, hash),
Err(e) => {
error!("Failed to create new PAK: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create new PAK"})),
));
}
};
// Update agent's PAK hash
match dal.agents().update_pak_hash(id, pak_hash) {
Ok(updated_agent) => {
info!("Successfully rotated PAK for agent with ID: {}", id);
// Invalidate old PAK hash from auth cache
dal.invalidate_auth_cache(&old_pak_hash);
// Log audit entry for PAK rotation
audit::log_action(
ACTOR_TYPE_ADMIN,
None,
ACTION_PAK_ROTATED,
RESOURCE_TYPE_AGENT,
Some(id),
Some(serde_json::json!({
"agent_name": updated_agent.name,
})),
None,
None,
);
Ok(Json(serde_json::json!({
"agent": updated_agent,
"pak": pak
})))
}
Err(e) => {
error!("Failed to update agent PAK hash: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update agent PAK hash"})),
))
}
}
}
}
brokkr-broker::api::v1::auth Rust
Authentication module for the Brokkr API v1.
This module provides routes and handlers for authentication-related endpoints.
Functions
brokkr-broker::api::v1::auth::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Creates and returns the authentication routes for the API.
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
Router::new().route("/auth/pak", post(verify_pak))
}
}
brokkr-broker::api::v1::auth::verify_pak
private
#![allow(unused)]
fn main() {
async fn verify_pak (Extension (auth_payload) : Extension < AuthPayload >) -> Json < AuthResponse >
}
Verifies a PAK (Personal Access Key) and returns an AuthResponse.
This function handles the authentication process for both admin and agent PAKs.
Source
#![allow(unused)]
fn main() {
async fn verify_pak(Extension(auth_payload): Extension<AuthPayload>) -> Json<AuthResponse> {
Json(AuthResponse {
admin: auth_payload.admin,
agent: auth_payload.agent.map(|id| id.to_string()),
generator: auth_payload.generator.map(|id| id.to_string()),
})
}
}
brokkr-broker::api::v1::deployment_objects Rust
Deployment Objects API module for Brokkr.
This module provides routes and handlers for managing deployment objects, including retrieval based on user authentication and authorization.
Functions
brokkr-broker::api::v1::deployment_objects::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Creates and returns the router for deployment object endpoints.
Returns:
A Router instance configured with the deployment object routes.
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
info!("Setting up deployment object routes");
Router::new().route("/deployment-objects/:id", get(get_deployment_object))
}
}
brokkr-broker::api::v1::deployment_objects::get_deployment_object
private
#![allow(unused)]
fn main() {
async fn get_deployment_object (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < DeploymentObject > , (axum :: http :: StatusCode , Json < serde_json :: Value >) >
}
Retrieves a deployment object by ID, with access control based on user role.
Source
#![allow(unused)]
fn main() {
async fn get_deployment_object(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<DeploymentObject>, (axum::http::StatusCode, Json<serde_json::Value>)> {
info!("Handling request to get deployment object with ID: {}", id);
match dal.deployment_objects().get(id) {
Ok(Some(object)) => {
if auth_payload.admin {
info!("Admin user accessed deployment object with ID: {}", id);
Ok(Json(object))
} else if let Some(agent_id) = auth_payload.agent {
match dal.agent_targets().list_for_agent(agent_id) {
Ok(targets) => {
if targets
.iter()
.any(|target| target.stack_id == object.stack_id)
{
info!(
"Agent {} accessed deployment object with ID: {}",
agent_id, id
);
Ok(Json(object))
} else {
warn!(
"Agent {} attempted to access unauthorized deployment object with ID: {}",
agent_id, id
);
Err((
axum::http::StatusCode::FORBIDDEN,
Json(
serde_json::json!({"error": "Agent is not associated with this deployment object"}),
),
))
}
}
Err(e) => {
error!(
"Failed to fetch agent targets for agent {}: {:?}",
agent_id, e
);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent targets"})),
))
}
}
} else if let Some(generator_id) = auth_payload.generator {
// Check if the generator is associated with the stack of this deployment object
match dal.stacks().get(vec![object.stack_id]) {
Ok(stacks) => {
if let Some(stack) = stacks.first() {
if stack.generator_id == generator_id {
info!(
"Generator '{}' (id: {}) accessed deployment object '{}' (id: {})",
stack.name, generator_id, object.yaml_content, id
);
Ok(Json(object))
} else {
warn!(
"Generator '{}' (id: {}) attempted unauthorized access to deployment object '{}' (id: {}) owned by generator {}",
stack.name,
generator_id,
object.yaml_content,
id,
stack.generator_id
);
Err((
axum::http::StatusCode::FORBIDDEN,
Json(
serde_json::json!({"error": "Generator is not associated with this deployment object"}),
),
))
}
} else {
warn!(
"Stack not found for deployment object '{}' (id: {}, stack_id: {})",
object.yaml_content, id, object.stack_id
);
Err((
axum::http::StatusCode::NOT_FOUND,
Json(
serde_json::json!({"error": "Stack not found for this deployment object"}),
),
))
}
}
Err(e) => {
error!(
"Database error while fetching stack for deployment object '{}' (id: {}, stack_id: {}): {:?}",
object.yaml_content, id, object.stack_id, e
);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack information"})),
))
}
}
} else {
warn!(
"Unauthorized access attempt to deployment object with ID: {}",
id
);
Err((
axum::http::StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"error": "Unauthorized access"})),
))
}
}
Ok(None) => {
warn!("Deployment object not found with ID: {}", id);
Err((
axum::http::StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Deployment object not found"})),
))
}
Err(e) => {
error!("Failed to fetch deployment object with ID {}: {:?}", id, e);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch deployment object"})),
))
}
}
}
}
brokkr-broker::api::v1::diagnostics Rust
Diagnostics API endpoints.
This module provides routes and handlers for on-demand diagnostic requests. Operators can request diagnostics for specific deployment objects, and agents pick up and execute these requests, returning detailed diagnostic data.
Structs
brokkr-broker::api::v1::diagnostics::CreateDiagnosticRequest
pub
Derives: Debug, Clone, Serialize, Deserialize, utoipa :: ToSchema
Request body for creating a diagnostic request.
Fields
| Name | Type | Description |
|---|---|---|
agent_id | Uuid | The agent that should handle this request. |
requested_by | Option < String > | Who is requesting the diagnostics (optional). |
retention_minutes | Option < i64 > | How long the request should be retained in minutes (default 60, max 1440). |
brokkr-broker::api::v1::diagnostics::DiagnosticResponse
pub
Derives: Debug, Clone, Serialize, Deserialize, utoipa :: ToSchema
Response containing a diagnostic request with optional result.
Fields
| Name | Type | Description |
|---|---|---|
request | DiagnosticRequest | The diagnostic request. |
result | Option < DiagnosticResult > | The diagnostic result, if completed. |
brokkr-broker::api::v1::diagnostics::SubmitDiagnosticResult
pub
Derives: Debug, Clone, Serialize, Deserialize, utoipa :: ToSchema
Request body for submitting diagnostic results.
Fields
| Name | Type | Description |
|---|---|---|
pod_statuses | String | JSON-encoded pod statuses. |
events | String | JSON-encoded Kubernetes events. |
log_tails | Option < String > | JSON-encoded log tails (optional). |
collected_at | DateTime < Utc > | When the diagnostics were collected. |
Functions
brokkr-broker::api::v1::diagnostics::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Creates and returns the router for diagnostic endpoints.
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
info!("Setting up diagnostic routes");
Router::new()
.route(
"/deployment-objects/:id/diagnostics",
post(create_diagnostic_request),
)
.route("/diagnostics/:id", get(get_diagnostic))
.route(
"/agents/:id/diagnostics/pending",
get(get_pending_diagnostics),
)
.route("/diagnostics/:id/claim", post(claim_diagnostic))
.route("/diagnostics/:id/result", post(submit_diagnostic_result))
}
}
brokkr-broker::api::v1::diagnostics::create_diagnostic_request
private
#![allow(unused)]
fn main() {
async fn create_diagnostic_request (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (deployment_object_id) : Path < Uuid > , Json (request) : Json < CreateDiagnosticRequest > ,) -> Result < (StatusCode , Json < DiagnosticRequest >) , (StatusCode , Json < serde_json :: Value >) >
}
Creates a diagnostic request for a deployment object.
Source
#![allow(unused)]
fn main() {
async fn create_diagnostic_request(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(deployment_object_id): Path<Uuid>,
Json(request): Json<CreateDiagnosticRequest>,
) -> Result<(StatusCode, Json<DiagnosticRequest>), (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to create diagnostic for deployment object {}",
deployment_object_id
);
if !auth_payload.admin {
warn!("Unauthorized attempt to create diagnostic request");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
// Verify the deployment object exists
match dal.deployment_objects().get(deployment_object_id) {
Ok(Some(_)) => {}
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Deployment object not found"})),
))
}
Err(e) => {
error!(
"Failed to fetch deployment object {}: {:?}",
deployment_object_id, e
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to verify deployment object"})),
));
}
}
// Verify the agent exists and is associated with the deployment object's stack
match dal.agents().get(request.agent_id) {
Ok(Some(_)) => {}
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Agent not found"})),
))
}
Err(e) => {
error!("Failed to fetch agent {}: {:?}", request.agent_id, e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to verify agent"})),
));
}
}
// Create the diagnostic request
match NewDiagnosticRequest::new(
request.agent_id,
deployment_object_id,
request.requested_by,
request.retention_minutes,
) {
Ok(new_request) => match dal.diagnostic_requests().create(&new_request) {
Ok(diagnostic_request) => {
info!(
"Created diagnostic request {} for deployment object {} assigned to agent {}",
diagnostic_request.id, deployment_object_id, request.agent_id
);
Ok((StatusCode::CREATED, Json(diagnostic_request)))
}
Err(e) => {
error!("Failed to create diagnostic request: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create diagnostic request"})),
))
}
},
Err(e) => {
warn!("Invalid diagnostic request parameters: {}", e);
Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e})),
))
}
}
}
}
brokkr-broker::api::v1::diagnostics::get_diagnostic
private
#![allow(unused)]
fn main() {
async fn get_diagnostic (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < DiagnosticResponse > , (StatusCode , Json < serde_json :: Value >) >
}
Gets a diagnostic request with its result if available.
Source
#![allow(unused)]
fn main() {
async fn get_diagnostic(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<DiagnosticResponse>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to get diagnostic {}", id);
match dal.diagnostic_requests().get(id) {
Ok(Some(request)) => {
// Check authorization
if !auth_payload.admin && auth_payload.agent != Some(request.agent_id) {
warn!("Unauthorized attempt to get diagnostic request {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
// Get the result if available
let result = match dal.diagnostic_results().get_by_request(id) {
Ok(result) => result,
Err(e) => {
error!(
"Failed to fetch diagnostic result for request {}: {:?}",
id, e
);
None
}
};
Ok(Json(DiagnosticResponse { request, result }))
}
Ok(None) => {
warn!("Diagnostic request not found: {}", id);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Diagnostic request not found"})),
))
}
Err(e) => {
error!("Failed to fetch diagnostic request {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch diagnostic request"})),
))
}
}
}
}
brokkr-broker::api::v1::diagnostics::get_pending_diagnostics
private
#![allow(unused)]
fn main() {
async fn get_pending_diagnostics (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (agent_id) : Path < Uuid > ,) -> Result < Json < Vec < DiagnosticRequest > > , (StatusCode , Json < serde_json :: Value >) >
}
Gets pending diagnostic requests for an agent.
Source
#![allow(unused)]
fn main() {
async fn get_pending_diagnostics(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(agent_id): Path<Uuid>,
) -> Result<Json<Vec<DiagnosticRequest>>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to get pending diagnostics for agent {}",
agent_id
);
if auth_payload.agent != Some(agent_id) && !auth_payload.admin {
warn!(
"Unauthorized attempt to get pending diagnostics for agent {}",
agent_id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.diagnostic_requests().get_pending_for_agent(agent_id) {
Ok(requests) => {
info!(
"Found {} pending diagnostic requests for agent {}",
requests.len(),
agent_id
);
Ok(Json(requests))
}
Err(e) => {
error!(
"Failed to fetch pending diagnostics for agent {}: {:?}",
agent_id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch pending diagnostics"})),
))
}
}
}
}
brokkr-broker::api::v1::diagnostics::claim_diagnostic
private
#![allow(unused)]
fn main() {
async fn claim_diagnostic (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < DiagnosticRequest > , (StatusCode , Json < serde_json :: Value >) >
}
Claims a diagnostic request for processing.
Source
#![allow(unused)]
fn main() {
async fn claim_diagnostic(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<DiagnosticRequest>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to claim diagnostic {}", id);
// First get the request to check authorization and status
match dal.diagnostic_requests().get(id) {
Ok(Some(request)) => {
// Verify the agent is authorized
if auth_payload.agent != Some(request.agent_id) && !auth_payload.admin {
warn!(
"Unauthorized attempt to claim diagnostic {} by {:?}",
id, auth_payload
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
// Verify the request is in pending status
if request.status != "pending" {
warn!(
"Attempt to claim diagnostic {} with status {}",
id, request.status
);
return Err((
StatusCode::CONFLICT,
Json(serde_json::json!({
"error": format!("Request is already {}", request.status)
})),
));
}
// Claim the request
match dal.diagnostic_requests().claim(id) {
Ok(claimed) => {
info!(
"Agent {} claimed diagnostic request {}",
request.agent_id, id
);
Ok(Json(claimed))
}
Err(e) => {
error!("Failed to claim diagnostic request {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to claim diagnostic request"})),
))
}
}
}
Ok(None) => {
warn!("Diagnostic request not found for claim: {}", id);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Diagnostic request not found"})),
))
}
Err(e) => {
error!(
"Failed to fetch diagnostic request {} for claim: {:?}",
id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch diagnostic request"})),
))
}
}
}
}
brokkr-broker::api::v1::diagnostics::submit_diagnostic_result
private
#![allow(unused)]
fn main() {
async fn submit_diagnostic_result (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (request_id) : Path < Uuid > , Json (result) : Json < SubmitDiagnosticResult > ,) -> Result < (StatusCode , Json < DiagnosticResult >) , (StatusCode , Json < serde_json :: Value >) >
}
Submits diagnostic results for a request.
Source
#![allow(unused)]
fn main() {
async fn submit_diagnostic_result(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(request_id): Path<Uuid>,
Json(result): Json<SubmitDiagnosticResult>,
) -> Result<(StatusCode, Json<DiagnosticResult>), (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling submission of diagnostic result for request {}",
request_id
);
// Get the request to verify authorization
match dal.diagnostic_requests().get(request_id) {
Ok(Some(request)) => {
// Verify the agent is authorized
if auth_payload.agent != Some(request.agent_id) {
warn!(
"Unauthorized attempt to submit result for diagnostic {} by {:?}",
request_id, auth_payload
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
// Verify the request is in claimed status
if request.status != "claimed" {
warn!(
"Attempt to submit result for diagnostic {} with status {}",
request_id, request.status
);
return Err((
StatusCode::CONFLICT,
Json(serde_json::json!({
"error": format!("Request status is {}, expected 'claimed'", request.status)
})),
));
}
// Create the result
match NewDiagnosticResult::new(
request_id,
result.pod_statuses,
result.events,
result.log_tails,
result.collected_at,
) {
Ok(new_result) => {
// Insert result and mark request as completed
match dal.diagnostic_results().create(&new_result) {
Ok(diagnostic_result) => {
// Mark the request as completed
if let Err(e) = dal.diagnostic_requests().complete(request_id) {
error!(
"Failed to mark diagnostic request {} as completed: {:?}",
request_id, e
);
}
info!(
"Agent {} submitted diagnostic result for request {}",
request.agent_id, request_id
);
Ok((StatusCode::CREATED, Json(diagnostic_result)))
}
Err(e) => {
error!("Failed to create diagnostic result: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(
serde_json::json!({"error": "Failed to store diagnostic result"}),
),
))
}
}
}
Err(e) => {
warn!("Invalid diagnostic result: {}", e);
Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e})),
))
}
}
}
Ok(None) => {
warn!(
"Diagnostic request not found for result submission: {}",
request_id
);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Diagnostic request not found"})),
))
}
Err(e) => {
error!(
"Failed to fetch diagnostic request {} for result submission: {:?}",
request_id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch diagnostic request"})),
))
}
}
}
}
brokkr-broker::api::v1::generators Rust
Generators API module for Brokkr.
This module provides routes and handlers for managing generators, including CRUD operations with appropriate access control.
Structs
brokkr-broker::api::v1::generators::CreateGeneratorResponse
pub
Derives: Serialize, ToSchema
Response for a successful generator creation
Fields
| Name | Type | Description |
|---|---|---|
generator | Generator | The created generator |
pak | String | The Pre-Authentication Key for the generator |
Functions
brokkr-broker::api::v1::generators::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Creates and returns the router for generator endpoints.
Returns:
A Router instance configured with the generator routes.
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
info!("Setting up generator routes");
Router::new()
.route("/generators", get(list_generators))
.route("/generators", post(create_generator))
.route("/generators/:id", get(get_generator))
.route("/generators/:id", put(update_generator))
.route("/generators/:id", delete(delete_generator))
.route("/generators/:id/rotate-pak", post(rotate_generator_pak))
}
}
brokkr-broker::api::v1::generators::list_generators
private
#![allow(unused)]
fn main() {
async fn list_generators (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > ,) -> Result < Json < Vec < Generator > > , (axum :: http :: StatusCode , Json < serde_json :: Value >) >
}
Lists all generators. Requires admin access.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The data access layer for database operations. |
auth_payload | - | The authentication payload containing user role information. |
Returns:
A Result containing either a list of Generators as JSON or an error response.
Source
#![allow(unused)]
fn main() {
async fn list_generators(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
) -> Result<Json<Vec<Generator>>, (axum::http::StatusCode, Json<serde_json::Value>)> {
info!("Handling request to list generators");
if !auth_payload.admin {
warn!("Unauthorized attempt to list generators");
return Err((
axum::http::StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
match dal.generators().list() {
Ok(generators) => {
info!("Successfully retrieved {} generators", generators.len());
Ok(Json(generators))
}
Err(e) => {
error!("Failed to fetch generators: {:?}", e);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch generators"})),
))
}
}
}
}
brokkr-broker::api::v1::generators::create_generator
private
#![allow(unused)]
fn main() {
async fn create_generator (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Json (new_generator) : Json < NewGenerator > ,) -> Result < Json < serde_json :: Value > , (axum :: http :: StatusCode , Json < serde_json :: Value >) >
}
Creates a new generator. Requires admin access.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The data access layer for database operations. |
auth_payload | - | The authentication payload containing user role information. |
new_generator | - | The data for the new generator to be created. |
Returns:
A Result containing either the created Generator and its PAK as JSON or an error response.
Source
#![allow(unused)]
fn main() {
async fn create_generator(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Json(new_generator): Json<NewGenerator>,
) -> Result<Json<serde_json::Value>, (axum::http::StatusCode, Json<serde_json::Value>)> {
info!("Handling request to create a new generator");
if !auth_payload.admin {
warn!("Unauthorized attempt to create a generator");
return Err((
axum::http::StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
let (pak, pak_hash) = pak::create_pak().map_err(|e| {
error!("Failed to create PAK: {:?}", e);
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create PAK"})),
)
})?;
match dal.generators().create(&new_generator) {
Ok(generator) => match dal.generators().update_pak_hash(generator.id, pak_hash) {
Ok(updated_generator) => {
info!(
"Successfully created generator with ID: {}",
updated_generator.id
);
Ok(Json(serde_json::json!({
"generator": updated_generator,
"pak": pak
})))
}
Err(e) => {
error!("Failed to update generator PAK hash: {:?}", e);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update generator PAK hash"})),
))
}
},
Err(e) => {
error!("Failed to create generator: {:?}", e);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create generator"})),
))
}
}
}
}
brokkr-broker::api::v1::generators::get_generator
private
#![allow(unused)]
fn main() {
async fn get_generator (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < Generator > , (axum :: http :: StatusCode , Json < serde_json :: Value >) >
}
Retrieves a specific generator by ID.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The data access layer for database operations. |
auth_payload | - | The authentication payload containing user role information. |
id | - | The UUID of the generator to retrieve. |
Returns:
A Result containing either the Generator as JSON or an error response.
Source
#![allow(unused)]
fn main() {
async fn get_generator(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<Generator>, (axum::http::StatusCode, Json<serde_json::Value>)> {
info!("Handling request to get generator with ID: {}", id);
if !auth_payload.admin && auth_payload.generator != Some(id) {
warn!("Unauthorized attempt to access generator with ID: {}", id);
return Err((
axum::http::StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized access"})),
));
}
match dal.generators().get(id) {
Ok(Some(generator)) => {
info!("Successfully retrieved generator with ID: {}", id);
Ok(Json(generator))
}
Ok(None) => {
warn!("Generator not found with ID: {}", id);
Err((
axum::http::StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Generator not found"})),
))
}
Err(e) => {
error!("Failed to fetch generator with ID {}: {:?}", id, e);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch generator"})),
))
}
}
}
}
brokkr-broker::api::v1::generators::update_generator
private
#![allow(unused)]
fn main() {
async fn update_generator (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Json (updated_generator) : Json < Generator > ,) -> Result < Json < Generator > , (axum :: http :: StatusCode , Json < serde_json :: Value >) >
}
Updates an existing generator.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The data access layer for database operations. |
auth_payload | - | The authentication payload containing user role information. |
id | - | The UUID of the generator to update. |
updated_generator | - | The updated generator data. |
Returns:
A Result containing either the updated Generator as JSON or an error response.
Source
#![allow(unused)]
fn main() {
async fn update_generator(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Json(updated_generator): Json<Generator>,
) -> Result<Json<Generator>, (axum::http::StatusCode, Json<serde_json::Value>)> {
info!("Handling request to update generator with ID: {}", id);
if !auth_payload.admin && auth_payload.generator != Some(id) {
warn!("Unauthorized attempt to update generator with ID: {}", id);
return Err((
axum::http::StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized access"})),
));
}
match dal.generators().update(id, &updated_generator) {
Ok(generator) => {
info!("Successfully updated generator with ID: {}", id);
Ok(Json(generator))
}
Err(e) => {
error!("Failed to update generator with ID {}: {:?}", id, e);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update generator"})),
))
}
}
}
}
brokkr-broker::api::v1::generators::delete_generator
private
#![allow(unused)]
fn main() {
async fn delete_generator (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < StatusCode , (axum :: http :: StatusCode , Json < serde_json :: Value >) >
}
Deletes a generator.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The data access layer for database operations. |
auth_payload | - | The authentication payload containing user role information. |
id | - | The UUID of the generator to delete. |
Returns:
A Result containing either a success status code or an error response.
Source
#![allow(unused)]
fn main() {
async fn delete_generator(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, (axum::http::StatusCode, Json<serde_json::Value>)> {
info!("Handling request to delete generator with ID: {}", id);
if !auth_payload.admin && auth_payload.generator != Some(id) {
warn!("Unauthorized attempt to delete generator with ID: {}", id);
return Err((
axum::http::StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized access"})),
));
}
// Capture old PAK hash for cache invalidation before deletion
let old_pak_hash = dal
.generators()
.get(id)
.ok()
.flatten()
.and_then(|g| g.pak_hash);
match dal.generators().soft_delete(id) {
Ok(_) => {
info!("Successfully deleted generator with ID: {}", id);
// Invalidate old PAK hash from auth cache
if let Some(ref hash) = old_pak_hash {
dal.invalidate_auth_cache(hash);
}
Ok(StatusCode::NO_CONTENT)
}
Err(e) => {
error!("Failed to delete generator with ID {}: {:?}", id, e);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to delete generator"})),
))
}
}
}
}
brokkr-broker::api::v1::generators::rotate_generator_pak
private
#![allow(unused)]
fn main() {
async fn rotate_generator_pak (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < serde_json :: Value > , (axum :: http :: StatusCode , Json < serde_json :: Value >) >
}
Rotates the PAK for a specific generator.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The data access layer for database operations. |
auth_payload | - | The authentication payload containing user role information. |
id | - | The UUID of the generator to rotate PAK for. |
Returns:
A Result containing either the updated Generator and its new PAK as JSON or an error response.
Source
#![allow(unused)]
fn main() {
async fn rotate_generator_pak(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, (axum::http::StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to rotate PAK for generator with ID: {}",
id
);
// Check authorization - must be admin or the generator itself
if !auth_payload.admin && auth_payload.generator != Some(id) {
warn!(
"Unauthorized attempt to rotate PAK for generator with ID: {}",
id
);
return Err((
axum::http::StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized access"})),
));
}
// Verify generator exists and capture old PAK hash for cache invalidation
let old_pak_hash: Option<String> = match dal.generators().get(id) {
Ok(Some(generator)) => generator.pak_hash,
Ok(None) => {
return Err((
axum::http::StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Generator not found"})),
));
}
Err(e) => {
error!("Failed to fetch generator with ID {}: {:?}", id, e);
return Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch generator"})),
));
}
};
// Generate new PAK and hash
let (pak, pak_hash) = match pak::create_pak() {
Ok((pak, hash)) => (pak, hash),
Err(e) => {
error!("Failed to create new PAK: {:?}", e);
return Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create new PAK"})),
));
}
};
// Update generator's PAK hash
match dal.generators().update_pak_hash(id, pak_hash) {
Ok(updated_generator) => {
info!("Successfully rotated PAK for generator with ID: {}", id);
// Invalidate old PAK hash from auth cache
if let Some(ref hash) = old_pak_hash {
dal.invalidate_auth_cache(hash);
}
Ok(Json(serde_json::json!({
"generator": updated_generator,
"pak": pak
})))
}
Err(e) => {
error!("Failed to update generator PAK hash: {:?}", e);
Err((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update generator PAK hash"})),
))
}
}
}
}
brokkr-broker::api::v1::health Rust
Deployment health API endpoints.
This module provides routes and handlers for managing deployment health status, including endpoints for agents to report health and for operators to query health.
Structs
brokkr-broker::api::v1::health::HealthStatusUpdate
pub
Derives: Debug, Clone, Serialize, Deserialize, utoipa :: ToSchema
Request body for updating health status from an agent.
Fields
| Name | Type | Description |
|---|---|---|
deployment_objects | Vec < DeploymentObjectHealthUpdate > | List of deployment object health updates. |
brokkr-broker::api::v1::health::DeploymentObjectHealthUpdate
pub
Derives: Debug, Clone, Serialize, Deserialize, utoipa :: ToSchema
Health update for a single deployment object.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | The deployment object ID. |
status | String | Health status: healthy, degraded, failing, or unknown. |
summary | Option < HealthSummary > | Structured health summary. |
checked_at | DateTime < Utc > | When the health was checked. |
brokkr-broker::api::v1::health::DeploymentHealthResponse
pub
Derives: Debug, Clone, Serialize, Deserialize, utoipa :: ToSchema
Response for deployment object health query.
Fields
| Name | Type | Description |
|---|---|---|
deployment_object_id | Uuid | The deployment object ID. |
health_records | Vec < DeploymentHealth > | List of health records from different agents. |
overall_status | String | Overall status (worst status across all agents). |
brokkr-broker::api::v1::health::StackHealthResponse
pub
Derives: Debug, Clone, Serialize, Deserialize, utoipa :: ToSchema
Response for stack health query.
Fields
| Name | Type | Description |
|---|---|---|
stack_id | Uuid | The stack ID. |
overall_status | String | Overall status for the stack. |
deployment_objects | Vec < DeploymentObjectHealthSummary > | Health per deployment object. |
brokkr-broker::api::v1::health::DeploymentObjectHealthSummary
pub
Derives: Debug, Clone, Serialize, Deserialize, utoipa :: ToSchema
Summary of health for a deployment object within a stack.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | The deployment object ID. |
status | String | Overall status for this deployment object. |
healthy_agents | usize | Number of agents reporting healthy. |
degraded_agents | usize | Number of agents reporting degraded. |
failing_agents | usize | Number of agents reporting failing. |
Functions
brokkr-broker::api::v1::health::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Creates and returns the router for health-related endpoints.
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
info!("Setting up health routes");
Router::new()
.route("/agents/:id/health-status", patch(update_health_status))
.route("/deployment-objects/:id/health", get(get_deployment_health))
.route("/stacks/:id/health", get(get_stack_health))
}
}
brokkr-broker::api::v1::health::update_health_status
private
#![allow(unused)]
fn main() {
async fn update_health_status (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (agent_id) : Path < Uuid > , Json (update) : Json < HealthStatusUpdate > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Updates health status for deployment objects from an agent.
Source
#![allow(unused)]
fn main() {
async fn update_health_status(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(agent_id): Path<Uuid>,
Json(update): Json<HealthStatusUpdate>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling health status update from agent {} for {} deployment objects",
agent_id,
update.deployment_objects.len()
);
// Verify the agent is authorized to report for this agent ID
if auth_payload.agent != Some(agent_id) && !auth_payload.admin {
warn!(
"Unauthorized attempt to update health status for agent {}",
agent_id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
// Convert updates to NewDeploymentHealth records
let health_records: Vec<NewDeploymentHealth> = update
.deployment_objects
.into_iter()
.filter_map(|update| {
let summary_json = update.summary.and_then(|s| serde_json::to_string(&s).ok());
NewDeploymentHealth::new(
agent_id,
update.id,
update.status,
summary_json,
update.checked_at,
)
.ok()
})
.collect();
if health_records.is_empty() {
return Ok(StatusCode::OK);
}
match dal.deployment_health().upsert_batch(&health_records) {
Ok(count) => {
info!(
"Successfully updated {} health records for agent {}",
count, agent_id
);
Ok(StatusCode::OK)
}
Err(e) => {
error!(
"Failed to update health status for agent {}: {:?}",
agent_id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update health status"})),
))
}
}
}
}
brokkr-broker::api::v1::health::get_deployment_health
private
#![allow(unused)]
fn main() {
async fn get_deployment_health (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (deployment_object_id) : Path < Uuid > ,) -> Result < Json < DeploymentHealthResponse > , (StatusCode , Json < serde_json :: Value >) >
}
Gets health status for a specific deployment object.
Source
#![allow(unused)]
fn main() {
async fn get_deployment_health(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(deployment_object_id): Path<Uuid>,
) -> Result<Json<DeploymentHealthResponse>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to get health for deployment object {}",
deployment_object_id
);
if !auth_payload.admin {
warn!("Unauthorized attempt to get deployment health");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal
.deployment_health()
.list_by_deployment_object(deployment_object_id)
{
Ok(health_records) => {
let overall_status = compute_overall_status(&health_records);
Ok(Json(DeploymentHealthResponse {
deployment_object_id,
health_records,
overall_status,
}))
}
Err(e) => {
error!(
"Failed to get health for deployment object {}: {:?}",
deployment_object_id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to get deployment health"})),
))
}
}
}
}
brokkr-broker::api::v1::health::get_stack_health
private
#![allow(unused)]
fn main() {
async fn get_stack_health (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (stack_id) : Path < Uuid > ,) -> Result < Json < StackHealthResponse > , (StatusCode , Json < serde_json :: Value >) >
}
Gets health status for all deployment objects in a stack.
Source
#![allow(unused)]
fn main() {
async fn get_stack_health(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(stack_id): Path<Uuid>,
) -> Result<Json<StackHealthResponse>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to get health for stack {}", stack_id);
if !auth_payload.admin {
warn!("Unauthorized attempt to get stack health");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized"})),
));
}
match dal.deployment_health().list_by_stack(stack_id) {
Ok(health_records) => {
// Group health records by deployment object
let mut deployment_health_map: std::collections::HashMap<Uuid, Vec<&DeploymentHealth>> =
std::collections::HashMap::new();
for record in &health_records {
deployment_health_map
.entry(record.deployment_object_id)
.or_default()
.push(record);
}
// Compute summary per deployment object
let deployment_objects: Vec<DeploymentObjectHealthSummary> = deployment_health_map
.into_iter()
.map(|(id, records)| {
let healthy_agents = records.iter().filter(|r| r.status == "healthy").count();
let degraded_agents = records.iter().filter(|r| r.status == "degraded").count();
let failing_agents = records.iter().filter(|r| r.status == "failing").count();
let status = if failing_agents > 0 {
"failing".to_string()
} else if degraded_agents > 0 {
"degraded".to_string()
} else if healthy_agents > 0 {
"healthy".to_string()
} else {
"unknown".to_string()
};
DeploymentObjectHealthSummary {
id,
status,
healthy_agents,
degraded_agents,
failing_agents,
}
})
.collect();
// Compute overall stack status
let overall_status = if deployment_objects.iter().any(|d| d.status == "failing") {
"failing".to_string()
} else if deployment_objects.iter().any(|d| d.status == "degraded") {
"degraded".to_string()
} else if deployment_objects.iter().any(|d| d.status == "healthy") {
"healthy".to_string()
} else {
"unknown".to_string()
};
Ok(Json(StackHealthResponse {
stack_id,
overall_status,
deployment_objects,
}))
}
Err(e) => {
error!("Failed to get health for stack {}: {:?}", stack_id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to get stack health"})),
))
}
}
}
}
brokkr-broker::api::v1::health::compute_overall_status
private
#![allow(unused)]
fn main() {
fn compute_overall_status (records : & [DeploymentHealth]) -> String
}
Computes the overall status from a list of health records. Returns the worst status: failing > degraded > healthy > unknown.
Source
#![allow(unused)]
fn main() {
fn compute_overall_status(records: &[DeploymentHealth]) -> String {
if records.iter().any(|r| r.status == "failing") {
"failing".to_string()
} else if records.iter().any(|r| r.status == "degraded") {
"degraded".to_string()
} else if records.iter().any(|r| r.status == "healthy") {
"healthy".to_string()
} else {
"unknown".to_string()
}
}
}
brokkr-broker::api::v1::middleware Rust
Authentication middleware for the Brokkr API v1.
This module provides middleware for authenticating requests using Pre-Authentication Keys (PAKs) and handling different types of authenticated entities (admin, agent, generator).
Structs
brokkr-broker::api::v1::middleware::AuthPayload
pub
Derives: Clone, Debug
Represents the authenticated entity’s payload.
Fields
| Name | Type | Description |
|---|---|---|
admin | bool | Indicates if the authenticated entity is an admin. |
agent | Option < Uuid > | The UUID of the authenticated agent, if applicable. |
generator | Option < Uuid > | The UUID of the authenticated generator, if applicable. |
brokkr-broker::api::v1::middleware::AuthResponse
pub
Derives: Serialize, ToSchema
Represents the response structure for authentication information.
Fields
| Name | Type | Description |
|---|---|---|
admin | bool | Indicates if the authenticated entity is an admin. |
agent | Option < String > | The string representation of the agent’s UUID, if applicable. |
generator | Option < String > | The string representation of the generator’s UUID, if applicable. |
Functions
brokkr-broker::api::v1::middleware::auth_middleware
pub
#![allow(unused)]
fn main() {
async fn auth_middleware < B > (State (dal) : State < DAL > , mut request : Request < Body > , next : Next ,) -> Result < Response , StatusCode >
}
Middleware function for authenticating requests.
This function extracts the PAK from the Authorization header, verifies it,
and adds the resulting AuthPayload to the request’s extensions.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The data access layer for database operations. |
request | - | The incoming HTTP request. |
next | - | The next middleware in the chain. |
Returns:
A Result containing either the response from the next middleware or an error status code.
Source
#![allow(unused)]
fn main() {
pub async fn auth_middleware<B>(
State(dal): State<DAL>,
mut request: Request<Body>,
next: Next,
) -> Result<Response, StatusCode> {
info!("Processing authentication middleware");
let pak = match request
.headers()
.get("Authorization")
.and_then(|header| header.to_str().ok())
{
Some(pak) => pak,
None => {
warn!("Authorization header missing or invalid");
return Err(StatusCode::UNAUTHORIZED);
}
};
match verify_pak(&dal, pak).await {
Ok(auth_payload) => {
info!("Authentication successful");
request.extensions_mut().insert(auth_payload);
Ok(next.run(request).await)
}
Err(status) => {
warn!("Authentication failed with status: {:?}", status);
Err(status)
}
}
}
}
brokkr-broker::api::v1::middleware::verify_pak
private
#![allow(unused)]
fn main() {
async fn verify_pak (dal : & DAL , pak : & str) -> Result < AuthPayload , StatusCode >
}
Verifies the provided PAK and returns the corresponding AuthPayload.
This function checks the PAK against agents, generators, and admin roles using indexed lookups for O(1) performance instead of O(n) table scans.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The data access layer for database operations. |
pak | - | The Pre-Authentication Key to verify. |
Returns:
A Result containing either the AuthPayload or an error status code.
Source
#![allow(unused)]
fn main() {
async fn verify_pak(dal: &DAL, pak: &str) -> Result<AuthPayload, StatusCode> {
info!("Verifying PAK");
// Generate the PAK hash early so we can use it as cache key
let pak_hash = pak::generate_pak_hash(pak.to_string());
// Check the auth cache first
if let Some(cache) = &dal.auth_cache {
if let Some(cached) = cache.get(&pak_hash) {
debug!("Auth cache hit for PAK hash");
return Ok(cached);
}
debug!("Auth cache miss for PAK hash");
}
let conn = &mut dal.pool.get().map_err(|e| {
error!("Failed to get database connection: {:?}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
// Check admin role
let admin_key = admin_role::table
.select(admin_role::pak_hash)
.first::<String>(conn)
.optional()
.map_err(|e| {
error!("Failed to fetch admin role: {:?}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
if let Some(admin_hash) = admin_key {
if pak::verify_pak(pak.to_string(), admin_hash) {
info!("Admin PAK verified");
let payload = AuthPayload {
admin: true,
agent: None,
generator: None,
};
if let Some(cache) = &dal.auth_cache {
cache.insert(pak_hash, payload.clone());
}
return Ok(payload);
}
}
// Check agents using indexed lookup for O(1) performance
match dal.agents().get_by_pak_hash(&pak_hash) {
Ok(Some(agent)) => {
info!("Agent PAK verified for agent ID: {}", agent.id);
let payload = AuthPayload {
admin: false,
agent: Some(agent.id),
generator: None,
};
if let Some(cache) = &dal.auth_cache {
cache.insert(pak_hash, payload.clone());
}
return Ok(payload);
}
Ok(None) => {} // Not an agent, continue checking
Err(e) => {
error!("Failed to lookup agent by PAK hash: {:?}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
}
// Check generators using indexed lookup for O(1) performance
match dal.generators().get_by_pak_hash(&pak_hash) {
Ok(Some(generator)) => {
info!("Generator PAK verified for generator ID: {}", generator.id);
let payload = AuthPayload {
admin: false,
agent: None,
generator: Some(generator.id),
};
if let Some(cache) = &dal.auth_cache {
cache.insert(pak_hash, payload.clone());
}
return Ok(payload);
}
Ok(None) => {} // Not a generator, continue checking
Err(e) => {
error!("Failed to lookup generator by PAK hash: {:?}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
}
warn!("PAK verification failed");
Err(StatusCode::UNAUTHORIZED)
}
}
brokkr-broker::api::v1::openapi Rust
Structs
brokkr-broker::api::v1::openapi::ApiDoc
pub
Derives: OpenApi
brokkr-broker::api::v1::openapi::SecurityAddon
private
Functions
brokkr-broker::api::v1::openapi::configure_openapi
pub
#![allow(unused)]
fn main() {
fn configure_openapi () -> Router < DAL >
}
Source
#![allow(unused)]
fn main() {
pub fn configure_openapi() -> Router<DAL> {
Router::new()
.route("/docs/openapi.json", get(serve_openapi))
.merge(SwaggerUi::new("/swagger-ui"))
}
}
brokkr-broker::api::v1::openapi::serve_openapi
private
#![allow(unused)]
fn main() {
async fn serve_openapi () -> Json < utoipa :: openapi :: OpenApi >
}
Source
#![allow(unused)]
fn main() {
async fn serve_openapi() -> Json<utoipa::openapi::OpenApi> {
Json(ApiDoc::openapi())
}
}
brokkr-broker::api::v1::stacks Rust
Structs
brokkr-broker::api::v1::stacks::TemplateInstantiationRequest
pub
Derives: Debug, Deserialize, Serialize, ToSchema
Request body for template instantiation.
Fields
| Name | Type | Description |
|---|---|---|
template_id | Uuid | ID of the template to instantiate. |
parameters | serde_json :: Value | Parameters to render the template with. |
Functions
brokkr-broker::api::v1::stacks::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
info!("Setting up stack routes");
Router::new()
.route("/stacks", get(list_stacks).post(create_stack))
.route(
"/stacks/:id",
get(get_stack).put(update_stack).delete(delete_stack),
)
.route(
"/stacks/:id/deployment-objects",
get(list_deployment_objects).post(create_deployment_object),
)
.route(
"/stacks/:id/deployment-objects/from-template",
post(instantiate_template),
)
.route("/stacks/:id/labels", get(list_labels).post(add_label))
.route("/stacks/:id/labels/:label", delete(remove_label))
.route(
"/stacks/:id/annotations",
get(list_annotations).post(add_annotation),
)
.route("/stacks/:id/annotations/:key", delete(remove_annotation))
}
}
brokkr-broker::api::v1::stacks::list_stacks
private
#![allow(unused)]
fn main() {
async fn list_stacks (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > ,) -> Result < Json < Vec < Stack > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists all stacks.
Source
#![allow(unused)]
fn main() {
async fn list_stacks(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
) -> Result<Json<Vec<Stack>>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to list stacks");
if !auth_payload.admin {
warn!("Unauthorized attempt to list stacks");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
match dal.stacks().list() {
Ok(stacks) => {
info!("Successfully retrieved {} stacks", stacks.len());
// Update stacks metric
metrics::set_stacks_total(stacks.len() as i64);
Ok(Json(stacks))
}
Err(e) => {
error!("Failed to fetch stacks: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stacks"})),
))
}
}
}
}
brokkr-broker::api::v1::stacks::create_stack
private
#![allow(unused)]
fn main() {
async fn create_stack (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Json (new_stack) : Json < NewStack > ,) -> Result < Json < Stack > , (StatusCode , Json < serde_json :: Value >) >
}
Creates a new stack.
Source
#![allow(unused)]
fn main() {
async fn create_stack(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Json(new_stack): Json<NewStack>,
) -> Result<Json<Stack>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to create a new stack");
if !auth_payload.admin && auth_payload.generator.is_none() {
warn!("Unauthorized attempt to create a stack");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin or generator access required"})),
));
}
if let Some(generator_id) = auth_payload.generator {
if generator_id != new_stack.generator_id {
warn!("Generator attempted to create stack for another generator");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Generator can only create stacks for itself"})),
));
}
}
match dal.stacks().create(&new_stack) {
Ok(stack) => {
info!("Successfully created stack with ID: {}", stack.id);
// Log audit entry for stack creation
audit::log_action(
ACTOR_TYPE_ADMIN,
None,
ACTION_STACK_CREATED,
RESOURCE_TYPE_STACK,
Some(stack.id),
Some(serde_json::json!({
"name": stack.name,
"generator_id": stack.generator_id,
})),
None,
None,
);
Ok(Json(stack))
}
Err(e) => {
error!("Failed to create stack: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create stack"})),
))
}
}
}
}
brokkr-broker::api::v1::stacks::get_stack
private
#![allow(unused)]
fn main() {
async fn get_stack (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < Stack > , (StatusCode , Json < serde_json :: Value >) >
}
Gets a stack by ID.
Source
#![allow(unused)]
fn main() {
async fn get_stack(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<Stack>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to get stack with ID: {}", id);
let stack = dal.stacks().get(vec![id]).map_err(|e| {
error!("Failed to fetch stack with ID {}: {:?}", id, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack"})),
)
})?;
if stack.is_empty() {
warn!("Stack not found with ID: {}", id);
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Stack not found"})),
));
}
let stack = &stack[0];
if !auth_payload.admin && auth_payload.generator != Some(stack.generator_id) {
warn!("Unauthorized attempt to access stack with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
info!("Successfully retrieved stack with ID: {}", id);
Ok(Json(stack.clone()))
}
}
brokkr-broker::api::v1::stacks::update_stack
private
#![allow(unused)]
fn main() {
async fn update_stack (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Json (updated_stack) : Json < Stack > ,) -> Result < Json < Stack > , (StatusCode , Json < serde_json :: Value >) >
}
Updates a stack.
Source
#![allow(unused)]
fn main() {
async fn update_stack(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Json(updated_stack): Json<Stack>,
) -> Result<Json<Stack>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to update stack with ID: {}", id);
let existing_stack = dal.stacks().get(vec![id]).map_err(|e| {
error!("Failed to fetch stack with ID {}: {:?}", id, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack"})),
)
})?;
if existing_stack.is_empty() {
warn!("Stack not found with ID: {}", id);
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Stack not found"})),
));
}
let existing_stack = &existing_stack[0];
if !auth_payload.admin && auth_payload.generator != Some(existing_stack.generator_id) {
warn!("Unauthorized attempt to update stack with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
if id != updated_stack.id {
warn!("Stack ID mismatch during update for ID: {}", id);
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Stack ID mismatch"})),
));
}
match dal.stacks().update(id, &updated_stack) {
Ok(stack) => {
info!("Successfully updated stack with ID: {}", id);
// Log audit entry for stack update
audit::log_action(
ACTOR_TYPE_ADMIN,
None,
ACTION_STACK_UPDATED,
RESOURCE_TYPE_STACK,
Some(id),
Some(serde_json::json!({
"name": stack.name,
})),
None,
None,
);
Ok(Json(stack))
}
Err(e) => {
error!("Failed to update stack with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update stack"})),
))
}
}
}
}
brokkr-broker::api::v1::stacks::delete_stack
private
#![allow(unused)]
fn main() {
async fn delete_stack (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Deletes a stack.
Source
#![allow(unused)]
fn main() {
async fn delete_stack(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to delete stack with ID: {}", id);
let existing_stack = dal.stacks().get(vec![id]).map_err(|e| {
error!("Failed to fetch stack with ID {}: {:?}", id, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack"})),
)
})?;
if existing_stack.is_empty() {
warn!("Stack not found with ID: {}", id);
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Stack not found"})),
));
}
let existing_stack = &existing_stack[0];
if !auth_payload.admin && auth_payload.generator != Some(existing_stack.generator_id) {
warn!("Unauthorized attempt to delete stack with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
match dal.stacks().soft_delete(id) {
Ok(_) => {
info!("Successfully deleted stack with ID: {}", id);
// Log audit entry for stack deletion
audit::log_action(
ACTOR_TYPE_ADMIN,
None,
ACTION_STACK_DELETED,
RESOURCE_TYPE_STACK,
Some(id),
None,
None,
None,
);
Ok(StatusCode::NO_CONTENT)
}
Err(e) => {
error!("Failed to delete stack with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to delete stack"})),
))
}
}
}
}
brokkr-broker::api::v1::stacks::list_deployment_objects
private
#![allow(unused)]
fn main() {
async fn list_deployment_objects (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (stack_id) : Path < Uuid > ,) -> Result < Json < Vec < DeploymentObject > > , (StatusCode , Json < serde_json :: Value >) >
}
Source
#![allow(unused)]
fn main() {
async fn list_deployment_objects(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(stack_id): Path<Uuid>,
) -> Result<Json<Vec<DeploymentObject>>, (StatusCode, Json<serde_json::Value>)> {
// Check if the user is an admin or the associated generator
if !auth_payload.admin {
let stack = dal.stacks().get(vec![stack_id]).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack"})),
)
})?;
if stack.is_empty() {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Stack not found"})),
));
}
let stack = &stack[0];
if auth_payload.generator != Some(stack.generator_id) {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
}
// Fetch deployment objects
match dal.deployment_objects().list_for_stack(stack_id) {
Ok(objects) => {
// Update deployment objects metric
metrics::set_deployment_objects_total(objects.len() as i64);
Ok(Json(objects))
}
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch deployment objects"})),
)),
}
}
}
brokkr-broker::api::v1::stacks::create_deployment_object
private
#![allow(unused)]
fn main() {
async fn create_deployment_object (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (stack_id) : Path < Uuid > , Json (payload) : Json < serde_json :: Value > ,) -> Result < Json < DeploymentObject > , (StatusCode , Json < serde_json :: Value >) >
}
Source
#![allow(unused)]
fn main() {
async fn create_deployment_object(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(stack_id): Path<Uuid>,
Json(payload): Json<serde_json::Value>,
) -> Result<Json<DeploymentObject>, (StatusCode, Json<serde_json::Value>)> {
// Check if the user is an admin or the associated generator
if !auth_payload.admin {
let stack = dal.stacks().get(vec![stack_id]).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack"})),
)
})?;
if stack.is_empty() {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Stack not found"})),
));
}
let stack = &stack[0];
if auth_payload.generator != Some(stack.generator_id) {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
}
// Extract required fields from payload
let yaml_content = payload["yaml_content"]
.as_str()
.ok_or((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Missing or invalid yaml_content"})),
))?
.to_string();
let is_deletion_marker = payload["is_deletion_marker"].as_bool().unwrap_or(false);
// Create new deployment object with proper hash calculation
let new_object =
NewDeploymentObject::new(stack_id, yaml_content, is_deletion_marker).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e})),
)
})?;
// Create the deployment object
match dal.deployment_objects().create(&new_object) {
Ok(object) => Ok(Json(object)),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create deployment object"})),
)),
}
}
}
brokkr-broker::api::v1::stacks::list_labels
private
#![allow(unused)]
fn main() {
async fn list_labels (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (stack_id) : Path < Uuid > ,) -> Result < Json < Vec < StackLabel > > , (StatusCode , Json < serde_json :: Value >) >
}
Source
#![allow(unused)]
fn main() {
async fn list_labels(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(stack_id): Path<Uuid>,
) -> Result<Json<Vec<StackLabel>>, (StatusCode, Json<serde_json::Value>)> {
// Check authorization
if !is_authorized_for_stack(&dal, &auth_payload, stack_id).await? {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
// Fetch labels
match dal.stack_labels().list_for_stack(stack_id) {
Ok(labels) => Ok(Json(labels)),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack labels"})),
)),
}
}
}
brokkr-broker::api::v1::stacks::add_label
private
#![allow(unused)]
fn main() {
async fn add_label (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (stack_id) : Path < Uuid > , Json (label) : Json < String > ,) -> Result < Json < StackLabel > , (StatusCode , Json < serde_json :: Value >) >
}
Source
#![allow(unused)]
fn main() {
async fn add_label(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(stack_id): Path<Uuid>,
Json(label): Json<String>,
) -> Result<Json<StackLabel>, (StatusCode, Json<serde_json::Value>)> {
// Check if the user is an admin or the associated generator
if !auth_payload.admin {
let stack = dal.stacks().get(vec![stack_id]).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack"})),
)
})?;
if stack.is_empty() {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Stack not found"})),
));
}
let stack = &stack[0];
if auth_payload.generator != Some(stack.generator_id) {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
}
// Create NewStackLabel
let new_label = match NewStackLabel::new(stack_id, label) {
Ok(label) => label,
Err(e) => {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e})),
));
}
};
// Add the label
match dal.stack_labels().create(&new_label) {
Ok(new_label) => Ok(Json(new_label)),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to add stack label"})),
)),
}
}
}
brokkr-broker::api::v1::stacks::remove_label
private
#![allow(unused)]
fn main() {
async fn remove_label (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path ((stack_id , label)) : Path < (Uuid , String) > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Source
#![allow(unused)]
fn main() {
async fn remove_label(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path((stack_id, label)): Path<(Uuid, String)>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
// Check authorization
if !auth_payload.admin {
let stack = dal.stacks().get(vec![stack_id]).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack"})),
)
})?;
if stack.is_empty() {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Stack not found"})),
));
}
let stack = &stack[0];
if auth_payload.generator != Some(stack.generator_id) {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
}
// Delete the label directly using indexed query
match dal
.stack_labels()
.delete_by_stack_and_label(stack_id, &label)
{
Ok(deleted_count) => {
if deleted_count > 0 {
Ok(StatusCode::NO_CONTENT)
} else {
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Label not found"})),
))
}
}
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to remove stack label"})),
)),
}
}
}
brokkr-broker::api::v1::stacks::is_authorized_for_stack
private
#![allow(unused)]
fn main() {
async fn is_authorized_for_stack (dal : & DAL , auth_payload : & AuthPayload , stack_id : Uuid ,) -> Result < bool , (StatusCode , Json < serde_json :: Value >) >
}
Source
#![allow(unused)]
fn main() {
async fn is_authorized_for_stack(
dal: &DAL,
auth_payload: &AuthPayload,
stack_id: Uuid,
) -> Result<bool, (StatusCode, Json<serde_json::Value>)> {
if auth_payload.admin {
return Ok(true);
}
let stack = dal.stacks().get(vec![stack_id]).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack"})),
)
})?;
if stack.is_empty() {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Stack not found"})),
));
}
let stack = &stack[0];
if auth_payload.generator == Some(stack.generator_id) {
return Ok(true);
}
if let Some(agent_id) = auth_payload.agent {
let agent_targets = dal.agent_targets().list_for_agent(agent_id).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent targets"})),
)
})?;
if agent_targets
.iter()
.any(|target| target.stack_id == stack_id)
{
return Ok(true);
}
}
Ok(false)
}
}
brokkr-broker::api::v1::stacks::list_annotations
private
#![allow(unused)]
fn main() {
async fn list_annotations (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (stack_id) : Path < Uuid > ,) -> Result < Json < Vec < StackAnnotation > > , (StatusCode , Json < serde_json :: Value >) >
}
Source
#![allow(unused)]
fn main() {
async fn list_annotations(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(stack_id): Path<Uuid>,
) -> Result<Json<Vec<StackAnnotation>>, (StatusCode, Json<serde_json::Value>)> {
// Check authorization
if !is_authorized_for_stack(&dal, &auth_payload, stack_id).await? {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
// Fetch annotations
match dal.stack_annotations().list_for_stack(stack_id) {
Ok(annotations) => Ok(Json(annotations)),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack annotations"})),
)),
}
}
}
brokkr-broker::api::v1::stacks::add_annotation
private
#![allow(unused)]
fn main() {
async fn add_annotation (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (stack_id) : Path < Uuid > , Json (new_annotation) : Json < NewStackAnnotation > ,) -> Result < Json < StackAnnotation > , (StatusCode , Json < serde_json :: Value >) >
}
Source
#![allow(unused)]
fn main() {
async fn add_annotation(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(stack_id): Path<Uuid>,
Json(new_annotation): Json<NewStackAnnotation>,
) -> Result<Json<StackAnnotation>, (StatusCode, Json<serde_json::Value>)> {
// Check if the user is an admin or the associated generator
if !auth_payload.admin {
let stack = dal.stacks().get(vec![stack_id]).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack"})),
)
})?;
if stack.is_empty() {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Stack not found"})),
));
}
let stack = &stack[0];
if auth_payload.generator != Some(stack.generator_id) {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
}
// Ensure the stack_id in the path matches the one in the new annotation
if new_annotation.stack_id != stack_id {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Stack ID mismatch"})),
));
}
// Add the annotation
match dal.stack_annotations().create(&new_annotation) {
Ok(new_annotation) => Ok(Json(new_annotation)),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to add stack annotation"})),
)),
}
}
}
brokkr-broker::api::v1::stacks::remove_annotation
private
#![allow(unused)]
fn main() {
async fn remove_annotation (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path ((stack_id , key)) : Path < (Uuid , String) > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Source
#![allow(unused)]
fn main() {
async fn remove_annotation(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path((stack_id, key)): Path<(Uuid, String)>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
// Check authorization
if !auth_payload.admin {
let stack = dal.stacks().get(vec![stack_id]).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack"})),
)
})?;
if stack.is_empty() {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Stack not found"})),
));
}
let stack = &stack[0];
if auth_payload.generator != Some(stack.generator_id) {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
}
// Delete the annotation directly using indexed query
match dal
.stack_annotations()
.delete_by_stack_and_key(stack_id, &key)
{
Ok(deleted_count) => {
if deleted_count > 0 {
Ok(StatusCode::NO_CONTENT)
} else {
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Annotation not found"})),
))
}
}
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to remove stack annotation"})),
)),
}
}
}
brokkr-broker::api::v1::stacks::instantiate_template
private
#![allow(unused)]
fn main() {
async fn instantiate_template (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (stack_id) : Path < Uuid > , Json (request) : Json < TemplateInstantiationRequest > ,) -> Result < (StatusCode , Json < DeploymentObject >) , (StatusCode , Json < serde_json :: Value >) >
}
Instantiates a template into a deployment object.
This endpoint renders a template with the provided parameters and creates a deployment object in the specified stack.
Source
#![allow(unused)]
fn main() {
async fn instantiate_template(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(stack_id): Path<Uuid>,
Json(request): Json<TemplateInstantiationRequest>,
) -> Result<(StatusCode, Json<DeploymentObject>), (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling template instantiation: template={}, stack={}",
request.template_id, stack_id
);
// 1. Get stack (404 if not found)
let stack = dal.stacks().get(vec![stack_id]).map_err(|e| {
error!("Failed to fetch stack {}: {:?}", stack_id, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack"})),
)
})?;
if stack.is_empty() {
warn!("Stack not found: {}", stack_id);
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Stack not found"})),
));
}
let stack = &stack[0];
// 2. Verify authorization (admin or generator with stack access)
if !auth_payload.admin && auth_payload.generator != Some(stack.generator_id) {
warn!(
"Unauthorized template instantiation attempt for stack {}",
stack_id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
// 3. Get template (404 if not found/deleted)
let template = dal.templates().get(request.template_id).map_err(|e| {
error!("Failed to fetch template {}: {:?}", request.template_id, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template"})),
)
})?;
let template = match template {
Some(t) => t,
None => {
warn!("Template not found: {}", request.template_id);
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Template not found"})),
));
}
};
// 4. Get template labels/annotations
let template_labels: Vec<String> = dal
.template_labels()
.list_for_template(template.id)
.map_err(|e| {
error!("Failed to fetch template labels: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template labels"})),
)
})?
.into_iter()
.map(|l| l.label)
.collect();
let template_annotations: Vec<(String, String)> = dal
.template_annotations()
.list_for_template(template.id)
.map_err(|e| {
error!("Failed to fetch template annotations: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template annotations"})),
)
})?
.into_iter()
.map(|a| (a.key, a.value))
.collect();
// 5. Get stack labels/annotations
let stack_labels: Vec<String> = dal
.stack_labels()
.list_for_stack(stack_id)
.map_err(|e| {
error!("Failed to fetch stack labels: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack labels"})),
)
})?
.into_iter()
.map(|l| l.label)
.collect();
let stack_annotations: Vec<(String, String)> = dal
.stack_annotations()
.list_for_stack(stack_id)
.map_err(|e| {
error!("Failed to fetch stack annotations: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch stack annotations"})),
)
})?
.into_iter()
.map(|a| (a.key, a.value))
.collect();
// 6. Validate label matching (422 with details on mismatch)
let match_result = template_matches_stack(
&template_labels,
&template_annotations,
&stack_labels,
&stack_annotations,
);
if !match_result.matches {
warn!(
"Template {} labels don't match stack {}: missing_labels={:?}, missing_annotations={:?}",
template.id, stack_id, match_result.missing_labels, match_result.missing_annotations
);
return Err((
StatusCode::UNPROCESSABLE_ENTITY,
Json(serde_json::json!({
"error": "Template labels do not match stack",
"missing_labels": match_result.missing_labels,
"missing_annotations": match_result.missing_annotations,
})),
));
}
// 7. Validate parameters against JSON Schema (400 on invalid)
if let Err(errors) =
templating::validate_parameters(&template.parameters_schema, &request.parameters)
{
let error_messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
warn!("Parameter validation failed: {:?}", error_messages);
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "Invalid parameters",
"validation_errors": error_messages,
})),
));
}
// 8. Render template with Tera (400 on render error)
let rendered_yaml =
templating::render_template(&template.template_content, &request.parameters).map_err(
|e| {
error!("Failed to render template: {:?}", e);
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e.to_string()})),
)
},
)?;
// 9. Create DeploymentObject
let new_deployment_object = NewDeploymentObject::new(stack_id, rendered_yaml.clone(), false)
.map_err(|e| {
error!("Failed to create deployment object: {:?}", e);
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e})),
)
})?;
let deployment_object = dal
.deployment_objects()
.create(&new_deployment_object)
.map_err(|e| {
error!("Failed to insert deployment object: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create deployment object"})),
)
})?;
// 10. Create RenderedDeploymentObject provenance record
let parameters_json = serde_json::to_string(&request.parameters).map_err(|e| {
error!("Failed to serialize parameters: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to serialize parameters"})),
)
})?;
let provenance = NewRenderedDeploymentObject::new(
deployment_object.id,
template.id,
template.version,
parameters_json,
)
.map_err(|e| {
error!("Failed to create provenance record: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e})),
)
})?;
dal.rendered_deployment_objects()
.create(&provenance)
.map_err(|e| {
error!("Failed to insert provenance record: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create provenance record"})),
)
})?;
info!(
"Successfully instantiated template {} into deployment object {} for stack {}",
template.id, deployment_object.id, stack_id
);
Ok((StatusCode::CREATED, Json(deployment_object)))
}
}
brokkr-broker::api::v1::templates Rust
API endpoints for stack template management.
This module provides REST API endpoints for creating, reading, updating, and deleting stack templates, as well as managing template labels and annotations.
Structs
brokkr-broker::api::v1::templates::CreateTemplateRequest
pub
Derives: Debug, Deserialize, Serialize, ToSchema
Request body for creating a new template.
Fields
| Name | Type | Description |
|---|---|---|
name | String | Name of the template. |
description | Option < String > | Optional description. |
template_content | String | Tera template content. |
parameters_schema | String | JSON Schema for parameter validation. |
brokkr-broker::api::v1::templates::UpdateTemplateRequest
pub
Derives: Debug, Deserialize, Serialize, ToSchema
Request body for updating a template (creates new version).
Fields
| Name | Type | Description |
|---|---|---|
description | Option < String > | Optional new description. |
template_content | String | Tera template content. |
parameters_schema | String | JSON Schema for parameter validation. |
brokkr-broker::api::v1::templates::AddAnnotationRequest
pub
Derives: Debug, Deserialize, Serialize, ToSchema
Request body for adding an annotation.
Fields
| Name | Type | Description |
|---|---|---|
key | String | Annotation key. |
value | String | Annotation value. |
Functions
brokkr-broker::api::v1::templates::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Sets up the routes for template management.
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
info!("Setting up template routes");
Router::new()
.route("/templates", get(list_templates).post(create_template))
.route(
"/templates/:id",
get(get_template)
.put(update_template)
.delete(delete_template),
)
.route("/templates/:id/labels", get(list_labels).post(add_label))
.route("/templates/:id/labels/:label", delete(remove_label))
.route(
"/templates/:id/annotations",
get(list_annotations).post(add_annotation),
)
.route("/templates/:id/annotations/:key", delete(remove_annotation))
}
}
brokkr-broker::api::v1::templates::can_modify_template
private
#![allow(unused)]
fn main() {
fn can_modify_template (auth : & AuthPayload , template : & StackTemplate) -> bool
}
Checks if the authenticated user can modify the given template.
System templates (generator_id = NULL) require admin access. Generator templates can be modified by admin or the owning generator.
Source
#![allow(unused)]
fn main() {
fn can_modify_template(auth: &AuthPayload, template: &StackTemplate) -> bool {
if auth.admin {
return true;
}
match (auth.generator, template.generator_id) {
(Some(auth_gen), Some(tmpl_gen)) => auth_gen == tmpl_gen,
_ => false,
}
}
}
brokkr-broker::api::v1::templates::list_templates
private
#![allow(unused)]
fn main() {
async fn list_templates (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > ,) -> Result < Json < Vec < StackTemplate > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists all templates.
Source
#![allow(unused)]
fn main() {
async fn list_templates(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
) -> Result<Json<Vec<StackTemplate>>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to list templates");
let templates = if auth_payload.admin {
// Admin sees all templates
dal.templates().list()
} else if let Some(generator_id) = auth_payload.generator {
// Generator sees system templates + own templates
let system = dal.templates().list_system_templates().map_err(|e| {
error!("Failed to fetch system templates: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch templates"})),
)
})?;
let own = dal
.templates()
.list_for_generator(generator_id)
.map_err(|e| {
error!("Failed to fetch generator templates: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch templates"})),
)
})?;
let mut all = system;
all.extend(own);
Ok(all)
} else {
warn!("Unauthorized attempt to list templates");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin or generator access required"})),
));
};
match templates {
Ok(templates) => {
info!("Successfully retrieved {} templates", templates.len());
Ok(Json(templates))
}
Err(e) => {
error!("Failed to fetch templates: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch templates"})),
))
}
}
}
}
brokkr-broker::api::v1::templates::create_template
private
#![allow(unused)]
fn main() {
async fn create_template (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Json (request) : Json < CreateTemplateRequest > ,) -> Result < (StatusCode , Json < StackTemplate >) , (StatusCode , Json < serde_json :: Value >) >
}
Creates a new template.
Source
#![allow(unused)]
fn main() {
async fn create_template(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Json(request): Json<CreateTemplateRequest>,
) -> Result<(StatusCode, Json<StackTemplate>), (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to create a new template");
// Determine generator_id based on auth
let generator_id = if auth_payload.admin {
// Admin can create system templates (None) or specify a generator
None
} else if let Some(gen_id) = auth_payload.generator {
Some(gen_id)
} else {
warn!("Unauthorized attempt to create template");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin or generator access required"})),
));
};
// Validate Tera syntax
if let Err(e) = templating::validate_tera_syntax(&request.template_content) {
warn!("Template validation failed: {}", e);
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e.to_string()})),
));
}
// Validate JSON Schema
if let Err(e) = templating::validate_json_schema(&request.parameters_schema) {
warn!("JSON Schema validation failed: {}", e);
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e.to_string()})),
));
}
// Create new version (auto-increments version number)
match dal.templates().create_new_version(
generator_id,
request.name,
request.description,
request.template_content,
request.parameters_schema,
) {
Ok(template) => {
info!(
"Successfully created template with ID: {} version: {}",
template.id, template.version
);
Ok((StatusCode::CREATED, Json(template)))
}
Err(e) => {
error!("Failed to create template: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create template"})),
))
}
}
}
}
brokkr-broker::api::v1::templates::get_template
private
#![allow(unused)]
fn main() {
async fn get_template (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < StackTemplate > , (StatusCode , Json < serde_json :: Value >) >
}
Gets a template by ID.
Source
#![allow(unused)]
fn main() {
async fn get_template(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<StackTemplate>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to get template with ID: {}", id);
let template = dal.templates().get(id).map_err(|e| {
error!("Failed to fetch template with ID {}: {:?}", id, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template"})),
)
})?;
let template = match template {
Some(t) => t,
None => {
warn!("Template not found with ID: {}", id);
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Template not found"})),
));
}
};
// Check read access
if !auth_payload.admin {
match (auth_payload.generator, template.generator_id) {
// Generator can read system templates or own templates
(Some(auth_gen), Some(tmpl_gen)) if auth_gen != tmpl_gen => {
warn!("Unauthorized attempt to access template with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
(None, _) => {
warn!("Unauthorized attempt to access template with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
_ => {} // System template (None) or own template - allow
}
}
info!("Successfully retrieved template with ID: {}", id);
Ok(Json(template))
}
}
brokkr-broker::api::v1::templates::update_template
private
#![allow(unused)]
fn main() {
async fn update_template (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Json (request) : Json < UpdateTemplateRequest > ,) -> Result < Json < StackTemplate > , (StatusCode , Json < serde_json :: Value >) >
}
Updates a template by creating a new version.
Templates are immutable - updating creates a new version with the same name.
Source
#![allow(unused)]
fn main() {
async fn update_template(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Json(request): Json<UpdateTemplateRequest>,
) -> Result<Json<StackTemplate>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to update template with ID: {}", id);
// Fetch existing template
let existing = dal.templates().get(id).map_err(|e| {
error!("Failed to fetch template with ID {}: {:?}", id, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template"})),
)
})?;
let existing = match existing {
Some(t) => t,
None => {
warn!("Template not found with ID: {}", id);
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Template not found"})),
));
}
};
// Check authorization
if !can_modify_template(&auth_payload, &existing) {
warn!("Unauthorized attempt to update template with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
// Validate Tera syntax
if let Err(e) = templating::validate_tera_syntax(&request.template_content) {
warn!("Template validation failed: {}", e);
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e.to_string()})),
));
}
// Validate JSON Schema
if let Err(e) = templating::validate_json_schema(&request.parameters_schema) {
warn!("JSON Schema validation failed: {}", e);
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e.to_string()})),
));
}
// Create new version with same name and generator_id
match dal.templates().create_new_version(
existing.generator_id,
existing.name,
request.description.or(existing.description),
request.template_content,
request.parameters_schema,
) {
Ok(template) => {
info!(
"Successfully created new version {} for template: {}",
template.version, template.name
);
Ok(Json(template))
}
Err(e) => {
error!("Failed to create new template version: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update template"})),
))
}
}
}
}
brokkr-broker::api::v1::templates::delete_template
private
#![allow(unused)]
fn main() {
async fn delete_template (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Deletes a template (soft delete).
Source
#![allow(unused)]
fn main() {
async fn delete_template(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to delete template with ID: {}", id);
// Fetch existing template
let existing = dal.templates().get(id).map_err(|e| {
error!("Failed to fetch template with ID {}: {:?}", id, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template"})),
)
})?;
let existing = match existing {
Some(t) => t,
None => {
warn!("Template not found with ID: {}", id);
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Template not found"})),
));
}
};
// Check authorization
if !can_modify_template(&auth_payload, &existing) {
warn!("Unauthorized attempt to delete template with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
match dal.templates().soft_delete(id) {
Ok(_) => {
info!("Successfully deleted template with ID: {}", id);
Ok(StatusCode::NO_CONTENT)
}
Err(e) => {
error!("Failed to delete template with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to delete template"})),
))
}
}
}
}
brokkr-broker::api::v1::templates::list_labels
private
#![allow(unused)]
fn main() {
async fn list_labels (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (template_id) : Path < Uuid > ,) -> Result < Json < Vec < TemplateLabel > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists labels for a template.
Source
#![allow(unused)]
fn main() {
async fn list_labels(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(template_id): Path<Uuid>,
) -> Result<Json<Vec<TemplateLabel>>, (StatusCode, Json<serde_json::Value>)> {
// Check if template exists and user has access
let template = dal.templates().get(template_id).map_err(|e| {
error!("Failed to fetch template: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template"})),
)
})?;
let template = match template {
Some(t) => t,
None => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Template not found"})),
))
}
};
// Check read access (same as get_template)
if !auth_payload.admin {
match (auth_payload.generator, template.generator_id) {
(Some(auth_gen), Some(tmpl_gen)) if auth_gen != tmpl_gen => {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
))
}
(None, _) => {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
))
}
_ => {}
}
}
match dal.template_labels().list_for_template(template_id) {
Ok(labels) => Ok(Json(labels)),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template labels"})),
)),
}
}
}
brokkr-broker::api::v1::templates::add_label
private
#![allow(unused)]
fn main() {
async fn add_label (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (template_id) : Path < Uuid > , Json (label) : Json < String > ,) -> Result < Json < TemplateLabel > , (StatusCode , Json < serde_json :: Value >) >
}
Adds a label to a template.
Source
#![allow(unused)]
fn main() {
async fn add_label(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(template_id): Path<Uuid>,
Json(label): Json<String>,
) -> Result<Json<TemplateLabel>, (StatusCode, Json<serde_json::Value>)> {
// Check if template exists and user can modify
let template = dal.templates().get(template_id).map_err(|e| {
error!("Failed to fetch template: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template"})),
)
})?;
let template = match template {
Some(t) => t,
None => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Template not found"})),
))
}
};
if !can_modify_template(&auth_payload, &template) {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
let new_label = NewTemplateLabel::new(template_id, label).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e})),
)
})?;
match dal.template_labels().create(&new_label) {
Ok(label) => Ok(Json(label)),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to add template label"})),
)),
}
}
}
brokkr-broker::api::v1::templates::remove_label
private
#![allow(unused)]
fn main() {
async fn remove_label (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path ((template_id , label)) : Path < (Uuid , String) > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Removes a label from a template.
Source
#![allow(unused)]
fn main() {
async fn remove_label(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path((template_id, label)): Path<(Uuid, String)>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
// Check if template exists and user can modify
let template = dal.templates().get(template_id).map_err(|e| {
error!("Failed to fetch template: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template"})),
)
})?;
let template = match template {
Some(t) => t,
None => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Template not found"})),
))
}
};
if !can_modify_template(&auth_payload, &template) {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
// Find and delete the label
match dal.template_labels().list_for_template(template_id) {
Ok(labels) => {
if let Some(template_label) = labels.into_iter().find(|l| l.label == label) {
match dal.template_labels().delete(template_label.id) {
Ok(_) => Ok(StatusCode::NO_CONTENT),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to remove template label"})),
)),
}
} else {
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Label not found"})),
))
}
}
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template labels"})),
)),
}
}
}
brokkr-broker::api::v1::templates::list_annotations
private
#![allow(unused)]
fn main() {
async fn list_annotations (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (template_id) : Path < Uuid > ,) -> Result < Json < Vec < TemplateAnnotation > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists annotations for a template.
Source
#![allow(unused)]
fn main() {
async fn list_annotations(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(template_id): Path<Uuid>,
) -> Result<Json<Vec<TemplateAnnotation>>, (StatusCode, Json<serde_json::Value>)> {
// Check if template exists and user has access
let template = dal.templates().get(template_id).map_err(|e| {
error!("Failed to fetch template: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template"})),
)
})?;
let template = match template {
Some(t) => t,
None => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Template not found"})),
))
}
};
// Check read access
if !auth_payload.admin {
match (auth_payload.generator, template.generator_id) {
(Some(auth_gen), Some(tmpl_gen)) if auth_gen != tmpl_gen => {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
))
}
(None, _) => {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
))
}
_ => {}
}
}
match dal.template_annotations().list_for_template(template_id) {
Ok(annotations) => Ok(Json(annotations)),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template annotations"})),
)),
}
}
}
brokkr-broker::api::v1::templates::add_annotation
private
#![allow(unused)]
fn main() {
async fn add_annotation (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (template_id) : Path < Uuid > , Json (request) : Json < AddAnnotationRequest > ,) -> Result < Json < TemplateAnnotation > , (StatusCode , Json < serde_json :: Value >) >
}
Adds an annotation to a template.
Source
#![allow(unused)]
fn main() {
async fn add_annotation(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(template_id): Path<Uuid>,
Json(request): Json<AddAnnotationRequest>,
) -> Result<Json<TemplateAnnotation>, (StatusCode, Json<serde_json::Value>)> {
// Check if template exists and user can modify
let template = dal.templates().get(template_id).map_err(|e| {
error!("Failed to fetch template: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template"})),
)
})?;
let template = match template {
Some(t) => t,
None => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Template not found"})),
))
}
};
if !can_modify_template(&auth_payload, &template) {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
let new_annotation = NewTemplateAnnotation::new(template_id, request.key, request.value)
.map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e})),
)
})?;
match dal.template_annotations().create(&new_annotation) {
Ok(annotation) => Ok(Json(annotation)),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to add template annotation"})),
)),
}
}
}
brokkr-broker::api::v1::templates::remove_annotation
private
#![allow(unused)]
fn main() {
async fn remove_annotation (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path ((template_id , key)) : Path < (Uuid , String) > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Removes an annotation from a template.
Source
#![allow(unused)]
fn main() {
async fn remove_annotation(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path((template_id, key)): Path<(Uuid, String)>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
// Check if template exists and user can modify
let template = dal.templates().get(template_id).map_err(|e| {
error!("Failed to fetch template: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template"})),
)
})?;
let template = match template {
Some(t) => t,
None => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Template not found"})),
))
}
};
if !can_modify_template(&auth_payload, &template) {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
// Find and delete the annotation
match dal.template_annotations().list_for_template(template_id) {
Ok(annotations) => {
if let Some(annotation) = annotations.into_iter().find(|a| a.key == key) {
match dal.template_annotations().delete(annotation.id) {
Ok(_) => Ok(StatusCode::NO_CONTENT),
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to remove template annotation"})),
)),
}
} else {
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Annotation not found"})),
))
}
}
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch template annotations"})),
)),
}
}
}
brokkr-broker::api::v1::webhooks Rust
Webhooks API module for Brokkr.
This module provides routes and handlers for managing webhook subscriptions, including CRUD operations and delivery status inspection.
Structs
brokkr-broker::api::v1::webhooks::CreateWebhookRequest
pub
Derives: Debug, Clone, Deserialize, ToSchema
Request body for creating a webhook subscription.
Fields
| Name | Type | Description |
|---|---|---|
name | String | Human-readable name for the subscription. |
url | String | Webhook endpoint URL (will be encrypted at rest). |
auth_header | Option < String > | Optional Authorization header value (will be encrypted at rest). |
event_types | Vec < String > | Event types to subscribe to (supports wildcards like “deployment.*”). |
filters | Option < WebhookFilters > | Optional filters to narrow which events are delivered. |
max_retries | Option < i32 > | Maximum number of delivery retries (default: 5). |
timeout_seconds | Option < i32 > | HTTP timeout in seconds (default: 30). |
validate | bool | Whether to validate the URL by sending a test request. |
target_labels | Option < Vec < String > > | Labels for delivery targeting. |
| NULL/empty = broker delivers; labels = matching agent delivers. |
brokkr-broker::api::v1::webhooks::UpdateWebhookRequest
pub
Derives: Debug, Clone, Deserialize, ToSchema
Request body for updating a webhook subscription.
Fields
| Name | Type | Description |
|---|---|---|
name | Option < String > | New name. |
url | Option < String > | New URL (will be encrypted at rest). |
auth_header | Option < Option < String > > | New Authorization header (will be encrypted at rest). |
| Use null to remove, omit to keep unchanged. | ||
event_types | Option < Vec < String > > | New event types. |
filters | Option < Option < WebhookFilters > > | New filters. |
enabled | Option < bool > | Enable/disable the subscription. |
max_retries | Option < i32 > | New max retries. |
timeout_seconds | Option < i32 > | New timeout. |
target_labels | Option < Option < Vec < String > > > | New target labels for delivery. |
| NULL/empty = broker delivers; labels = matching agent delivers. |
brokkr-broker::api::v1::webhooks::WebhookResponse
pub
Derives: Debug, Clone, Serialize, ToSchema
Response for a webhook subscription (safe view without encrypted fields).
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier. |
name | String | Human-readable name. |
has_url | bool | Whether a URL is configured (actual value is encrypted). |
has_auth_header | bool | Whether an auth header is configured (actual value is encrypted). |
event_types | Vec < String > | Subscribed event types. |
filters | Option < WebhookFilters > | Configured filters. |
target_labels | Option < Vec < String > > | Labels for delivery targeting (NULL = broker delivers). |
enabled | bool | Whether the subscription is active. |
max_retries | i32 | Maximum delivery retries. |
timeout_seconds | i32 | HTTP timeout in seconds. |
created_at | chrono :: DateTime < chrono :: Utc > | When created. |
updated_at | chrono :: DateTime < chrono :: Utc > | When last updated. |
created_by | Option < String > | Who created this subscription. |
brokkr-broker::api::v1::webhooks::ListDeliveriesQuery
pub
Derives: Debug, Clone, Deserialize, ToSchema
Query parameters for listing deliveries.
Fields
| Name | Type | Description |
|---|---|---|
status | Option < String > | Filter by status (pending, acquired, success, failed, dead). |
limit | Option < i64 > | Maximum number of results (default: 50). |
offset | Option < i64 > | Offset for pagination. |
brokkr-broker::api::v1::webhooks::PendingWebhookDelivery
pub
Derives: Debug, Clone, Serialize, ToSchema
Pending webhook delivery for an agent (includes decrypted secrets).
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Delivery ID. |
subscription_id | Uuid | Subscription ID. |
event_type | String | Event type being delivered. |
payload | String | JSON-encoded event payload. |
url | String | Decrypted webhook URL. |
auth_header | Option < String > | Decrypted Authorization header (if configured). |
timeout_seconds | i32 | HTTP timeout in seconds. |
max_retries | i32 | Maximum retries for this subscription. |
attempts | i32 | Current attempt number. |
brokkr-broker::api::v1::webhooks::DeliveryResultRequest
pub
Derives: Debug, Clone, Deserialize, ToSchema
Request body for reporting delivery result.
Fields
| Name | Type | Description |
|---|---|---|
success | bool | Whether delivery succeeded. |
status_code | Option < i32 > | HTTP status code (if available). |
error | Option < String > | Error message (if failed). |
duration_ms | Option < i64 > | Delivery duration in milliseconds. |
Functions
brokkr-broker::api::v1::webhooks::encrypt_value
private
#![allow(unused)]
fn main() {
fn encrypt_value (value : & str) -> Result < Vec < u8 > , (StatusCode , Json < serde_json :: Value >) >
}
Encrypts a value for storage.
Source
#![allow(unused)]
fn main() {
fn encrypt_value(value: &str) -> Result<Vec<u8>, (StatusCode, Json<serde_json::Value>)> {
encryption::encrypt_string(value).map_err(|e| {
error!("Encryption failed: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to encrypt data"})),
)
})
}
}
brokkr-broker::api::v1::webhooks::decrypt_value
private
#![allow(unused)]
fn main() {
fn decrypt_value (encrypted : & [u8]) -> Result < String , String >
}
Decrypts a stored value back to a string.
Source
#![allow(unused)]
fn main() {
fn decrypt_value(encrypted: &[u8]) -> Result<String, String> {
encryption::decrypt_string(encrypted)
}
}
brokkr-broker::api::v1::webhooks::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Creates and returns the router for webhook endpoints.
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
info!("Setting up webhook routes");
Router::new()
.route("/webhooks", get(list_webhooks))
.route("/webhooks", post(create_webhook))
.route("/webhooks/event-types", get(list_event_types))
.route("/webhooks/:id", get(get_webhook))
.route("/webhooks/:id", put(update_webhook))
.route("/webhooks/:id", delete(delete_webhook))
.route("/webhooks/:id/deliveries", get(list_deliveries))
.route("/webhooks/:id/test", post(test_webhook))
// Agent webhook delivery endpoints
.route(
"/agents/:agent_id/webhooks/pending",
get(get_pending_agent_webhooks),
)
.route(
"/webhook-deliveries/:id/result",
post(report_delivery_result),
)
}
}
brokkr-broker::api::v1::webhooks::list_webhooks
private
#![allow(unused)]
fn main() {
async fn list_webhooks (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > ,) -> Result < Json < Vec < WebhookResponse > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists all webhook subscriptions. Requires admin access.
Source
#![allow(unused)]
fn main() {
async fn list_webhooks(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
) -> Result<Json<Vec<WebhookResponse>>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to list webhook subscriptions");
if !auth_payload.admin {
warn!("Unauthorized attempt to list webhooks");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
match dal.webhook_subscriptions().list(false) {
Ok(subscriptions) => {
info!(
"Successfully retrieved {} webhook subscriptions",
subscriptions.len()
);
let responses: Vec<WebhookResponse> =
subscriptions.into_iter().map(Into::into).collect();
Ok(Json(responses))
}
Err(e) => {
error!("Failed to fetch webhook subscriptions: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch webhook subscriptions"})),
))
}
}
}
}
brokkr-broker::api::v1::webhooks::list_event_types
private
#![allow(unused)]
fn main() {
async fn list_event_types (Extension (auth_payload) : Extension < AuthPayload > ,) -> Result < Json < Vec < & 'static str > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists all available event types for webhook subscriptions.
Source
#![allow(unused)]
fn main() {
async fn list_event_types(
Extension(auth_payload): Extension<AuthPayload>,
) -> Result<Json<Vec<&'static str>>, (StatusCode, Json<serde_json::Value>)> {
if !auth_payload.admin {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
Ok(Json(VALID_EVENT_TYPES.to_vec()))
}
}
brokkr-broker::api::v1::webhooks::create_webhook
private
#![allow(unused)]
fn main() {
async fn create_webhook (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Json (request) : Json < CreateWebhookRequest > ,) -> Result < (StatusCode , Json < WebhookResponse >) , (StatusCode , Json < serde_json :: Value >) >
}
Creates a new webhook subscription. Requires admin access.
Source
#![allow(unused)]
fn main() {
async fn create_webhook(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Json(request): Json<CreateWebhookRequest>,
) -> Result<(StatusCode, Json<WebhookResponse>), (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to create webhook subscription");
if !auth_payload.admin {
warn!("Unauthorized attempt to create webhook");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
// Validate URL
if request.url.trim().is_empty() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "URL is required"})),
));
}
// Basic URL validation
if !request.url.starts_with("http://") && !request.url.starts_with("https://") {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "URL must start with http:// or https://"})),
));
}
// Encrypt URL and auth header
let url_encrypted = encrypt_value(&request.url)?;
let auth_header_encrypted = match &request.auth_header {
Some(h) => Some(encrypt_value(h)?),
None => None,
};
// Determine who created this
let created_by = if auth_payload.admin {
Some("admin".to_string())
} else {
auth_payload.generator.map(|id| id.to_string())
};
// Build the new subscription
let new_sub = match NewWebhookSubscription::new(
request.name,
url_encrypted,
auth_header_encrypted,
request.event_types,
request.filters,
request.target_labels,
created_by,
) {
Ok(mut sub) => {
// Apply optional settings
if let Some(max_retries) = request.max_retries {
sub.max_retries = max_retries;
}
if let Some(timeout) = request.timeout_seconds {
sub.timeout_seconds = timeout;
}
sub
}
Err(e) => {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e})),
));
}
};
// Create in database
match dal.webhook_subscriptions().create(&new_sub) {
Ok(subscription) => {
info!(
"Successfully created webhook subscription with ID: {}",
subscription.id
);
// Log audit entry for webhook creation
audit::log_action(
ACTOR_TYPE_ADMIN,
None,
ACTION_WEBHOOK_CREATED,
RESOURCE_TYPE_WEBHOOK,
Some(subscription.id),
Some(serde_json::json!({
"name": subscription.name,
"event_types": subscription.event_types,
})),
None,
None,
);
Ok((StatusCode::CREATED, Json(subscription.into())))
}
Err(e) => {
error!("Failed to create webhook subscription: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create webhook subscription"})),
))
}
}
}
}
brokkr-broker::api::v1::webhooks::get_webhook
private
#![allow(unused)]
fn main() {
async fn get_webhook (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < WebhookResponse > , (StatusCode , Json < serde_json :: Value >) >
}
Retrieves a specific webhook subscription by ID. Requires admin access.
Source
#![allow(unused)]
fn main() {
async fn get_webhook(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<WebhookResponse>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to get webhook subscription with ID: {}",
id
);
if !auth_payload.admin {
warn!("Unauthorized attempt to access webhook with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
match dal.webhook_subscriptions().get(id) {
Ok(Some(subscription)) => {
info!(
"Successfully retrieved webhook subscription with ID: {}",
id
);
Ok(Json(subscription.into()))
}
Ok(None) => {
warn!("Webhook subscription not found with ID: {}", id);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Webhook subscription not found"})),
))
}
Err(e) => {
error!(
"Failed to fetch webhook subscription with ID {}: {:?}",
id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch webhook subscription"})),
))
}
}
}
}
brokkr-broker::api::v1::webhooks::update_webhook
private
#![allow(unused)]
fn main() {
async fn update_webhook (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Json (request) : Json < UpdateWebhookRequest > ,) -> Result < Json < WebhookResponse > , (StatusCode , Json < serde_json :: Value >) >
}
Updates an existing webhook subscription. Requires admin access.
Source
#![allow(unused)]
fn main() {
async fn update_webhook(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Json(request): Json<UpdateWebhookRequest>,
) -> Result<Json<WebhookResponse>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to update webhook subscription with ID: {}",
id
);
if !auth_payload.admin {
warn!("Unauthorized attempt to update webhook with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
// Verify it exists
match dal.webhook_subscriptions().get(id) {
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Webhook subscription not found"})),
));
}
Err(e) => {
error!("Failed to fetch webhook subscription: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch webhook subscription"})),
));
}
Ok(Some(_)) => {}
}
// Build changeset - handle encryption with proper error handling
let url_encrypted = match request.url {
Some(u) => Some(encrypt_value(&u)?),
None => None,
};
let auth_header_encrypted = match request.auth_header {
Some(Some(h)) => Some(Some(encrypt_value(&h)?)),
Some(None) => Some(None), // Explicitly clear the auth header
None => None, // No change to auth header
};
// Convert target_labels to the format expected by the changeset
let target_labels = request
.target_labels
.map(|opt| opt.map(|labels| labels.into_iter().map(Some).collect()));
let changeset = UpdateWebhookSubscription {
name: request.name,
url_encrypted,
auth_header_encrypted,
event_types: request
.event_types
.map(|types| types.into_iter().map(Some).collect()),
filters: request
.filters
.map(|opt| opt.map(|f| serde_json::to_string(&f).unwrap_or_default())),
target_labels,
enabled: request.enabled,
max_retries: request.max_retries,
timeout_seconds: request.timeout_seconds,
};
match dal.webhook_subscriptions().update(id, &changeset) {
Ok(subscription) => {
info!("Successfully updated webhook subscription with ID: {}", id);
// Log audit entry for webhook update
audit::log_action(
ACTOR_TYPE_ADMIN,
None,
ACTION_WEBHOOK_UPDATED,
RESOURCE_TYPE_WEBHOOK,
Some(id),
Some(serde_json::json!({
"name": subscription.name,
"enabled": subscription.enabled,
})),
None,
None,
);
Ok(Json(subscription.into()))
}
Err(e) => {
error!(
"Failed to update webhook subscription with ID {}: {:?}",
id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update webhook subscription"})),
))
}
}
}
}
brokkr-broker::api::v1::webhooks::delete_webhook
private
#![allow(unused)]
fn main() {
async fn delete_webhook (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Deletes a webhook subscription. Requires admin access.
Source
#![allow(unused)]
fn main() {
async fn delete_webhook(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to delete webhook subscription with ID: {}",
id
);
if !auth_payload.admin {
warn!("Unauthorized attempt to delete webhook with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
match dal.webhook_subscriptions().delete(id) {
Ok(count) if count > 0 => {
info!("Successfully deleted webhook subscription with ID: {}", id);
// Log audit entry for webhook deletion
audit::log_action(
ACTOR_TYPE_ADMIN,
None,
ACTION_WEBHOOK_DELETED,
RESOURCE_TYPE_WEBHOOK,
Some(id),
None,
None,
None,
);
Ok(StatusCode::NO_CONTENT)
}
Ok(_) => {
warn!("Webhook subscription not found with ID: {}", id);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Webhook subscription not found"})),
))
}
Err(e) => {
error!(
"Failed to delete webhook subscription with ID {}: {:?}",
id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to delete webhook subscription"})),
))
}
}
}
}
brokkr-broker::api::v1::webhooks::list_deliveries
private
#![allow(unused)]
fn main() {
async fn list_deliveries (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Query (query) : Query < ListDeliveriesQuery > ,) -> Result < Json < Vec < WebhookDelivery > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists deliveries for a specific webhook subscription. Requires admin access.
Source
#![allow(unused)]
fn main() {
async fn list_deliveries(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Query(query): Query<ListDeliveriesQuery>,
) -> Result<Json<Vec<WebhookDelivery>>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to list deliveries for webhook subscription: {}",
id
);
if !auth_payload.admin {
warn!(
"Unauthorized attempt to list deliveries for webhook: {}",
id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
// Verify subscription exists
match dal.webhook_subscriptions().get(id) {
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Webhook subscription not found"})),
));
}
Err(e) => {
error!("Failed to fetch webhook subscription: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch webhook subscription"})),
));
}
Ok(Some(_)) => {}
}
let limit = query.limit.unwrap_or(50);
let offset = query.offset.unwrap_or(0);
match dal
.webhook_deliveries()
.list_for_subscription(id, query.status.as_deref(), limit, offset)
{
Ok(deliveries) => {
info!(
"Successfully retrieved {} deliveries for subscription {}",
deliveries.len(),
id
);
Ok(Json(deliveries))
}
Err(e) => {
error!(
"Failed to fetch deliveries for subscription {}: {:?}",
id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch deliveries"})),
))
}
}
}
}
brokkr-broker::api::v1::webhooks::test_webhook
private
#![allow(unused)]
fn main() {
async fn test_webhook (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < serde_json :: Value > , (StatusCode , Json < serde_json :: Value >) >
}
Sends a test event to the webhook endpoint. Requires admin access.
Source
#![allow(unused)]
fn main() {
async fn test_webhook(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to test webhook subscription with ID: {}",
id
);
if !auth_payload.admin {
warn!("Unauthorized attempt to test webhook with ID: {}", id);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
// Get the subscription
let subscription = match dal.webhook_subscriptions().get(id) {
Ok(Some(sub)) => sub,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Webhook subscription not found"})),
));
}
Err(e) => {
error!("Failed to fetch webhook subscription: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch webhook subscription"})),
));
}
};
// Decrypt URL and auth header
let url = match decrypt_value(&subscription.url_encrypted) {
Ok(u) => u,
Err(e) => {
error!("Failed to decrypt URL: {}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to decrypt webhook URL"})),
));
}
};
let auth_header = subscription
.auth_header_encrypted
.as_ref()
.map(|h| decrypt_value(h))
.transpose()
.map_err(|e| {
error!("Failed to decrypt auth header: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to decrypt auth header"})),
)
})?;
// Create test payload
let test_event = serde_json::json!({
"id": Uuid::new_v4(),
"event_type": "webhook.test",
"timestamp": chrono::Utc::now(),
"data": {
"message": "This is a test webhook delivery from Brokkr",
"subscription_id": id
}
});
// Send test request
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(
subscription.timeout_seconds as u64,
))
.build()
.map_err(|e| {
error!("Failed to create HTTP client: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create HTTP client"})),
)
})?;
let mut request = client
.post(&url)
.header("Content-Type", "application/json")
.json(&test_event);
if let Some(auth) = &auth_header {
request = request.header("Authorization", auth);
}
match request.send().await {
Ok(response) => {
let status = response.status();
if status.is_success() {
info!("Test webhook delivery succeeded for subscription {}", id);
Ok(Json(serde_json::json!({
"success": true,
"status_code": status.as_u16(),
"message": "Test delivery successful"
})))
} else {
let body = response.text().await.unwrap_or_default();
warn!(
"Test webhook delivery failed with status {}: {}",
status, body
);
Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"success": false,
"status_code": status.as_u16(),
"error": format!("Endpoint returned HTTP {}", status),
"body": body.chars().take(500).collect::<String>()
})),
))
}
}
Err(e) => {
error!("Test webhook delivery failed: {:?}", e);
Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"success": false,
"error": format!("Request failed: {}", e)
})),
))
}
}
}
}
brokkr-broker::api::v1::webhooks::get_pending_agent_webhooks
private
#![allow(unused)]
fn main() {
async fn get_pending_agent_webhooks (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (agent_id) : Path < Uuid > ,) -> Result < Json < Vec < PendingWebhookDelivery > > , (StatusCode , Json < serde_json :: Value >) >
}
Gets pending webhook deliveries for an agent to process. Claims deliveries matching the agent’s labels and returns them with decrypted URLs.
Source
#![allow(unused)]
fn main() {
async fn get_pending_agent_webhooks(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(agent_id): Path<Uuid>,
) -> Result<Json<Vec<PendingWebhookDelivery>>, (StatusCode, Json<serde_json::Value>)> {
debug!(
"Handling request for pending webhooks for agent: {}",
agent_id
);
// Verify the caller is the agent itself or an admin
if !auth_payload.admin && auth_payload.agent != Some(agent_id) {
warn!(
"Unauthorized access to agent webhooks: {:?} != {:?}",
auth_payload.agent, agent_id
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Unauthorized - must be the agent or admin"})),
));
}
// Verify agent exists and get its labels
let agent_labels: Vec<String> = match dal.agents().get(agent_id) {
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Agent not found"})),
));
}
Err(e) => {
error!("Failed to fetch agent: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch agent"})),
));
}
Ok(Some(_)) => {
// Get agent labels
match dal.agent_labels().list_for_agent(agent_id) {
Ok(labels) => labels.into_iter().map(|l| l.label).collect(),
Err(e) => {
error!("Failed to fetch agent labels: {:?}", e);
vec![]
}
}
}
};
// Claim pending deliveries for this agent based on label matching
let deliveries =
match dal
.webhook_deliveries()
.claim_for_agent(agent_id, &agent_labels, 10, None)
{
Ok(d) => d,
Err(e) => {
error!("Failed to claim pending deliveries: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to claim pending deliveries"})),
));
}
};
// For each claimed delivery, get the subscription to decrypt URL/auth
let mut pending = Vec::with_capacity(deliveries.len());
for delivery in deliveries {
// Get the subscription
let subscription = match dal.webhook_subscriptions().get(delivery.subscription_id) {
Ok(Some(sub)) => sub,
Ok(None) => {
warn!(
"Subscription {} not found for delivery {}",
delivery.subscription_id, delivery.id
);
continue;
}
Err(e) => {
error!("Failed to fetch subscription: {:?}", e);
continue;
}
};
// Decrypt URL
let url = match decrypt_value(&subscription.url_encrypted) {
Ok(u) => u,
Err(e) => {
error!(
"Failed to decrypt URL for subscription {}: {}",
subscription.id, e
);
continue;
}
};
// Decrypt auth header if present
let auth_header = match subscription.auth_header_encrypted {
Some(ref encrypted) => match decrypt_value(encrypted) {
Ok(h) => Some(h),
Err(e) => {
error!(
"Failed to decrypt auth header for subscription {}: {}",
subscription.id, e
);
None
}
},
None => None,
};
pending.push(PendingWebhookDelivery {
id: delivery.id,
subscription_id: delivery.subscription_id,
event_type: delivery.event_type,
payload: delivery.payload,
url,
auth_header,
timeout_seconds: subscription.timeout_seconds,
max_retries: subscription.max_retries,
attempts: delivery.attempts,
});
}
debug!(
"Returning {} pending webhook deliveries for agent {}",
pending.len(),
agent_id
);
Ok(Json(pending))
}
}
brokkr-broker::api::v1::webhooks::report_delivery_result
private
#![allow(unused)]
fn main() {
async fn report_delivery_result (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (delivery_id) : Path < Uuid > , Json (request) : Json < DeliveryResultRequest > ,) -> Result < Json < serde_json :: Value > , (StatusCode , Json < serde_json :: Value >) >
}
Reports the result of a webhook delivery attempt by an agent.
Source
#![allow(unused)]
fn main() {
async fn report_delivery_result(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(delivery_id): Path<Uuid>,
Json(request): Json<DeliveryResultRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
debug!(
"Handling delivery result report for delivery: {}",
delivery_id
);
// Must be an agent
let agent_id = match auth_payload.agent {
Some(id) => id,
None if auth_payload.admin => {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Agent authentication required"})),
));
}
None => {
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Agent authentication required"})),
));
}
};
// Get the delivery to verify it exists and was acquired by this agent
let delivery = match dal.webhook_deliveries().get(delivery_id) {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Delivery not found"})),
));
}
Err(e) => {
error!("Failed to fetch delivery: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch delivery"})),
));
}
};
// Verify this delivery was acquired by this agent
if delivery.acquired_by != Some(agent_id) {
warn!(
"Agent {} tried to report result for delivery {} acquired by {:?}",
agent_id, delivery_id, delivery.acquired_by
);
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Delivery not acquired by this agent"})),
));
}
// Get subscription for max_retries
let subscription = match dal.webhook_subscriptions().get(delivery.subscription_id) {
Ok(Some(sub)) => sub,
Ok(None) => {
error!(
"Subscription {} not found for delivery {}",
delivery.subscription_id, delivery_id
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Subscription not found"})),
));
}
Err(e) => {
error!("Failed to fetch subscription: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch subscription"})),
));
}
};
// Record the result
if request.success {
match dal.webhook_deliveries().mark_success(delivery_id) {
Ok(_) => {
info!(
"Webhook delivery {} succeeded via agent {}",
delivery_id, agent_id
);
Ok(Json(serde_json::json!({
"status": "success",
"delivery_id": delivery_id
})))
}
Err(e) => {
error!("Failed to mark delivery as success: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update delivery"})),
))
}
}
} else {
let error_msg = request.error.unwrap_or_else(|| "Unknown error".to_string());
match dal.webhook_deliveries().mark_failed(
delivery_id,
&error_msg,
subscription.max_retries,
) {
Ok(updated) => {
info!(
"Webhook delivery {} failed via agent {}: {}",
delivery_id, agent_id, error_msg
);
Ok(Json(serde_json::json!({
"status": updated.status,
"delivery_id": delivery_id,
"attempts": updated.attempts,
"next_retry_at": updated.next_retry_at
})))
}
Err(e) => {
error!("Failed to mark delivery as failed: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update delivery"})),
))
}
}
}
}
}
brokkr-broker::api::v1::work_orders Rust
Handles API routes and logic for work orders.
This module provides functionality for managing work orders through their lifecycle:
- Creating work orders with target agents
- Claiming work orders by agents
- Completing work orders (success/failure)
- Querying work order history from the log
Structs
brokkr-broker::api::v1::work_orders::CreateWorkOrderRequest
pub
Derives: Debug, Deserialize, Serialize, ToSchema
Request body for creating a new work order.
Fields
| Name | Type | Description |
|---|---|---|
work_type | String | Type of work (e.g., “build”, “test”, “backup”). |
yaml_content | String | Multi-document YAML content. |
max_retries | Option < i32 > | Maximum number of retry attempts (default: 3). |
backoff_seconds | Option < i32 > | Base backoff seconds for exponential retry (default: 60). |
claim_timeout_seconds | Option < i32 > | Claim timeout in seconds (default: 3600). |
targeting | Option < WorkOrderTargeting > | Optional targeting configuration. At least one targeting method must be specified. |
target_agent_ids | Option < Vec < Uuid > > | DEPRECATED: Use targeting.agent_ids instead. Target agent IDs that can claim this work order. |
brokkr-broker::api::v1::work_orders::WorkOrderTargeting
pub
Derives: Debug, Default, Deserialize, Serialize, ToSchema
Targeting configuration for work orders. Work orders can be targeted using any combination of hard targets (agent IDs), labels, or annotations. Matching uses OR logic - an agent is eligible if it matches ANY of the specified targeting criteria.
Fields
| Name | Type | Description |
|---|---|---|
agent_ids | Option < Vec < Uuid > > | Direct agent IDs that can claim this work order (hard targets). |
labels | Option < Vec < String > > | Labels that agents must have (OR logic - agent needs any one of these labels). |
annotations | Option < std :: collections :: HashMap < String , String > > | Annotations that agents must have (OR logic - agent needs any one of these key-value pairs). |
brokkr-broker::api::v1::work_orders::ClaimWorkOrderRequest
pub
Derives: Debug, Deserialize, Serialize, ToSchema
Request body for claiming a work order.
Fields
| Name | Type | Description |
|---|---|---|
agent_id | Uuid | ID of the agent claiming the work order. |
brokkr-broker::api::v1::work_orders::CompleteWorkOrderRequest
pub
Derives: Debug, Deserialize, Serialize, ToSchema
Request body for completing a work order.
Fields
| Name | Type | Description |
|---|---|---|
success | bool | Whether the work completed successfully. |
message | Option < String > | Result message (image digest on success, error details on failure). |
retryable | bool | Whether the failure is retryable. Defaults to true if not specified. |
| Set to false for permanent failures (e.g., invalid YAML, missing namespace). |
brokkr-broker::api::v1::work_orders::ListWorkOrdersQuery
pub
Derives: Debug, Deserialize
Query parameters for listing work orders.
Fields
| Name | Type | Description |
|---|---|---|
status | Option < String > | Filter by status (PENDING, CLAIMED, RETRY_PENDING). |
work_type | Option < String > | Filter by work type. |
brokkr-broker::api::v1::work_orders::ListPendingQuery
pub
Derives: Debug, Deserialize
Query parameters for listing pending work orders for an agent.
Fields
| Name | Type | Description |
|---|---|---|
work_type | Option < String > | Filter by work type. |
brokkr-broker::api::v1::work_orders::ListLogQuery
pub
Derives: Debug, Deserialize
Query parameters for listing work order log.
Fields
| Name | Type | Description |
|---|---|---|
work_type | Option < String > | Filter by work type. |
success | Option < bool > | Filter by success status. |
agent_id | Option < Uuid > | Filter by agent ID. |
limit | Option < i64 > | Limit number of results. |
Functions
brokkr-broker::api::v1::work_orders::routes
pub
#![allow(unused)]
fn main() {
fn routes () -> Router < DAL >
}
Creates and returns a router for work order-related endpoints.
Source
#![allow(unused)]
fn main() {
pub fn routes() -> Router<DAL> {
info!("Setting up work order routes");
Router::new()
// Work order management
.route(
"/work-orders",
get(list_work_orders).post(create_work_order),
)
.route(
"/work-orders/:id",
get(get_work_order).delete(delete_work_order),
)
// Claim and complete operations
.route("/work-orders/:id/claim", post(claim_work_order))
.route("/work-orders/:id/complete", post(complete_work_order))
// Work order log
.route("/work-order-log", get(list_work_order_log))
.route("/work-order-log/:id", get(get_work_order_log))
}
}
brokkr-broker::api::v1::work_orders::agent_routes
pub
#![allow(unused)]
fn main() {
fn agent_routes () -> Router < DAL >
}
Creates agent-specific routes for work order operations. These routes are nested under /agents/:id in the main router.
Source
#![allow(unused)]
fn main() {
pub fn agent_routes() -> Router<DAL> {
Router::new().route(
"/agents/:agent_id/work-orders/pending",
get(list_pending_for_agent),
)
}
}
brokkr-broker::api::v1::work_orders::default_retryable
private
#![allow(unused)]
fn main() {
fn default_retryable () -> bool
}
Source
#![allow(unused)]
fn main() {
fn default_retryable() -> bool {
true
}
}
brokkr-broker::api::v1::work_orders::list_work_orders
private
#![allow(unused)]
fn main() {
async fn list_work_orders (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Query (params) : Query < ListWorkOrdersQuery > ,) -> Result < Json < Vec < WorkOrder > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists all work orders.
Source
#![allow(unused)]
fn main() {
async fn list_work_orders(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Query(params): Query<ListWorkOrdersQuery>,
) -> Result<Json<Vec<WorkOrder>>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to list work orders");
if !auth_payload.admin {
warn!("Unauthorized attempt to list work orders");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
match dal
.work_orders()
.list_filtered(params.status.as_deref(), params.work_type.as_deref())
{
Ok(work_orders) => {
info!("Successfully retrieved {} work orders", work_orders.len());
Ok(Json(work_orders))
}
Err(e) => {
error!("Failed to fetch work orders: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch work orders"})),
))
}
}
}
}
brokkr-broker::api::v1::work_orders::create_work_order
private
#![allow(unused)]
fn main() {
async fn create_work_order (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Json (request) : Json < CreateWorkOrderRequest > ,) -> Result < (StatusCode , Json < WorkOrder >) , (StatusCode , Json < serde_json :: Value >) >
}
Creates a new work order.
Source
#![allow(unused)]
fn main() {
async fn create_work_order(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Json(request): Json<CreateWorkOrderRequest>,
) -> Result<(StatusCode, Json<WorkOrder>), (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to create a new work order");
if !auth_payload.admin {
warn!("Unauthorized attempt to create work order");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
// Extract targeting from either the new targeting field or deprecated target_agent_ids
let targeting = request.targeting.unwrap_or_default();
let legacy_agent_ids = request.target_agent_ids.unwrap_or_default();
// Combine agent IDs from both sources (for backwards compatibility)
let agent_ids: Vec<Uuid> = targeting
.agent_ids
.unwrap_or_default()
.into_iter()
.chain(legacy_agent_ids)
.collect();
let labels = targeting.labels.unwrap_or_default();
let annotations = targeting.annotations.unwrap_or_default();
// Validate that at least one targeting method is specified
if agent_ids.is_empty() && labels.is_empty() && annotations.is_empty() {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "At least one targeting method must be specified (agent_ids, labels, or annotations)"
})),
));
}
// Create the work order
let new_work_order = match NewWorkOrder::new(
request.work_type,
request.yaml_content,
request.max_retries,
request.backoff_seconds,
request.claim_timeout_seconds,
) {
Ok(wo) => wo,
Err(e) => {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": e})),
));
}
};
let work_order = match dal.work_orders().create(&new_work_order) {
Ok(wo) => wo,
Err(e) => {
error!("Failed to create work order: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to create work order"})),
));
}
};
// Add hard targets (agent IDs)
if !agent_ids.is_empty() {
if let Err(e) = dal.work_orders().add_targets(work_order.id, &agent_ids) {
error!("Failed to add work order targets: {:?}", e);
let _ = dal.work_orders().delete(work_order.id);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to add work order targets"})),
));
}
}
// Add labels
if !labels.is_empty() {
if let Err(e) = dal.work_orders().add_labels(work_order.id, &labels) {
error!("Failed to add work order labels: {:?}", e);
let _ = dal.work_orders().delete(work_order.id);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to add work order labels"})),
));
}
}
// Add annotations
if !annotations.is_empty() {
if let Err(e) = dal
.work_orders()
.add_annotations(work_order.id, &annotations)
{
error!("Failed to add work order annotations: {:?}", e);
let _ = dal.work_orders().delete(work_order.id);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to add work order annotations"})),
));
}
}
info!("Successfully created work order with ID: {}", work_order.id);
Ok((StatusCode::CREATED, Json(work_order)))
}
}
brokkr-broker::api::v1::work_orders::get_work_order
private
#![allow(unused)]
fn main() {
async fn get_work_order (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < WorkOrder > , (StatusCode , Json < serde_json :: Value >) >
}
Gets a work order by ID.
Source
#![allow(unused)]
fn main() {
async fn get_work_order(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<WorkOrder>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to get work order with ID: {}", id);
if !auth_payload.admin {
warn!("Unauthorized attempt to get work order");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
// First check the active work_orders table
match dal.work_orders().get(id) {
Ok(Some(work_order)) => {
info!("Successfully retrieved work order with ID: {}", id);
return Ok(Json(work_order));
}
Ok(None) => {
// Not in active table, check the log for completed work orders
debug!("Work order {} not in active table, checking log", id);
}
Err(e) => {
error!("Failed to fetch work order with ID {}: {:?}", id, e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch work order"})),
));
}
}
// Check the work order log for completed work orders
match dal.work_orders().get_log(id) {
Ok(Some(log_entry)) => {
info!(
"Successfully retrieved completed work order with ID: {} from log",
id
);
// Synthesize a WorkOrder from the log entry
let status = if log_entry.success {
"COMPLETED".to_string()
} else {
"FAILED".to_string()
};
// For failed work orders, populate last_error from result_message
let (last_error, last_error_at) = if !log_entry.success {
(
log_entry.result_message.clone(),
Some(log_entry.completed_at),
)
} else {
(None, None)
};
let work_order = WorkOrder {
id: log_entry.id,
created_at: log_entry.created_at,
updated_at: log_entry.completed_at, // Use completed_at as updated_at
work_type: log_entry.work_type,
yaml_content: log_entry.yaml_content,
status,
claimed_by: log_entry.claimed_by,
claimed_at: log_entry.claimed_at,
retry_count: log_entry.retries_attempted,
max_retries: 0, // Not available in log
next_retry_after: None,
backoff_seconds: 0, // Not available in log
claim_timeout_seconds: 0, // Not available in log
last_error,
last_error_at,
};
Ok(Json(work_order))
}
Ok(None) => {
warn!("Work order not found with ID: {}", id);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Work order not found"})),
))
}
Err(e) => {
error!("Failed to fetch work order log with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch work order"})),
))
}
}
}
}
brokkr-broker::api::v1::work_orders::delete_work_order
private
#![allow(unused)]
fn main() {
async fn delete_work_order (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < StatusCode , (StatusCode , Json < serde_json :: Value >) >
}
Deletes/cancels a work order.
Source
#![allow(unused)]
fn main() {
async fn delete_work_order(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to delete work order with ID: {}", id);
if !auth_payload.admin {
warn!("Unauthorized attempt to delete work order");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
match dal.work_orders().delete(id) {
Ok(0) => {
warn!("Work order not found with ID: {}", id);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Work order not found"})),
))
}
Ok(_) => {
info!("Successfully deleted work order with ID: {}", id);
Ok(StatusCode::NO_CONTENT)
}
Err(e) => {
error!("Failed to delete work order with ID {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to delete work order"})),
))
}
}
}
}
brokkr-broker::api::v1::work_orders::list_pending_for_agent
private
#![allow(unused)]
fn main() {
async fn list_pending_for_agent (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (agent_id) : Path < Uuid > , Query (params) : Query < ListPendingQuery > ,) -> Result < Json < Vec < WorkOrder > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists pending work orders claimable by a specific agent.
Source
#![allow(unused)]
fn main() {
async fn list_pending_for_agent(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(agent_id): Path<Uuid>,
Query(params): Query<ListPendingQuery>,
) -> Result<Json<Vec<WorkOrder>>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to list pending work orders for agent: {}",
agent_id
);
// Allow admin or the agent itself
if !auth_payload.admin && auth_payload.agent != Some(agent_id) {
warn!("Unauthorized attempt to list pending work orders for agent");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
match dal
.work_orders()
.list_pending_for_agent(agent_id, params.work_type.as_deref())
{
Ok(work_orders) => {
info!(
"Successfully retrieved {} pending work orders for agent {}",
work_orders.len(),
agent_id
);
Ok(Json(work_orders))
}
Err(e) => {
error!(
"Failed to fetch pending work orders for agent {}: {:?}",
agent_id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch pending work orders"})),
))
}
}
}
}
brokkr-broker::api::v1::work_orders::claim_work_order
private
#![allow(unused)]
fn main() {
async fn claim_work_order (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Json (request) : Json < ClaimWorkOrderRequest > ,) -> Result < Json < WorkOrder > , (StatusCode , Json < serde_json :: Value >) >
}
Claims a work order for an agent.
Source
#![allow(unused)]
fn main() {
async fn claim_work_order(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Json(request): Json<ClaimWorkOrderRequest>,
) -> Result<Json<WorkOrder>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to claim work order {} by agent {}",
id, request.agent_id
);
// Allow admin or the agent itself
if !auth_payload.admin && auth_payload.agent != Some(request.agent_id) {
warn!("Unauthorized attempt to claim work order");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
match dal.work_orders().claim(id, request.agent_id) {
Ok(work_order) => {
info!(
"Successfully claimed work order {} by agent {}",
id, request.agent_id
);
Ok(Json(work_order))
}
Err(diesel::result::Error::NotFound) => {
warn!(
"Work order {} not found or not claimable by agent {}",
id, request.agent_id
);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Work order not found or not claimable"})),
))
}
Err(e) => {
error!("Failed to claim work order {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to claim work order"})),
))
}
}
}
}
brokkr-broker::api::v1::work_orders::complete_work_order
private
#![allow(unused)]
fn main() {
async fn complete_work_order (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > , Json (request) : Json < CompleteWorkOrderRequest > ,) -> Result < (StatusCode , Json < serde_json :: Value >) , (StatusCode , Json < serde_json :: Value >) >
}
Completes a work order (success or failure).
On success, the work order is moved to the log. On failure, if retries remain, the work order is scheduled for retry. If max retries exceeded, the work order is moved to the log.
Source
#![allow(unused)]
fn main() {
async fn complete_work_order(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
Json(request): Json<CompleteWorkOrderRequest>,
) -> Result<(StatusCode, Json<serde_json::Value>), (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to complete work order {} (success: {})",
id, request.success
);
// Get the work order to verify authorization
let work_order = match dal.work_orders().get(id) {
Ok(Some(wo)) => wo,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Work order not found"})),
));
}
Err(e) => {
error!("Failed to fetch work order {}: {:?}", id, e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch work order"})),
));
}
};
// Allow admin or the agent that claimed the work order
if !auth_payload.admin && auth_payload.agent != work_order.claimed_by {
warn!("Unauthorized attempt to complete work order");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Access denied"})),
));
}
if request.success {
match dal.work_orders().complete_success(id, request.message) {
Ok(log_entry) => {
info!("Successfully completed work order {} with success", id);
Ok((StatusCode::OK, Json(serde_json::json!(log_entry))))
}
Err(e) => {
error!("Failed to complete work order {}: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to complete work order"})),
))
}
}
} else {
let error_message = request
.message
.unwrap_or_else(|| "Unknown error".to_string());
match dal
.work_orders()
.complete_failure(id, error_message, request.retryable)
{
Ok(Some(log_entry)) => {
if request.retryable {
info!(
"Work order {} failed and exceeded max retries, moved to log",
id
);
} else {
info!(
"Work order {} failed with non-retryable error, moved to log",
id
);
}
Ok((StatusCode::OK, Json(serde_json::json!(log_entry))))
}
Ok(None) => {
info!("Work order {} failed and scheduled for retry", id);
Ok((
StatusCode::ACCEPTED,
Json(serde_json::json!({"status": "retry_scheduled"})),
))
}
Err(e) => {
error!("Failed to complete work order {} with failure: {:?}", id, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to complete work order"})),
))
}
}
}
}
}
brokkr-broker::api::v1::work_orders::list_work_order_log
private
#![allow(unused)]
fn main() {
async fn list_work_order_log (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Query (params) : Query < ListLogQuery > ,) -> Result < Json < Vec < WorkOrderLog > > , (StatusCode , Json < serde_json :: Value >) >
}
Lists completed work orders from the log.
Source
#![allow(unused)]
fn main() {
async fn list_work_order_log(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Query(params): Query<ListLogQuery>,
) -> Result<Json<Vec<WorkOrderLog>>, (StatusCode, Json<serde_json::Value>)> {
info!("Handling request to list work order log");
if !auth_payload.admin {
warn!("Unauthorized attempt to list work order log");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
match dal.work_orders().list_log(
params.work_type.as_deref(),
params.success,
params.agent_id,
params.limit,
) {
Ok(log_entries) => {
info!(
"Successfully retrieved {} work order log entries",
log_entries.len()
);
Ok(Json(log_entries))
}
Err(e) => {
error!("Failed to fetch work order log: {:?}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch work order log"})),
))
}
}
}
}
brokkr-broker::api::v1::work_orders::get_work_order_log
private
#![allow(unused)]
fn main() {
async fn get_work_order_log (State (dal) : State < DAL > , Extension (auth_payload) : Extension < AuthPayload > , Path (id) : Path < Uuid > ,) -> Result < Json < WorkOrderLog > , (StatusCode , Json < serde_json :: Value >) >
}
Gets a completed work order from the log by ID.
Source
#![allow(unused)]
fn main() {
async fn get_work_order_log(
State(dal): State<DAL>,
Extension(auth_payload): Extension<AuthPayload>,
Path(id): Path<Uuid>,
) -> Result<Json<WorkOrderLog>, (StatusCode, Json<serde_json::Value>)> {
info!(
"Handling request to get work order log entry with ID: {}",
id
);
if !auth_payload.admin {
warn!("Unauthorized attempt to get work order log entry");
return Err((
StatusCode::FORBIDDEN,
Json(serde_json::json!({"error": "Admin access required"})),
));
}
match dal.work_orders().get_log(id) {
Ok(Some(log_entry)) => {
info!(
"Successfully retrieved work order log entry with ID: {}",
id
);
Ok(Json(log_entry))
}
Ok(None) => {
warn!("Work order log entry not found with ID: {}", id);
Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Work order log entry not found"})),
))
}
Err(e) => {
error!(
"Failed to fetch work order log entry with ID {}: {:?}",
id, e
);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch work order log entry"})),
))
}
}
}
}
brokkr-broker::bin Rust
Brokkr Broker CLI application
This module provides the command-line interface for the Brokkr Broker application. It includes functionality for serving the broker, rotating keys, and managing the application.
Functions
brokkr-broker::bin::main
private
async fn main () -> Result < () , Box < dyn std :: error :: Error > >
Main function to run the Brokkr Broker application
This function initializes the application, parses command-line arguments, and executes the appropriate command based on user input.
Source
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = parse_cli();
// Load configuration
let config = Settings::new(None).expect("Failed to load configuration");
// Initialize telemetry (includes tracing/logging setup)
let telemetry_config = config.telemetry.for_broker();
brokkr_utils::telemetry::init(&telemetry_config, &config.log.level, &config.log.format)
.expect("Failed to initialize telemetry");
// Create PAK controller
let _ =
utils::pak::create_pak_controller(Some(&config)).expect("Failed to create PAK controller");
// Execute the appropriate command
match cli.command {
Commands::Serve => commands::serve(&config).await?,
Commands::Create(create_commands) => match create_commands.command {
CreateSubcommands::Agent { name, cluster_name } => {
commands::create_agent(&config, name, cluster_name)?
}
CreateSubcommands::Generator { name, description } => {
commands::create_generator(&config, name, description)?
}
},
Commands::Rotate(rotate_commands) => match rotate_commands.command {
RotateSubcommands::Admin => commands::rotate_admin(&config)?,
RotateSubcommands::Agent { uuid } => commands::rotate_agent_key(&config, uuid)?,
RotateSubcommands::Generator { uuid } => commands::rotate_generator_key(&config, uuid)?,
},
}
// Shutdown telemetry on exit
brokkr_utils::telemetry::shutdown();
Ok(())
}
brokkr-broker::cli Rust
Structs
brokkr-broker::cli::Cli
pub
Derives: Parser
Brokkr Broker CLI
This CLI provides commands to manage the Brokkr Broker, including serving the broker, creating agents and generators, and rotating keys.
Fields
| Name | Type | Description |
|---|---|---|
command | Commands |
brokkr-broker::cli::CreateCommands
pub
Derives: Args
Fields
| Name | Type | Description |
|---|---|---|
command | CreateSubcommands |
brokkr-broker::cli::RotateCommands
pub
Derives: Args
Fields
| Name | Type | Description |
|---|---|---|
command | RotateSubcommands |
Enums
brokkr-broker::cli::Commands pub
Variants
Serve- Start the Brokkr Broker serverCreate- Create new entitiesRotate- Rotate keys
brokkr-broker::cli::CreateSubcommands pub
Variants
Agent- Create a new agentGenerator- Create a new generator
brokkr-broker::cli::RotateSubcommands pub
Variants
Agent- Rotate an agent keyGenerator- Rotate a generator keyAdmin- Rotate the admin key
Functions
brokkr-broker::cli::parse_cli
pub
#![allow(unused)]
fn main() {
fn parse_cli () -> Cli
}
Source
#![allow(unused)]
fn main() {
pub fn parse_cli() -> Cli {
Cli::parse()
}
}
brokkr-broker::cli::commands Rust
Structs
brokkr-broker::cli::commands::Count
private
Derives: QueryableByName, Debug
Fields
| Name | Type | Description |
|---|---|---|
count | i64 |
Functions
brokkr-broker::cli::commands::serve
pub
#![allow(unused)]
fn main() {
async fn serve (config : & Settings) -> Result < () , Box < dyn std :: error :: Error > >
}
Function to start the Brokkr Broker server
This function initializes the database, runs migrations, checks for first-time setup, configures API routes, and starts the server with graceful shutdown support.
Source
#![allow(unused)]
fn main() {
pub async fn serve(config: &Settings) -> Result<(), Box<dyn std::error::Error>> {
info!("Starting Brokkr Broker application");
// Create database connection pool
// Pool size needs to accommodate:
// - 5 background tasks (diagnostic cleanup, work order maintenance, webhook delivery/cleanup, audit cleanup)
// - HTTP requests (middleware holds 1 connection while DAL methods need another)
// - Concurrent request handling and webhook event emission
info!("Creating database connection pool");
let connection_pool = create_shared_connection_pool(
&config.database.url,
"brokkr",
50,
config.database.schema.as_deref(),
);
info!("Database connection pool created successfully");
// Set up schema if configured (for multi-tenant deployments)
if let Some(ref schema) = config.database.schema {
info!("Setting up schema: {}", schema);
connection_pool
.setup_schema(schema)
.expect("Failed to set up schema");
info!("Schema '{}' set up successfully", schema);
}
// Run pending migrations
info!("Running pending database migrations");
let mut conn = connection_pool.get().expect("Failed to get DB connection");
conn.run_pending_migrations(MIGRATIONS)
.expect("Failed to run migrations");
info!("Database migrations completed successfully");
// Check if this is the first time running the application
let is_first_run = conn
.transaction(|conn| {
let result: Count =
sql_query("SELECT COUNT(*) as count FROM app_initialization").get_result(conn)?;
if result.count == 0 {
// If it's the first run, insert a record into app_initialization
sql_query("INSERT INTO app_initialization DEFAULT VALUES").execute(conn)?;
Ok::<bool, DieselError>(true)
} else {
Ok::<bool, DieselError>(false)
}
})
.expect("Failed to check initialization status");
// Perform first-time setup if necessary
if is_first_run {
info!("First time application startup detected. Creating admin role...");
utils::first_startup(&mut conn, config)?;
} else {
info!("Existing application detected. Proceeding with normal startup.");
}
// Initialize Data Access Layer
info!("Initializing Data Access Layer");
let auth_cache_ttl = config.broker.auth_cache_ttl_seconds.unwrap_or(60);
let dal = DAL::new_with_auth_cache(connection_pool.clone(), auth_cache_ttl);
info!(
"Auth cache TTL: {}s ({})",
auth_cache_ttl,
if auth_cache_ttl > 0 {
"enabled"
} else {
"disabled"
}
);
// Initialize encryption key for webhooks
info!("Initializing encryption key");
utils::encryption::init_encryption_key(config.broker.webhook_encryption_key.as_deref())
.expect("Failed to initialize encryption key");
// Initialize audit logger for compliance tracking
info!("Initializing audit logger");
utils::audit::init_audit_logger(dal.clone()).expect("Failed to initialize audit logger");
// Start background tasks
info!("Starting background tasks");
let cleanup_config = utils::background_tasks::DiagnosticCleanupConfig {
interval_seconds: config
.broker
.diagnostic_cleanup_interval_seconds
.unwrap_or(900),
max_age_hours: config.broker.diagnostic_max_age_hours.unwrap_or(1),
};
utils::background_tasks::start_diagnostic_cleanup_task(dal.clone(), cleanup_config);
// Start work order maintenance task (retry processing and stale claim detection)
let work_order_config = utils::background_tasks::WorkOrderMaintenanceConfig::default();
utils::background_tasks::start_work_order_maintenance_task(dal.clone(), work_order_config);
// Start webhook delivery worker
let webhook_delivery_config = utils::background_tasks::WebhookDeliveryConfig {
interval_seconds: config.broker.webhook_delivery_interval_seconds.unwrap_or(5),
batch_size: config.broker.webhook_delivery_batch_size.unwrap_or(50),
};
utils::background_tasks::start_webhook_delivery_task(dal.clone(), webhook_delivery_config);
// Start webhook cleanup task
let webhook_cleanup_config = utils::background_tasks::WebhookCleanupConfig {
interval_seconds: 3600, // Every hour
retention_days: config.broker.webhook_cleanup_retention_days.unwrap_or(7),
};
utils::background_tasks::start_webhook_cleanup_task(dal.clone(), webhook_cleanup_config);
// Start audit log cleanup task
let audit_cleanup_config = utils::background_tasks::AuditLogCleanupConfig {
interval_seconds: 86400, // Daily
retention_days: config.broker.audit_log_retention_days.unwrap_or(90),
};
utils::background_tasks::start_audit_log_cleanup_task(dal.clone(), audit_cleanup_config);
// Create reloadable configuration for hot-reload support
info!("Initializing reloadable configuration");
let reloadable_config = ReloadableConfig::from_settings(config.clone(), None);
// Start ConfigMap watcher for Kubernetes hot-reload (if running in K8s)
if let Some(watcher_config) = utils::config_watcher::ConfigWatcherConfig::from_environment() {
utils::config_watcher::start_config_watcher(reloadable_config.clone(), watcher_config);
}
// Configure API routes
info!("Configuring API routes");
let app = api::configure_api_routes(dal.clone(), &config.cors, Some(reloadable_config))
.with_state(dal);
// Set up the server address
let addr = "0.0.0.0:3000";
info!("Starting server on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
// Set up shutdown signal handler
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
tokio::spawn(async move {
signal::ctrl_c().await.expect("Failed to listen for ctrl+c");
shutdown_tx.send(()).ok();
});
// Start the server with graceful shutdown
info!("Brokkr Broker is now running");
axum::serve(listener, app)
.with_graceful_shutdown(utils::shutdown(shutdown_rx))
.await?;
Ok(())
}
}
brokkr-broker::cli::commands::rotate_admin
pub
#![allow(unused)]
fn main() {
fn rotate_admin (config : & Settings) -> Result < () , Box < dyn std :: error :: Error > >
}
Function to rotate the admin key
This function generates a new admin key and updates it in the database.
Source
#![allow(unused)]
fn main() {
pub fn rotate_admin(config: &Settings) -> Result<(), Box<dyn std::error::Error>> {
info!("Rotating admin key");
// Create database connection
let mut conn = PgConnection::establish(&config.database.url)
.expect("Failed to establish database connection");
// Run the first_startup function to generate a new admin key
utils::upsert_admin(&mut conn, config)?;
info!("Admin key rotated successfully");
Ok(())
}
}
brokkr-broker::cli::commands::rotate_agent_key
pub
#![allow(unused)]
fn main() {
fn rotate_agent_key (config : & Settings , uuid : Uuid) -> Result < () , Box < dyn std :: error :: Error > >
}
Source
#![allow(unused)]
fn main() {
pub fn rotate_agent_key(config: &Settings, uuid: Uuid) -> Result<(), Box<dyn std::error::Error>> {
info!("Rotating agent key");
let pool = create_shared_connection_pool(
&config.database.url,
"brokkr",
1,
config.database.schema.as_deref(),
);
let dal = DAL::new(pool.clone());
let agent = dal.agents().get(uuid)?.ok_or("Agent not found")?;
let new_pak_hash = utils::pak::create_pak()?.1;
dal.agents().update_pak_hash(agent.id, new_pak_hash)?;
info!("Agent key rotated successfully for agent: {}", agent.name);
Ok(())
}
}
brokkr-broker::cli::commands::rotate_generator_key
pub
#![allow(unused)]
fn main() {
fn rotate_generator_key (config : & Settings , uuid : Uuid ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Source
#![allow(unused)]
fn main() {
pub fn rotate_generator_key(
config: &Settings,
uuid: Uuid,
) -> Result<(), Box<dyn std::error::Error>> {
info!("Rotating generator key");
let pool = create_shared_connection_pool(
&config.database.url,
"brokkr",
1,
config.database.schema.as_deref(),
);
let dal = DAL::new(pool.clone());
let generator = dal.generators().get(uuid)?.ok_or("Generator not found")?;
let new_pak_hash = utils::pak::create_pak()?.1;
dal.generators()
.update_pak_hash(generator.id, new_pak_hash)?;
info!(
"Generator key rotated successfully for generator: {}",
generator.name
);
Ok(())
}
}
brokkr-broker::cli::commands::create_agent
pub
#![allow(unused)]
fn main() {
fn create_agent (config : & Settings , name : String , cluster_name : String ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Source
#![allow(unused)]
fn main() {
pub fn create_agent(
config: &Settings,
name: String,
cluster_name: String,
) -> Result<(), Box<dyn std::error::Error>> {
info!("Creating new agent: {}", name);
// Use pool size 2 because agent creation emits webhook events
// which require a second connection while the first is still held
let pool = create_shared_connection_pool(
&config.database.url,
"brokkr",
2,
config.database.schema.as_deref(),
);
let dal = DAL::new(pool.clone());
let new_agent = NewAgent::new(name, cluster_name)
.map_err(|e| format!("Failed to create NewAgent: {}", e))?;
let (pak, pak_hash) = pak::create_pak()?;
let agent = dal.agents().create(&new_agent)?;
dal.agents().update_pak_hash(agent.id, pak_hash)?;
info!("Successfully created agent with ID: {}", agent.id);
println!("Agent created successfully:");
println!("ID: {}", agent.id);
println!("Name: {}", agent.name);
println!("Cluster: {}", agent.cluster_name);
println!("Initial PAK: {}", pak);
Ok(())
}
}
brokkr-broker::cli::commands::create_generator
pub
#![allow(unused)]
fn main() {
fn create_generator (config : & Settings , name : String , description : Option < String > ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Source
#![allow(unused)]
fn main() {
pub fn create_generator(
config: &Settings,
name: String,
description: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
info!("Creating new generator: {}", name);
let pool = create_shared_connection_pool(
&config.database.url,
"brokkr",
1,
config.database.schema.as_deref(),
);
let dal = DAL::new(pool.clone());
let new_generator = NewGenerator::new(name, description)
.map_err(|e| format!("Failed to create NewGenerator: {}", e))?;
let (pak, pak_hash) = pak::create_pak()?;
let generator = dal.generators().create(&new_generator)?;
dal.generators().update_pak_hash(generator.id, pak_hash)?;
info!("Successfully created generator with ID: {}", generator.id);
println!("Generator created successfully:");
println!("ID: {}", generator.id);
println!("Name: {}", generator.name);
println!("Initial PAK: {}", pak);
Ok(())
}
}
brokkr-broker::dal Rust
Structs
brokkr-broker::dal::DAL
pub
Derives: Clone
The main Data Access Layer struct.
This struct serves as the central point for database operations, managing a connection pool and providing access to specific DAL implementations for different entities.
Fields
| Name | Type | Description |
|---|---|---|
pool | ConnectionPool | A connection pool for PostgreSQL database connections with schema support. |
auth_cache | Option < Cache < String , AuthPayload > > | In-memory cache for PAK authentication results, keyed by PAK hash. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (pool : ConnectionPool) -> Self
}
Creates a new DAL instance with the given connection pool.
Parameters:
| Name | Type | Description |
|---|---|---|
pool | - | A connection pool for PostgreSQL database connections with schema support. |
Returns:
A new DAL instance.
Source
#![allow(unused)]
fn main() {
pub fn new(pool: ConnectionPool) -> Self {
DAL {
pool,
auth_cache: None,
}
}
}
new_with_auth_cache pub
#![allow(unused)]
fn main() {
fn new_with_auth_cache (pool : ConnectionPool , auth_cache_ttl_seconds : u64) -> Self
}
Creates a new DAL instance with an auth cache.
Parameters:
| Name | Type | Description |
|---|---|---|
pool | - | A connection pool for PostgreSQL database connections with schema support. |
auth_cache_ttl_seconds | - | TTL for cached auth results. 0 disables caching. |
Source
#![allow(unused)]
fn main() {
pub fn new_with_auth_cache(pool: ConnectionPool, auth_cache_ttl_seconds: u64) -> Self {
let auth_cache = if auth_cache_ttl_seconds > 0 {
Some(
Cache::builder()
.time_to_live(Duration::from_secs(auth_cache_ttl_seconds))
.max_capacity(10_000)
.build(),
)
} else {
None
};
DAL { pool, auth_cache }
}
}
invalidate_auth_cache pub
#![allow(unused)]
fn main() {
fn invalidate_auth_cache (& self , pak_hash : & str)
}
Invalidates a specific entry in the auth cache by PAK hash.
Source
#![allow(unused)]
fn main() {
pub fn invalidate_auth_cache(&self, pak_hash: &str) {
if let Some(cache) = &self.auth_cache {
cache.invalidate(pak_hash);
}
}
}
invalidate_all_auth_cache pub
#![allow(unused)]
fn main() {
fn invalidate_all_auth_cache (& self)
}
Invalidates all entries in the auth cache.
Source
#![allow(unused)]
fn main() {
pub fn invalidate_all_auth_cache(&self) {
if let Some(cache) = &self.auth_cache {
cache.invalidate_all();
}
}
}
agents pub
#![allow(unused)]
fn main() {
fn agents (& self) -> AgentsDAL < '_ >
}
Provides access to the Agents Data Access Layer.
Returns:
An instance of AgentsDAL.
Source
#![allow(unused)]
fn main() {
pub fn agents(&self) -> AgentsDAL<'_> {
AgentsDAL { dal: self }
}
}
agent_annotations pub
#![allow(unused)]
fn main() {
fn agent_annotations (& self) -> AgentAnnotationsDAL < '_ >
}
Provides access to the Agent Annotations Data Access Layer.
Returns:
An instance of AgentAnontationsDAL.
Source
#![allow(unused)]
fn main() {
pub fn agent_annotations(&self) -> AgentAnnotationsDAL<'_> {
AgentAnnotationsDAL { dal: self }
}
}
agent_events pub
#![allow(unused)]
fn main() {
fn agent_events (& self) -> AgentEventsDAL < '_ >
}
Provides access to the Agent Events Data Access Layer.
Returns:
An instance of AgentEventsDAL.
Source
#![allow(unused)]
fn main() {
pub fn agent_events(&self) -> AgentEventsDAL<'_> {
AgentEventsDAL { dal: self }
}
}
agent_labels pub
#![allow(unused)]
fn main() {
fn agent_labels (& self) -> AgentLabelsDAL < '_ >
}
Provides access to the Agent Labels Data Access Layer.
Returns:
An instance of AgentLabelsDAL.
Source
#![allow(unused)]
fn main() {
pub fn agent_labels(&self) -> AgentLabelsDAL<'_> {
AgentLabelsDAL { dal: self }
}
}
agent_targets pub
#![allow(unused)]
fn main() {
fn agent_targets (& self) -> AgentTargetsDAL < '_ >
}
Provides access to the Agent Targets Data Access Layer.
Returns:
An instance of AgentTargetssDAL.
Source
#![allow(unused)]
fn main() {
pub fn agent_targets(&self) -> AgentTargetsDAL<'_> {
AgentTargetsDAL { dal: self }
}
}
stack_labels pub
#![allow(unused)]
fn main() {
fn stack_labels (& self) -> StackLabelsDAL < '_ >
}
Provides access to the Stack Labels Data Access Layer.
Returns:
An instance of StackLabelsDAL.
Source
#![allow(unused)]
fn main() {
pub fn stack_labels(&self) -> StackLabelsDAL<'_> {
StackLabelsDAL { dal: self }
}
}
stack_annotations pub
#![allow(unused)]
fn main() {
fn stack_annotations (& self) -> StackAnnotationsDAL < '_ >
}
Provides access to the Stack Annotations Data Access Layer.
Returns:
An instance of StackAnontationsDAL.
Source
#![allow(unused)]
fn main() {
pub fn stack_annotations(&self) -> StackAnnotationsDAL<'_> {
StackAnnotationsDAL { dal: self }
}
}
stacks pub
#![allow(unused)]
fn main() {
fn stacks (& self) -> StacksDAL < '_ >
}
Provides access to the Stacks Data Access Layer.
Returns:
An instance of StacksDAL.
Source
#![allow(unused)]
fn main() {
pub fn stacks(&self) -> StacksDAL<'_> {
StacksDAL { dal: self }
}
}
deployment_health pub
#![allow(unused)]
fn main() {
fn deployment_health (& self) -> DeploymentHealthDAL < '_ >
}
Provides access to the Deployment Health Data Access Layer.
Returns:
An instance of DeploymentHealthDAL.
Source
#![allow(unused)]
fn main() {
pub fn deployment_health(&self) -> DeploymentHealthDAL<'_> {
DeploymentHealthDAL { dal: self }
}
}
deployment_objects pub
#![allow(unused)]
fn main() {
fn deployment_objects (& self) -> DeploymentObjectsDAL < '_ >
}
Provides access to the Deployment Objects Data Access Layer.
Returns:
An instance of DeploymentObjectsDAL.
Source
#![allow(unused)]
fn main() {
pub fn deployment_objects(&self) -> DeploymentObjectsDAL<'_> {
DeploymentObjectsDAL { dal: self }
}
}
generators pub
#![allow(unused)]
fn main() {
fn generators (& self) -> GeneratorsDAL < '_ >
}
Provides access to the Generators Data Access Layer.
Returns:
An instance of GeneratorsDal.
Source
#![allow(unused)]
fn main() {
pub fn generators(&self) -> GeneratorsDAL<'_> {
GeneratorsDAL { dal: self }
}
}
templates pub
#![allow(unused)]
fn main() {
fn templates (& self) -> TemplatesDAL < '_ >
}
Provides access to the Templates Data Access Layer.
Returns:
An instance of TemplatesDAL.
Source
#![allow(unused)]
fn main() {
pub fn templates(&self) -> TemplatesDAL<'_> {
TemplatesDAL { dal: self }
}
}
template_labels pub
#![allow(unused)]
fn main() {
fn template_labels (& self) -> TemplateLabelsDAL < '_ >
}
Provides access to the Template Labels Data Access Layer.
Returns:
An instance of TemplateLabelsDAL.
Source
#![allow(unused)]
fn main() {
pub fn template_labels(&self) -> TemplateLabelsDAL<'_> {
TemplateLabelsDAL { dal: self }
}
}
template_annotations pub
#![allow(unused)]
fn main() {
fn template_annotations (& self) -> TemplateAnnotationsDAL < '_ >
}
Provides access to the Template Annotations Data Access Layer.
Returns:
An instance of TemplateAnnotationsDAL.
Source
#![allow(unused)]
fn main() {
pub fn template_annotations(&self) -> TemplateAnnotationsDAL<'_> {
TemplateAnnotationsDAL { dal: self }
}
}
template_targets pub
#![allow(unused)]
fn main() {
fn template_targets (& self) -> TemplateTargetsDAL < '_ >
}
Provides access to the Template Targets Data Access Layer.
Returns:
An instance of TemplateTargetsDAL.
Source
#![allow(unused)]
fn main() {
pub fn template_targets(&self) -> TemplateTargetsDAL<'_> {
TemplateTargetsDAL { dal: self }
}
}
rendered_deployment_objects pub
#![allow(unused)]
fn main() {
fn rendered_deployment_objects (& self) -> RenderedDeploymentObjectsDAL < '_ >
}
Provides access to the Rendered Deployment Objects Data Access Layer.
Returns:
An instance of RenderedDeploymentObjectsDAL.
Source
#![allow(unused)]
fn main() {
pub fn rendered_deployment_objects(&self) -> RenderedDeploymentObjectsDAL<'_> {
RenderedDeploymentObjectsDAL { dal: self }
}
}
work_orders pub
#![allow(unused)]
fn main() {
fn work_orders (& self) -> WorkOrdersDAL < '_ >
}
Provides access to the Work Orders Data Access Layer.
Returns:
An instance of WorkOrdersDAL.
Source
#![allow(unused)]
fn main() {
pub fn work_orders(&self) -> WorkOrdersDAL<'_> {
WorkOrdersDAL { dal: self }
}
}
diagnostic_requests pub
#![allow(unused)]
fn main() {
fn diagnostic_requests (& self) -> DiagnosticRequestsDAL < '_ >
}
Provides access to the Diagnostic Requests Data Access Layer.
Returns:
An instance of DiagnosticRequestsDAL.
Source
#![allow(unused)]
fn main() {
pub fn diagnostic_requests(&self) -> DiagnosticRequestsDAL<'_> {
DiagnosticRequestsDAL { dal: self }
}
}
diagnostic_results pub
#![allow(unused)]
fn main() {
fn diagnostic_results (& self) -> DiagnosticResultsDAL < '_ >
}
Provides access to the Diagnostic Results Data Access Layer.
Returns:
An instance of DiagnosticResultsDAL.
Source
#![allow(unused)]
fn main() {
pub fn diagnostic_results(&self) -> DiagnosticResultsDAL<'_> {
DiagnosticResultsDAL { dal: self }
}
}
webhook_subscriptions pub
#![allow(unused)]
fn main() {
fn webhook_subscriptions (& self) -> WebhookSubscriptionsDAL < '_ >
}
Provides access to the Webhook Subscriptions Data Access Layer.
Returns:
An instance of WebhookSubscriptionsDAL.
Source
#![allow(unused)]
fn main() {
pub fn webhook_subscriptions(&self) -> WebhookSubscriptionsDAL<'_> {
WebhookSubscriptionsDAL { dal: self }
}
}
webhook_deliveries pub
#![allow(unused)]
fn main() {
fn webhook_deliveries (& self) -> WebhookDeliveriesDAL < '_ >
}
Provides access to the Webhook Deliveries Data Access Layer.
Returns:
An instance of WebhookDeliveriesDAL.
Source
#![allow(unused)]
fn main() {
pub fn webhook_deliveries(&self) -> WebhookDeliveriesDAL<'_> {
WebhookDeliveriesDAL { dal: self }
}
}
audit_logs pub
#![allow(unused)]
fn main() {
fn audit_logs (& self) -> AuditLogsDAL < '_ >
}
Provides access to the Audit Logs Data Access Layer.
Returns:
An instance of AuditLogsDAL.
Source
#![allow(unused)]
fn main() {
pub fn audit_logs(&self) -> AuditLogsDAL<'_> {
AuditLogsDAL { dal: self }
}
}
Enums
brokkr-broker::dal::DalError pub
Error types for DAL operations.
Variants
ConnectionPool- Failed to get a connection from the poolQuery- Database query errorNotFound- Resource not found
brokkr-broker::dal::FilterType pub
Variants
AndOr
brokkr-broker::dal::agent_annotations Rust
Data Access Layer for Agent Annotation operations.
This module provides functionality to interact with agent annotations in the database, including creating, retrieving, updating, and deleting annotations.
Structs
brokkr-broker::dal::agent_annotations::AgentAnnotationsDAL<’a>
pub
Handles database operations for Agent Annotations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::agent_events Rust
Data Access Layer for AgentEvent operations.
This module provides functionality to interact with the agent_events table in the database. It includes methods for creating, retrieving, updating, and deleting agent events, as well as listing events with various filtering options.
Structs
brokkr-broker::dal::agent_events::AgentEventsDAL<’a>
pub
Data Access Layer for AgentEvent operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::agent_labels Rust
Data Access Layer for AgentLabel operations.
This module provides functionality to interact with the agent_labels table in the database. It includes methods for creating, retrieving, listing, and deleting agent labels, as well as checking for label existence.
Structs
brokkr-broker::dal::agent_labels::AgentLabelsDAL<’a>
pub
Data Access Layer for AgentLabel operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::agent_targets Rust
Data Access Layer for AgentTarget operations.
This module provides functionality to interact with the agent_targets table in the database, allowing CRUD operations on AgentTarget entities.
Structs
brokkr-broker::dal::agent_targets::AgentTargetsDAL<’a>
pub
Handles database operations for AgentTarget entities.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::agents Rust
Structs
brokkr-broker::dal::agents::AgentFilter
pub
Struct for filtering agents based on various criteria.
Fields
| Name | Type | Description |
|---|---|---|
labels | Vec < String > | |
annotations | Vec < (String , String) > | |
agent_targets | Vec < Uuid > | |
filter_type | FilterType |
brokkr-broker::dal::agents::AgentsDAL<’a>
pub
Data Access Layer for Agent operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::audit_logs Rust
Data Access Layer for AuditLog operations.
This module provides functionality to interact with the audit_logs table. Audit logs are immutable - only create and query operations are supported.
Structs
brokkr-broker::dal::audit_logs::AuditLogsDAL<’a>
pub
Data Access Layer for AuditLog operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::deployment_health Rust
Data Access Layer for DeploymentHealth operations.
This module provides functionality to interact with the deployment_health table in the database. It includes methods for upserting health status, querying health by agent/deployment/stack, and aggregating health across deployments.
Structs
brokkr-broker::dal::deployment_health::DeploymentHealthDAL<’a>
pub
Data Access Layer for DeploymentHealth operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::deployment_objects Rust
Data Access Layer for DeploymentObject operations.
This module provides functionality to interact with deployment objects in the database, including creating, retrieving, listing, and soft-deleting deployment objects.
Structs
brokkr-broker::dal::deployment_objects::DeploymentObjectsDAL<’a>
pub
Data Access Layer for DeploymentObject operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::diagnostic_requests Rust
Data Access Layer for DiagnosticRequest operations.
This module provides functionality to interact with the diagnostic_requests table. It includes methods for creating, claiming, completing, and querying diagnostic requests.
Structs
brokkr-broker::dal::diagnostic_requests::DiagnosticRequestsDAL<’a>
pub
Data Access Layer for DiagnosticRequest operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::diagnostic_results Rust
Data Access Layer for DiagnosticResult operations.
This module provides functionality to interact with the diagnostic_results table. It includes methods for creating and querying diagnostic results.
Structs
brokkr-broker::dal::diagnostic_results::DiagnosticResultsDAL<’a>
pub
Data Access Layer for DiagnosticResult operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::generators Rust
Structs
brokkr-broker::dal::generators::GeneratorsDAL<’a>
pub
Data Access Layer for Generator operations.
This module provides a set of methods to interact with the generators table in the database. It includes operations for creating, retrieving, updating, and deleting generators, as well as specialized queries for filtering and updating specific fields.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::rendered_deployment_objects Rust
Data Access Layer for Rendered Deployment Object operations.
This module provides functionality to interact with the rendered_deployment_objects table in the database, tracking the provenance of deployment objects created from templates.
Structs
brokkr-broker::dal::rendered_deployment_objects::RenderedDeploymentObjectsDAL<’a>
pub
Handles database operations for RenderedDeploymentObject entities.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::stack_annotations Rust
Data Access Layer for Stack Annotation operations.
This module provides functionality to interact with stack annotations in the database, including creating, retrieving, updating, and deleting annotations.
Structs
brokkr-broker::dal::stack_annotations::StackAnnotationsDAL<’a>
pub
Handles database operations for Stack Annotations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::stack_labels Rust
Data Access Layer for Stack Label operations.
This module provides functionality to interact with stack labels in the database, including creating, retrieving, listing, and deleting labels.
Structs
brokkr-broker::dal::stack_labels::StackLabelsDAL<’a>
pub
Handles database operations for Stack Labels.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::stacks Rust
Data Access Layer for Stack operations.
This module provides functionality to interact with the database for Stack-related operations, including creating, retrieving, updating, and deleting stacks, as well as filtering stacks based on various criteria.
Structs
brokkr-broker::dal::stacks::StacksDAL<’a>
pub
Data Access Layer for Stack operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::template_annotations Rust
Data Access Layer for Template Annotation operations.
This module provides functionality to interact with template annotations in the database, including creating, retrieving, listing, and deleting annotations.
Structs
brokkr-broker::dal::template_annotations::TemplateAnnotationsDAL<’a>
pub
Handles database operations for Template Annotations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::template_labels Rust
Data Access Layer for Template Label operations.
This module provides functionality to interact with template labels in the database, including creating, retrieving, listing, and deleting labels.
Structs
brokkr-broker::dal::template_labels::TemplateLabelsDAL<’a>
pub
Handles database operations for Template Labels.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::template_targets Rust
Data Access Layer for TemplateTarget operations.
This module provides functionality to interact with the template_targets table in the database, allowing CRUD operations on TemplateTarget entities.
Structs
brokkr-broker::dal::template_targets::TemplateTargetsDAL<’a>
pub
Handles database operations for TemplateTarget entities.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::templates Rust
Data Access Layer for Stack Template operations.
This module provides functionality to interact with the database for Stack Template-related operations, including creating, retrieving, listing, and managing template versions.
Structs
brokkr-broker::dal::templates::TemplatesDAL<’a>
pub
Data Access Layer for Stack Template operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::webhook_deliveries Rust
Data Access Layer for WebhookDelivery operations.
This module provides functionality to interact with the webhook_deliveries table. It includes methods for creating deliveries, claiming with TTL, processing pending deliveries, recording attempts, and cleaning up old records.
Structs
brokkr-broker::dal::webhook_deliveries::WebhookDeliveriesDAL<’a>
pub
Data Access Layer for WebhookDelivery operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::dal::webhook_deliveries::DeliveryStats
pub
Derives: Debug, Default, Clone
Statistics about webhook deliveries.
Fields
| Name | Type | Description |
|---|---|---|
pending | i64 | Number of pending deliveries. |
acquired | i64 | Number of acquired deliveries (in progress). |
success | i64 | Number of successful deliveries. |
failed | i64 | Number of failed deliveries (retrying). |
dead | i64 | Number of dead deliveries (max retries exceeded). |
brokkr-broker::dal::webhook_subscriptions Rust
Data Access Layer for WebhookSubscription operations.
This module provides functionality to interact with the webhook_subscriptions table. It includes methods for creating, updating, deleting, and querying webhook subscriptions.
Structs
brokkr-broker::dal::webhook_subscriptions::WebhookSubscriptionsDAL<’a>
pub
Data Access Layer for WebhookSubscription operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
Functions
brokkr-broker::dal::webhook_subscriptions::matches_event_pattern
private
#![allow(unused)]
fn main() {
fn matches_event_pattern (pattern : & str , event_type : & str) -> bool
}
Matches an event type against a pattern.
Patterns support:
- Exact match: “health.degraded” matches “health.degraded”
- Wildcard suffix: “health.*” matches “health.degraded”, “health.failing”, etc.
- Full wildcard: “*” matches everything
Source
#![allow(unused)]
fn main() {
fn matches_event_pattern(pattern: &str, event_type: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(prefix) = pattern.strip_suffix(".*") {
return event_type.starts_with(prefix) && event_type[prefix.len()..].starts_with('.');
}
pattern == event_type
}
}
brokkr-broker::dal::work_orders Rust
Data Access Layer for WorkOrder operations.
This module provides functionality to interact with the work_orders, work_order_log, and work_order_targets tables in the database. It includes methods for creating, claiming, completing, and managing work orders through their lifecycle.
Structs
brokkr-broker::dal::work_orders::WorkOrdersDAL<’a>
pub
Data Access Layer for WorkOrder operations.
Fields
| Name | Type | Description |
|---|---|---|
dal | & 'a DAL | Reference to the main DAL instance. |
brokkr-broker::db Rust
Database connection pool management using diesel and r2d2.
For detailed documentation, see the Brokkr Documentation.
Structs
brokkr-broker::db::ConnectionPool
pub
Derives: Clone
Represents a pool of PostgreSQL database connections.
Fields
| Name | Type | Description |
|---|---|---|
pool | Pool < ConnectionManager < PgConnection > > | The actual connection pool. |
schema | Option < String > | Optional schema name for multi-tenant deployments. |
Methods
get pub
#![allow(unused)]
fn main() {
fn get (& self ,) -> Result < diesel :: r2d2 :: PooledConnection < ConnectionManager < PgConnection > > , r2d2 :: Error >
}
Gets a connection from the pool with automatic schema search_path configuration.
If a schema is configured, this method automatically executes SET search_path on the
connection to ensure all queries execute in the correct schema context.
Returns:
Returns a pooled connection ready for use.
Raises:
| Exception | Description |
|---|---|
Panic | This method will panic if: |
Panic | Unable to get a connection from the pool |
Panic | The schema name is invalid |
Panic | Failed to set the search path |
Source
#![allow(unused)]
fn main() {
pub fn get(
&self,
) -> Result<diesel::r2d2::PooledConnection<ConnectionManager<PgConnection>>, r2d2::Error> {
use diesel::prelude::*;
let mut conn = self.pool.get()?;
if let Some(ref schema) = self.schema {
// Validate schema name to prevent SQL injection
validate_schema_name(schema).expect("Invalid schema name");
// Set search_path for this connection
let sql = format!("SET search_path TO {}, public", schema);
diesel::sql_query(&sql)
.execute(&mut conn)
.expect("Failed to set search_path");
}
Ok(conn)
}
}
setup_schema pub
#![allow(unused)]
fn main() {
fn setup_schema (& self , schema : & str) -> Result < () , String >
}
Sets up a PostgreSQL schema for multi-tenant isolation.
This method creates the schema if it doesn’t exist and prepares it for migrations. It should be called during application startup before running migrations.
Parameters:
| Name | Type | Description |
|---|---|---|
schema | - | The schema name to set up |
Returns:
Returns Ok(()) on success, or an error if schema setup fails.
Source
#![allow(unused)]
fn main() {
pub fn setup_schema(&self, schema: &str) -> Result<(), String> {
use diesel::prelude::*;
// Validate schema name
validate_schema_name(schema).map_err(|e| format!("Invalid schema name: {}", e))?;
let mut conn = self
.pool
.get()
.map_err(|e| format!("Failed to get connection: {}", e))?;
// Create schema if it doesn't exist
let create_schema_sql = format!("CREATE SCHEMA IF NOT EXISTS {}", schema);
diesel::sql_query(&create_schema_sql)
.execute(&mut conn)
.map_err(|e| format!("Failed to create schema '{}': {}", schema, e))?;
// Set search_path for subsequent operations
let set_search_path_sql = format!("SET search_path TO {}, public", schema);
diesel::sql_query(&set_search_path_sql)
.execute(&mut conn)
.map_err(|e| format!("Failed to set search path: {}", e))?;
Ok(())
}
}
Functions
brokkr-broker::db::create_shared_connection_pool
pub
#![allow(unused)]
fn main() {
fn create_shared_connection_pool (base_url : & str , database_name : & str , max_size : u32 , schema : Option < & str > ,) -> ConnectionPool
}
Creates a shared connection pool for PostgreSQL databases.
Parameters:
| Name | Type | Description |
|---|---|---|
base_url | - | The base URL of the database server (e.g., “postgres://username:password@localhost:5432”) |
database_name | - | The name of the database to connect to |
max_size | - | The maximum number of connections the pool should maintain |
schema | - | Optional schema name for multi-tenant isolation |
Returns:
Returns a ConnectionPool instance containing the created connection pool.
Raises:
| Exception | Description |
|---|---|
Panic | This function will panic if: |
Panic | The base URL is invalid |
Panic | The connection pool creation fails |
Source
#![allow(unused)]
fn main() {
pub fn create_shared_connection_pool(
base_url: &str,
database_name: &str,
max_size: u32,
schema: Option<&str>,
) -> ConnectionPool {
// Parse the base URL and set the database name
let mut url = Url::parse(base_url).expect("Invalid base URL");
url.set_path(database_name);
// Create a connection manager
let manager = ConnectionManager::<PgConnection>::new(url.as_str());
// Build the connection pool
let pool = Pool::builder()
.max_size(max_size)
.build(manager)
.expect("Failed to create connection pool");
ConnectionPool {
pool,
schema: schema.map(String::from),
}
}
}
brokkr-broker::db::validate_schema_name
pub
#![allow(unused)]
fn main() {
fn validate_schema_name (schema : & str) -> Result < () , String >
}
Validates a PostgreSQL schema name to prevent SQL injection.
Schema names must start with a letter and contain only alphanumeric characters and underscores.
Parameters:
| Name | Type | Description |
|---|---|---|
schema | - | The schema name to validate |
Returns:
Returns Ok(()) if valid, or an error message if invalid.
Source
#![allow(unused)]
fn main() {
pub fn validate_schema_name(schema: &str) -> Result<(), String> {
if schema.is_empty() {
return Err("Schema name cannot be empty".to_string());
}
// Check first character is a letter
if !schema.chars().next().unwrap().is_ascii_alphabetic() {
return Err("Schema name must start with a letter".to_string());
}
// Check all characters are alphanumeric or underscore
if !schema
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return Err("Schema name can only contain letters, numbers, and underscores".to_string());
}
Ok(())
}
}
brokkr-broker::metrics Rust
Functions
brokkr-broker::metrics::init
pub
#![allow(unused)]
fn main() {
fn init ()
}
Initializes all metrics by forcing lazy static evaluation
This ensures all metric definitions are registered with the registry before attempting to encode/export them. Should be called once at startup.
Source
#![allow(unused)]
fn main() {
pub fn init() {
// Force initialization of all lazy static metrics by accessing them
let _ = &*HTTP_REQUESTS_TOTAL;
let _ = &*HTTP_REQUEST_DURATION_SECONDS;
let _ = &*DATABASE_QUERIES_TOTAL;
let _ = &*DATABASE_QUERY_DURATION_SECONDS;
let _ = &*ACTIVE_AGENTS;
let _ = &*AGENT_HEARTBEAT_AGE_SECONDS;
let _ = &*STACKS_TOTAL;
let _ = &*DEPLOYMENT_OBJECTS_TOTAL;
}
}
brokkr-broker::metrics::encode_metrics
pub
#![allow(unused)]
fn main() {
fn encode_metrics () -> String
}
Encodes all registered metrics in Prometheus text format
Returns:
Returns a String containing all metrics in Prometheus exposition format
Source
#![allow(unused)]
fn main() {
pub fn encode_metrics() -> String {
// Ensure all metrics are initialized before encoding
init();
let encoder = TextEncoder::new();
let metric_families = REGISTRY.gather();
let mut buffer = vec![];
encoder
.encode(&metric_families, &mut buffer)
.expect("Failed to encode metrics");
String::from_utf8(buffer).expect("Failed to convert metrics to UTF-8")
}
}
brokkr-broker::metrics::record_http_request
pub
#![allow(unused)]
fn main() {
fn record_http_request (endpoint : & str , method : & str , status : u16 , duration_seconds : f64)
}
Records an HTTP request metric
Parameters:
| Name | Type | Description |
|---|---|---|
endpoint | - | The request path/endpoint |
method | - | The HTTP method (GET, POST, etc.) |
status | - | The response status code |
duration_seconds | - | The request duration in seconds |
Source
#![allow(unused)]
fn main() {
pub fn record_http_request(endpoint: &str, method: &str, status: u16, duration_seconds: f64) {
// Normalize endpoint to avoid high cardinality from path parameters
let normalized_endpoint = normalize_endpoint(endpoint);
let status_str = status.to_string();
HTTP_REQUESTS_TOTAL
.with_label_values(&[&normalized_endpoint, method, &status_str])
.inc();
HTTP_REQUEST_DURATION_SECONDS
.with_label_values(&[&normalized_endpoint, method])
.observe(duration_seconds);
}
}
brokkr-broker::metrics::normalize_endpoint
private
#![allow(unused)]
fn main() {
fn normalize_endpoint (path : & str) -> String
}
Normalizes an endpoint path to reduce cardinality Replaces UUIDs and numeric IDs with placeholders
Source
#![allow(unused)]
fn main() {
fn normalize_endpoint(path: &str) -> String {
let parts: Vec<&str> = path.split('/').collect();
let normalized: Vec<String> = parts
.iter()
.map(|part| {
// Check if it's a UUID (36 chars with hyphens) or purely numeric
if (part.len() == 36 && part.chars().filter(|c| *c == '-').count() == 4)
|| (!part.is_empty() && part.chars().all(|c| c.is_ascii_digit()))
{
":id".to_string()
} else {
(*part).to_string()
}
})
.collect();
normalized.join("/")
}
}
brokkr-broker::metrics::record_db_query
pub
#![allow(unused)]
fn main() {
fn record_db_query (query_type : & str , duration_seconds : f64)
}
Records a database query metric
Parameters:
| Name | Type | Description |
|---|---|---|
query_type | - | The type of query (select, insert, update, delete) |
duration_seconds | - | The query duration in seconds |
Source
#![allow(unused)]
fn main() {
pub fn record_db_query(query_type: &str, duration_seconds: f64) {
DATABASE_QUERIES_TOTAL
.with_label_values(&[query_type])
.inc();
DATABASE_QUERY_DURATION_SECONDS
.with_label_values(&[query_type])
.observe(duration_seconds);
}
}
brokkr-broker::metrics::set_active_agents
pub
#![allow(unused)]
fn main() {
fn set_active_agents (count : i64)
}
Updates the active agents gauge
Source
#![allow(unused)]
fn main() {
pub fn set_active_agents(count: i64) {
ACTIVE_AGENTS.set(count);
}
}
brokkr-broker::metrics::set_stacks_total
pub
#![allow(unused)]
fn main() {
fn set_stacks_total (count : i64)
}
Updates the total stacks gauge
Source
#![allow(unused)]
fn main() {
pub fn set_stacks_total(count: i64) {
STACKS_TOTAL.set(count);
}
}
brokkr-broker::metrics::set_deployment_objects_total
pub
#![allow(unused)]
fn main() {
fn set_deployment_objects_total (count : i64)
}
Updates the total deployment objects gauge
Source
#![allow(unused)]
fn main() {
pub fn set_deployment_objects_total(count: i64) {
DEPLOYMENT_OBJECTS_TOTAL.set(count);
}
}
brokkr-broker::metrics::set_agent_heartbeat_age
pub
#![allow(unused)]
fn main() {
fn set_agent_heartbeat_age (agent_id : & str , agent_name : & str , age_seconds : f64)
}
Updates the heartbeat age for a specific agent
Source
#![allow(unused)]
fn main() {
pub fn set_agent_heartbeat_age(agent_id: &str, agent_name: &str, age_seconds: f64) {
AGENT_HEARTBEAT_AGE_SECONDS
.with_label_values(&[agent_id, agent_name])
.set(age_seconds);
}
}
brokkr-broker::utils Rust
Utility functions and structures for the Brokkr broker.
This module contains various helper functions and structures used throughout the broker, including admin key management and shutdown procedures.
Structs
brokkr-broker::utils::AdminKey
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone
Represents an admin key in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | |
created_at | chrono :: DateTime < Utc > | |
updated_at | chrono :: DateTime < Utc > | |
pak_hash | String |
brokkr-broker::utils::NewAdminKey
pub
Derives: Insertable
Represents a new admin key to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
pak_hash | String |
Functions
brokkr-broker::utils::shutdown
pub
#![allow(unused)]
fn main() {
async fn shutdown (shutdown_rx : oneshot :: Receiver < () >)
}
Handles the shutdown process for the broker.
This function waits for a shutdown signal and then performs cleanup tasks.
Source
#![allow(unused)]
fn main() {
pub async fn shutdown(shutdown_rx: oneshot::Receiver<()>) {
let _ = shutdown_rx.await;
// Remove the temporary key file
let _ = fs::remove_file("/tmp/key.txt");
}
}
brokkr-broker::utils::first_startup
pub
#![allow(unused)]
fn main() {
fn first_startup (conn : & mut PgConnection , config : & Settings ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Performs first-time startup operations.
This function is called when the broker starts for the first time and sets up the initial admin key.
Source
#![allow(unused)]
fn main() {
pub fn first_startup(
conn: &mut PgConnection,
config: &Settings,
) -> Result<(), Box<dyn std::error::Error>> {
upsert_admin(conn, config)
}
}
brokkr-broker::utils::create_pak
private
#![allow(unused)]
fn main() {
fn create_pak () -> Result < (String , String) , Box < dyn std :: error :: Error > >
}
Creates a new PAK (Privileged Access Key) and its hash.
This function generates a new PAK and returns both the key and its hash.
Source
#![allow(unused)]
fn main() {
fn create_pak() -> Result<(String, String), Box<dyn std::error::Error>> {
// Generate PAK and hash using the PAK controller
let controller = pak::create_pak_controller(None);
controller
.unwrap()
.try_generate_key_and_hash()
.map(|(pak, hash)| (pak.to_string(), hash))
.map_err(|e| e.into())
}
}
brokkr-broker::utils::upsert_admin
pub
#![allow(unused)]
fn main() {
fn upsert_admin (conn : & mut PgConnection , config : & Settings ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Updates or inserts the admin key and related generator.
This function creates or updates the admin key in the database, creates or updates the associated admin generator, and writes the PAK to a temporary file.
Source
#![allow(unused)]
fn main() {
pub fn upsert_admin(
conn: &mut PgConnection,
config: &Settings,
) -> Result<(), Box<dyn std::error::Error>> {
let pak_hash = match &config.broker.pak_hash {
Some(hash) if !hash.is_empty() => {
// Validate the provided hash
if !validate_pak_hash(hash) {
return Err("Invalid PAK hash provided in configuration".into());
}
hash.clone()
}
_ => {
// Generate new PAK and hash
let (pak, hash) = create_pak()?;
// Write PAK to temporary file
info!("Writing PAK to temporary file");
let key_path = Path::new("/tmp/brokkr-keys/key.txt");
fs::create_dir_all(key_path.parent().unwrap())?;
fs::write(key_path, pak)?;
hash
}
};
// Update or insert admin key
let existing_admin_key = admin_role::table
.select(admin_role::id)
.first::<Uuid>(conn)
.optional()?;
match existing_admin_key {
Some(id) => {
diesel::update(admin_role::table.find(id))
.set(admin_role::pak_hash.eq(&pak_hash))
.execute(conn)?;
}
None => {
diesel::insert_into(admin_role::table)
.values(&NewAdminKey {
pak_hash: pak_hash.clone(),
})
.execute(conn)?;
}
}
// Update or insert admin generator
use brokkr_models::schema::generators;
let existing_admin_generator = generators::table
.filter(generators::name.eq("admin-generator"))
.select(generators::id)
.first::<Uuid>(conn)
.optional()?;
match existing_admin_generator {
Some(id) => {
diesel::update(generators::table.find(id))
.set((
generators::pak_hash.eq(&pak_hash),
generators::description.eq("Linked to Admin PAK"),
))
.execute(conn)?;
}
None => {
diesel::insert_into(generators::table)
.values((
generators::name.eq("admin-generator"),
generators::description.eq("Linked to Admin PAK"),
generators::pak_hash.eq(&pak_hash),
))
.execute(conn)?;
}
}
Ok(())
}
}
brokkr-broker::utils::validate_pak_hash
private
#![allow(unused)]
fn main() {
fn validate_pak_hash (hash : & str) -> bool
}
Source
#![allow(unused)]
fn main() {
fn validate_pak_hash(hash: &str) -> bool {
// Implement hash validation logic here
// For example, check if it's a valid SHA-256 hash
hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit())
}
}
brokkr-broker::utils::audit Rust
Async Audit Logger for Brokkr.
This module provides an asynchronous audit logging service that buffers audit entries and writes them to the database in batches to avoid impacting request latency.
Structs
brokkr-broker::utils::audit::AuditLoggerConfig
pub
Derives: Debug, Clone
Configuration for the audit logger.
Fields
| Name | Type | Description |
|---|---|---|
channel_size | usize | Channel buffer size. |
batch_size | usize | Maximum batch size for writes. |
flush_interval_ms | u64 | Flush interval in milliseconds. |
brokkr-broker::utils::audit::AuditLogger
pub
Derives: Clone
The async audit logger for buffering and batching audit entries.
Fields
| Name | Type | Description |
|---|---|---|
sender | mpsc :: Sender < NewAuditLog > | Sender for emitting audit entries. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (dal : DAL) -> Self
}
Creates a new audit logger and starts the background writer.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The Data Access Layer for database operations. |
Returns:
An AuditLogger instance.
Source
#![allow(unused)]
fn main() {
pub fn new(dal: DAL) -> Self {
Self::with_config(dal, AuditLoggerConfig::default())
}
}
with_config pub
#![allow(unused)]
fn main() {
fn with_config (dal : DAL , config : AuditLoggerConfig) -> Self
}
Creates a new audit logger with custom configuration.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The Data Access Layer for database operations. |
config | - | The logger configuration. |
Returns:
An AuditLogger instance.
Source
#![allow(unused)]
fn main() {
pub fn with_config(dal: DAL, config: AuditLoggerConfig) -> Self {
let (sender, receiver) = mpsc::channel(config.channel_size);
// Start the background writer task
start_audit_writer(dal, receiver, config.batch_size, config.flush_interval_ms);
info!(
"Audit logger started (buffer: {}, batch: {}, flush: {}ms)",
config.channel_size, config.batch_size, config.flush_interval_ms
);
Self { sender }
}
}
log pub
#![allow(unused)]
fn main() {
fn log (& self , entry : NewAuditLog)
}
Logs an audit entry asynchronously (non-blocking).
If the channel is full, the entry will be dropped and an error logged.
Parameters:
| Name | Type | Description |
|---|---|---|
entry | - | The audit log entry to record. |
Source
#![allow(unused)]
fn main() {
pub fn log(&self, entry: NewAuditLog) {
let sender = self.sender.clone();
let action = entry.action.clone();
tokio::spawn(async move {
match sender.send(entry).await {
Ok(_) => {
debug!("Audit entry queued: {}", action);
}
Err(e) => {
error!(
"Failed to queue audit entry (action: {}): channel full or closed - {}",
action, e
);
}
}
});
}
}
log_async pub
async
#![allow(unused)]
fn main() {
async fn log_async (& self , entry : NewAuditLog ,) -> Result < () , mpsc :: error :: SendError < NewAuditLog > >
}
Logs an audit entry, waiting for it to be accepted.
Parameters:
| Name | Type | Description |
|---|---|---|
entry | - | The audit log entry to record. |
Returns:
Ok if the entry was accepted, Err if the channel is closed.
Source
#![allow(unused)]
fn main() {
pub async fn log_async(
&self,
entry: NewAuditLog,
) -> Result<(), mpsc::error::SendError<NewAuditLog>> {
let action = entry.action.clone();
self.sender.send(entry).await.map_err(|e| {
error!("Failed to queue audit entry (action: {}): {}", action, e);
e
})?;
debug!("Audit entry queued (async): {}", action);
Ok(())
}
}
try_log pub
#![allow(unused)]
fn main() {
fn try_log (& self , entry : NewAuditLog) -> bool
}
Tries to log an audit entry without blocking.
Parameters:
| Name | Type | Description |
|---|---|---|
entry | - | The audit log entry to record. |
Returns:
true if the entry was queued, false if the channel is full.
Source
#![allow(unused)]
fn main() {
pub fn try_log(&self, entry: NewAuditLog) -> bool {
match self.sender.try_send(entry) {
Ok(_) => true,
Err(mpsc::error::TrySendError::Full(_)) => {
warn!("Audit log channel full, entry dropped");
false
}
Err(mpsc::error::TrySendError::Closed(_)) => {
error!("Audit log channel closed");
false
}
}
}
}
Functions
brokkr-broker::utils::audit::init_audit_logger
pub
#![allow(unused)]
fn main() {
fn init_audit_logger (dal : DAL) -> Result < () , String >
}
Initializes the global audit logger.
This should be called once during broker startup.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The Data Access Layer for database operations. |
Returns:
Ok(()) if initialization succeeded, Err if already initialized.
Source
#![allow(unused)]
fn main() {
pub fn init_audit_logger(dal: DAL) -> Result<(), String> {
init_audit_logger_with_config(dal, AuditLoggerConfig::default())
}
}
brokkr-broker::utils::audit::init_audit_logger_with_config
pub
#![allow(unused)]
fn main() {
fn init_audit_logger_with_config (dal : DAL , config : AuditLoggerConfig) -> Result < () , String >
}
Initializes the global audit logger with custom configuration.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The Data Access Layer for database operations. |
config | - | The logger configuration. |
Returns:
Ok(()) if initialization succeeded, Err if already initialized.
Source
#![allow(unused)]
fn main() {
pub fn init_audit_logger_with_config(dal: DAL, config: AuditLoggerConfig) -> Result<(), String> {
let logger = AuditLogger::with_config(dal, config);
AUDIT_LOGGER
.set(Arc::new(logger))
.map_err(|_| "Audit logger already initialized".to_string())
}
}
brokkr-broker::utils::audit::get_audit_logger
pub
#![allow(unused)]
fn main() {
fn get_audit_logger () -> Option < Arc < AuditLogger > >
}
Gets the global audit logger.
Returns:
The audit logger, or None if not initialized.
Source
#![allow(unused)]
fn main() {
pub fn get_audit_logger() -> Option<Arc<AuditLogger>> {
AUDIT_LOGGER.get().cloned()
}
}
brokkr-broker::utils::audit::log
pub
#![allow(unused)]
fn main() {
fn log (entry : NewAuditLog)
}
Logs an audit entry to the global audit logger.
This is a convenience function for logging without needing to get the logger directly.
Parameters:
| Name | Type | Description |
|---|---|---|
entry | - | The audit log entry to record. |
Source
#![allow(unused)]
fn main() {
pub fn log(entry: NewAuditLog) {
if let Some(logger) = get_audit_logger() {
logger.log(entry);
} else {
warn!(
"Audit logger not initialized, entry dropped: {}",
entry.action
);
}
}
}
brokkr-broker::utils::audit::try_log
pub
#![allow(unused)]
fn main() {
fn try_log (entry : NewAuditLog) -> bool
}
Tries to log an audit entry without blocking.
Parameters:
| Name | Type | Description |
|---|---|---|
entry | - | The audit log entry to record. |
Returns:
true if logged, false if channel full or logger not initialized.
Source
#![allow(unused)]
fn main() {
pub fn try_log(entry: NewAuditLog) -> bool {
if let Some(logger) = get_audit_logger() {
logger.try_log(entry)
} else {
warn!(
"Audit logger not initialized, entry dropped: {}",
entry.action
);
false
}
}
}
brokkr-broker::utils::audit::start_audit_writer
private
#![allow(unused)]
fn main() {
fn start_audit_writer (dal : DAL , mut receiver : mpsc :: Receiver < NewAuditLog > , batch_size : usize , flush_interval_ms : u64 ,)
}
Starts the background audit writer task.
This task receives audit entries from the channel and writes them to the database in batches for efficiency.
Source
#![allow(unused)]
fn main() {
fn start_audit_writer(
dal: DAL,
mut receiver: mpsc::Receiver<NewAuditLog>,
batch_size: usize,
flush_interval_ms: u64,
) {
tokio::spawn(async move {
info!("Audit writer started");
let mut buffer: Vec<NewAuditLog> = Vec::with_capacity(batch_size);
let mut ticker = interval(Duration::from_millis(flush_interval_ms));
loop {
tokio::select! {
// Receive new entries
Some(entry) = receiver.recv() => {
buffer.push(entry);
// Flush if buffer is full
if buffer.len() >= batch_size {
flush_buffer(&dal, &mut buffer);
}
}
// Periodic flush
_ = ticker.tick() => {
if !buffer.is_empty() {
flush_buffer(&dal, &mut buffer);
}
}
// Channel closed
else => {
// Final flush before shutdown
if !buffer.is_empty() {
flush_buffer(&dal, &mut buffer);
}
warn!("Audit writer stopped - channel closed");
break;
}
}
}
});
}
}
brokkr-broker::utils::audit::flush_buffer
private
#![allow(unused)]
fn main() {
fn flush_buffer (dal : & DAL , buffer : & mut Vec < NewAuditLog >)
}
Flushes the buffer to the database.
Source
#![allow(unused)]
fn main() {
fn flush_buffer(dal: &DAL, buffer: &mut Vec<NewAuditLog>) {
if buffer.is_empty() {
return;
}
let count = buffer.len();
match dal.audit_logs().create_batch(buffer) {
Ok(inserted) => {
debug!("Flushed {} audit entries to database", inserted);
}
Err(e) => {
error!(
"Failed to flush {} audit entries to database: {:?}",
count, e
);
// Don't lose the entries - they'll be retried on next flush
// Actually, we should clear the buffer anyway to prevent infinite retries
// Log the actions that failed
for entry in buffer.iter() {
error!(
"Lost audit entry: {} ({})",
entry.action, entry.resource_type
);
}
}
}
buffer.clear();
}
}
brokkr-broker::utils::audit::log_action
pub
#![allow(unused)]
fn main() {
fn log_action (actor_type : & str , actor_id : Option < uuid :: Uuid > , action : & str , resource_type : & str , resource_id : Option < uuid :: Uuid > , details : Option < serde_json :: Value > , ip_address : Option < String > , user_agent : Option < String > ,)
}
Helper to create and log an audit entry in one call.
Parameters:
| Name | Type | Description |
|---|---|---|
actor_type | - | Type of actor (admin, agent, generator, system). |
actor_id | - | ID of the actor. |
action | - | The action performed. |
resource_type | - | Type of resource affected. |
resource_id | - | ID of the affected resource. |
details | - | Optional additional details. |
ip_address | - | Optional client IP address. |
user_agent | - | Optional client user agent. |
Source
#![allow(unused)]
fn main() {
pub fn log_action(
actor_type: &str,
actor_id: Option<uuid::Uuid>,
action: &str,
resource_type: &str,
resource_id: Option<uuid::Uuid>,
details: Option<serde_json::Value>,
ip_address: Option<String>,
user_agent: Option<String>,
) {
match NewAuditLog::new(actor_type, actor_id, action, resource_type, resource_id) {
Ok(mut entry) => {
if let Some(d) = details {
entry = entry.with_details(d);
}
if let Some(ip) = ip_address {
entry = entry.with_ip_address(ip);
}
if let Some(ua) = user_agent {
entry = entry.with_user_agent(ua);
}
log(entry);
}
Err(e) => {
error!("Failed to create audit entry: {}", e);
}
}
}
}
brokkr-broker::utils::background_tasks Rust
Background tasks for the Brokkr Broker.
This module contains background tasks that run periodically to maintain system health and cleanup expired data.
Structs
brokkr-broker::utils::background_tasks::DiagnosticCleanupConfig
pub
Configuration for diagnostic cleanup task.
Fields
| Name | Type | Description |
|---|---|---|
interval_seconds | u64 | How often to run the cleanup (in seconds). |
max_age_hours | i64 | Maximum age for completed/expired diagnostics before deletion (in hours). |
brokkr-broker::utils::background_tasks::WorkOrderMaintenanceConfig
pub
Configuration for work order maintenance task.
Fields
| Name | Type | Description |
|---|---|---|
interval_seconds | u64 | How often to run the maintenance (in seconds). |
brokkr-broker::utils::background_tasks::WebhookDeliveryConfig
pub
Configuration for webhook delivery worker.
Fields
| Name | Type | Description |
|---|---|---|
interval_seconds | u64 | How often to poll for pending deliveries (in seconds). |
batch_size | i64 | Maximum number of deliveries to process per interval. |
brokkr-broker::utils::background_tasks::WebhookCleanupConfig
pub
Configuration for webhook cleanup task.
Fields
| Name | Type | Description |
|---|---|---|
interval_seconds | u64 | How often to run the cleanup (in seconds). |
retention_days | i64 | Number of days to retain completed/dead deliveries. |
brokkr-broker::utils::background_tasks::AuditLogCleanupConfig
pub
Configuration for audit log cleanup task.
Fields
| Name | Type | Description |
|---|---|---|
interval_seconds | u64 | How often to run the cleanup (in seconds). |
retention_days | i64 | Number of days to retain audit logs. |
Functions
brokkr-broker::utils::background_tasks::start_diagnostic_cleanup_task
pub
#![allow(unused)]
fn main() {
fn start_diagnostic_cleanup_task (dal : DAL , config : DiagnosticCleanupConfig)
}
Starts the diagnostic cleanup background task.
This task periodically:
- Expires pending diagnostic requests that have passed their expiry time
- Deletes old completed/expired/failed diagnostic requests and their results
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The Data Access Layer instance |
config | - | Configuration for the cleanup task |
Source
#![allow(unused)]
fn main() {
pub fn start_diagnostic_cleanup_task(dal: DAL, config: DiagnosticCleanupConfig) {
info!(
"Starting diagnostic cleanup task (interval: {}s, max_age: {}h)",
config.interval_seconds, config.max_age_hours
);
tokio::spawn(async move {
let mut ticker = interval(Duration::from_secs(config.interval_seconds));
loop {
ticker.tick().await;
// Expire pending requests that have passed their expiry time
match dal.diagnostic_requests().expire_old_requests() {
Ok(expired) => {
if expired > 0 {
info!("Expired {} pending diagnostic requests", expired);
}
}
Err(e) => {
error!("Failed to expire diagnostic requests: {:?}", e);
}
}
// Delete old completed/expired/failed requests (cascades to results)
match dal
.diagnostic_requests()
.cleanup_old_requests(config.max_age_hours)
{
Ok(deleted) => {
if deleted > 0 {
info!(
"Cleaned up {} old diagnostic requests (age > {}h)",
deleted, config.max_age_hours
);
}
}
Err(e) => {
error!("Failed to cleanup old diagnostic requests: {:?}", e);
}
}
}
});
}
}
brokkr-broker::utils::background_tasks::start_work_order_maintenance_task
pub
#![allow(unused)]
fn main() {
fn start_work_order_maintenance_task (dal : DAL , config : WorkOrderMaintenanceConfig)
}
Starts the work order maintenance background task.
This task periodically:
- Moves RETRY_PENDING work orders back to PENDING when their backoff has elapsed
- Reclaims stale CLAIMED work orders that have timed out
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The Data Access Layer instance |
config | - | Configuration for the maintenance task |
Source
#![allow(unused)]
fn main() {
pub fn start_work_order_maintenance_task(dal: DAL, config: WorkOrderMaintenanceConfig) {
info!(
"Starting work order maintenance task (interval: {}s)",
config.interval_seconds
);
tokio::spawn(async move {
let mut ticker = interval(Duration::from_secs(config.interval_seconds));
loop {
ticker.tick().await;
// Process RETRY_PENDING work orders whose backoff has elapsed
match dal.work_orders().process_retry_pending() {
Ok(count) => {
if count > 0 {
info!("Reset {} RETRY_PENDING work orders to PENDING", count);
}
}
Err(e) => {
error!("Failed to process retry pending work orders: {:?}", e);
}
}
// Reclaim stale CLAIMED work orders
match dal.work_orders().process_stale_claims() {
Ok(count) => {
if count > 0 {
info!("Released {} stale claimed work orders", count);
}
}
Err(e) => {
error!("Failed to process stale claims: {:?}", e);
}
}
}
});
}
}
brokkr-broker::utils::background_tasks::start_webhook_delivery_task
pub
#![allow(unused)]
fn main() {
fn start_webhook_delivery_task (dal : DAL , config : WebhookDeliveryConfig)
}
Starts the webhook delivery worker background task.
This task periodically:
- Releases expired acquired deliveries back to pending
- Moves failed deliveries with elapsed backoff back to pending
- Claims pending deliveries for broker (target_labels is NULL)
- Attempts to deliver each via HTTP POST
- Marks deliveries as success or failure (with retry scheduling)
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The Data Access Layer instance |
config | - | Configuration for the delivery worker |
Source
#![allow(unused)]
fn main() {
pub fn start_webhook_delivery_task(dal: DAL, config: WebhookDeliveryConfig) {
info!(
"Starting webhook delivery worker (interval: {}s, batch_size: {})",
config.interval_seconds, config.batch_size
);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client");
tokio::spawn(async move {
let mut ticker = interval(Duration::from_secs(config.interval_seconds));
loop {
ticker.tick().await;
// First, release any expired acquired deliveries
match dal.webhook_deliveries().release_expired() {
Ok(count) => {
if count > 0 {
debug!("Released {} expired webhook delivery claims", count);
}
}
Err(e) => {
error!("Failed to release expired webhook deliveries: {:?}", e);
}
}
// Move failed deliveries back to pending if retry time has elapsed
match dal.webhook_deliveries().process_retries() {
Ok(count) => {
if count > 0 {
debug!(
"Moved {} webhook deliveries from failed to pending for retry",
count
);
}
}
Err(e) => {
error!("Failed to process webhook retries: {:?}", e);
}
}
// Claim pending broker deliveries (target_labels is NULL)
let deliveries = match dal
.webhook_deliveries()
.claim_for_broker(config.batch_size, None)
{
Ok(d) => d,
Err(e) => {
error!("Failed to claim pending webhook deliveries: {:?}", e);
continue;
}
};
if deliveries.is_empty() {
continue;
}
debug!("Processing {} claimed webhook deliveries", deliveries.len());
for delivery in deliveries {
// Get the subscription to retrieve URL and auth header
let subscription = match dal.webhook_subscriptions().get(delivery.subscription_id) {
Ok(Some(sub)) => sub,
Ok(None) => {
warn!(
"Subscription {} not found for delivery {}, marking as dead",
delivery.subscription_id, delivery.id
);
let _ = dal.webhook_deliveries().mark_failed(
delivery.id,
"Subscription not found",
0, // Force dead
);
continue;
}
Err(e) => {
error!(
"Failed to get subscription {} for delivery {}: {:?}",
delivery.subscription_id, delivery.id, e
);
continue;
}
};
// Decrypt URL and auth header
let url = match super::encryption::decrypt_string(&subscription.url_encrypted) {
Ok(u) => u,
Err(e) => {
error!(
"Failed to decrypt URL for subscription {}: {}",
subscription.id, e
);
let _ = dal.webhook_deliveries().mark_failed(
delivery.id,
&format!("Failed to decrypt URL: {}", e),
0,
);
continue;
}
};
let auth_header = subscription
.auth_header_encrypted
.as_ref()
.map(|encrypted| super::encryption::decrypt_string(encrypted))
.transpose();
let auth_header = match auth_header {
Ok(h) => h,
Err(e) => {
error!(
"Failed to decrypt auth header for subscription {}: {}",
subscription.id, e
);
let _ = dal.webhook_deliveries().mark_failed(
delivery.id,
&format!("Failed to decrypt auth header: {}", e),
0,
);
continue;
}
};
// Attempt delivery
let result =
attempt_delivery(&client, &url, auth_header.as_deref(), &delivery.payload)
.await;
match result {
Ok(_) => match dal.webhook_deliveries().mark_success(delivery.id) {
Ok(_) => {
debug!(
"Webhook delivery {} succeeded for subscription {}",
delivery.id, subscription.id
);
}
Err(e) => {
error!(
"Failed to mark delivery {} as success: {:?}",
delivery.id, e
);
}
},
Err(error) => {
match dal.webhook_deliveries().mark_failed(
delivery.id,
&error,
subscription.max_retries,
) {
Ok(updated) => {
if updated.status == "dead" {
warn!(
"Webhook delivery {} dead after {} attempts: {}",
delivery.id, updated.attempts, error
);
} else {
debug!(
"Webhook delivery {} failed (attempt {}), will retry: {}",
delivery.id, updated.attempts, error
);
}
}
Err(e) => {
error!(
"Failed to mark delivery {} as failed: {:?}",
delivery.id, e
);
}
}
}
}
}
}
});
}
}
brokkr-broker::utils::background_tasks::attempt_delivery
private
#![allow(unused)]
fn main() {
async fn attempt_delivery (client : & reqwest :: Client , url : & str , auth_header : Option < & str > , payload : & str ,) -> Result < () , String >
}
Attempts to deliver a webhook payload via HTTP POST.
Source
#![allow(unused)]
fn main() {
async fn attempt_delivery(
client: &reqwest::Client,
url: &str,
auth_header: Option<&str>,
payload: &str,
) -> Result<(), String> {
let mut request = client
.post(url)
.header("Content-Type", "application/json")
.body(payload.to_string());
if let Some(auth) = auth_header {
request = request.header("Authorization", auth);
}
let response = request
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
let status = response.status();
if status.is_success() {
Ok(())
} else {
let body = response.text().await.unwrap_or_default();
Err(format!(
"HTTP {}: {}",
status,
body.chars().take(200).collect::<String>()
))
}
}
}
brokkr-broker::utils::background_tasks::start_webhook_cleanup_task
pub
#![allow(unused)]
fn main() {
fn start_webhook_cleanup_task (dal : DAL , config : WebhookCleanupConfig)
}
Starts the webhook cleanup background task.
This task periodically deletes old completed/dead deliveries based on the retention policy.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The Data Access Layer instance |
config | - | Configuration for the cleanup task |
Source
#![allow(unused)]
fn main() {
pub fn start_webhook_cleanup_task(dal: DAL, config: WebhookCleanupConfig) {
info!(
"Starting webhook cleanup task (interval: {}s, retention: {}d)",
config.interval_seconds, config.retention_days
);
tokio::spawn(async move {
let mut ticker = interval(Duration::from_secs(config.interval_seconds));
loop {
ticker.tick().await;
match dal.webhook_deliveries().cleanup_old(config.retention_days) {
Ok(deleted) => {
if deleted > 0 {
info!(
"Cleaned up {} old webhook deliveries (age > {}d)",
deleted, config.retention_days
);
}
}
Err(e) => {
error!("Failed to cleanup old webhook deliveries: {:?}", e);
}
}
}
});
}
}
brokkr-broker::utils::background_tasks::start_audit_log_cleanup_task
pub
#![allow(unused)]
fn main() {
fn start_audit_log_cleanup_task (dal : DAL , config : AuditLogCleanupConfig)
}
Starts the audit log cleanup background task.
This task periodically deletes old audit log entries based on the configured retention policy.
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The Data Access Layer instance |
config | - | Configuration for the cleanup task |
Source
#![allow(unused)]
fn main() {
pub fn start_audit_log_cleanup_task(dal: DAL, config: AuditLogCleanupConfig) {
info!(
"Starting audit log cleanup task (interval: {}s, retention: {}d)",
config.interval_seconds, config.retention_days
);
tokio::spawn(async move {
let mut ticker = interval(Duration::from_secs(config.interval_seconds));
loop {
ticker.tick().await;
match dal.audit_logs().cleanup_old_logs(config.retention_days) {
Ok(deleted) => {
if deleted > 0 {
info!(
"Cleaned up {} old audit logs (age > {}d)",
deleted, config.retention_days
);
}
}
Err(e) => {
error!("Failed to cleanup old audit logs: {:?}", e);
}
}
}
});
}
}
brokkr-broker::utils::config_watcher Rust
Configuration file watcher for hot-reload support.
This module provides functionality to watch for changes to the broker’s configuration file and trigger configuration reloads automatically.
Structs
brokkr-broker::utils::config_watcher::ConfigWatcherConfig
pub
Derives: Debug, Clone
Configuration for the file watcher.
Fields
| Name | Type | Description |
|---|---|---|
config_file_path | String | Path to the configuration file to watch. |
debounce_duration | Duration | Debounce duration to prevent rapid successive reloads. |
enabled | bool | Whether the watcher is enabled. |
Methods
from_environment pub
#![allow(unused)]
fn main() {
fn from_environment () -> Option < Self >
}
Creates a new ConfigWatcherConfig from environment variables.
Looks for BROKKR_CONFIG_FILE environment variable to determine the config file path. If not set, returns None (watcher disabled).
Source
#![allow(unused)]
fn main() {
pub fn from_environment() -> Option<Self> {
// Check if config file path is specified
let config_file_path = match std::env::var("BROKKR_CONFIG_FILE") {
Ok(path) if !path.is_empty() => path,
_ => {
debug!("BROKKR_CONFIG_FILE not set, config file watcher disabled");
return None;
}
};
// Verify the file exists
if !Path::new(&config_file_path).exists() {
warn!(
"Config file '{}' does not exist, config file watcher disabled",
config_file_path
);
return None;
}
// Check if watcher is explicitly disabled
let enabled = std::env::var("BROKKR_CONFIG_WATCHER_ENABLED")
.map(|v| v.to_lowercase() != "false" && v != "0")
.unwrap_or(true);
if !enabled {
info!("Config file watcher explicitly disabled via environment variable");
return None;
}
// Get debounce duration from environment (in seconds)
let debounce_secs = std::env::var("BROKKR_CONFIG_WATCHER_DEBOUNCE_SECONDS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5);
Some(Self {
config_file_path,
debounce_duration: Duration::from_secs(debounce_secs),
enabled: true,
})
}
}
Functions
brokkr-broker::utils::config_watcher::start_config_watcher
pub
#![allow(unused)]
fn main() {
fn start_config_watcher (config : ReloadableConfig , watcher_config : ConfigWatcherConfig ,) -> Option < tokio :: task :: JoinHandle < () > >
}
Starts the configuration file watcher as a background task.
This function spawns a tokio task that watches for changes to the specified configuration file and triggers configuration reloads with debouncing.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | The ReloadableConfig instance to reload on changes. |
watcher_config | - | Configuration for the watcher. |
Returns:
A handle to the spawned task, or None if the watcher couldn’t be started.
Source
#![allow(unused)]
fn main() {
pub fn start_config_watcher(
config: ReloadableConfig,
watcher_config: ConfigWatcherConfig,
) -> Option<tokio::task::JoinHandle<()>> {
if !watcher_config.enabled {
info!("Config file watcher is disabled");
return None;
}
info!(
"Starting config file watcher for '{}' with {}s debounce",
watcher_config.config_file_path,
watcher_config.debounce_duration.as_secs()
);
let handle = tokio::spawn(async move {
if let Err(e) = run_config_watcher(config, watcher_config).await {
error!("Config file watcher error: {}", e);
}
});
Some(handle)
}
}
brokkr-broker::utils::config_watcher::run_config_watcher
private
#![allow(unused)]
fn main() {
async fn run_config_watcher (config : ReloadableConfig , watcher_config : ConfigWatcherConfig ,) -> Result < () , Box < dyn std :: error :: Error + Send + Sync > >
}
Internal function that runs the configuration file watcher loop.
Source
#![allow(unused)]
fn main() {
async fn run_config_watcher(
config: ReloadableConfig,
watcher_config: ConfigWatcherConfig,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config_path = watcher_config.config_file_path.clone();
let debounce_duration = watcher_config.debounce_duration;
// Create a channel for file events
let (tx, rx) = mpsc::channel();
// Create a file watcher
let mut watcher: RecommendedWatcher =
notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
if let Ok(event) = res {
// Only send for modify/create events
if event.kind.is_modify() || event.kind.is_create() {
let _ = tx.send(());
}
}
})?;
// Watch the config file's parent directory (some editors replace files atomically)
let config_path_ref = Path::new(&config_path);
let watch_path = config_path_ref.parent().unwrap_or(config_path_ref);
watcher.watch(watch_path, RecursiveMode::NonRecursive)?;
info!("Config file watcher started for '{}'", config_path);
// Track last reload time for debouncing
let mut last_reload: Option<Instant> = None;
// Process events
loop {
// Block waiting for events with a timeout
match rx.recv_timeout(Duration::from_secs(60)) {
Ok(()) => {
// Check debounce
let should_reload = match last_reload {
Some(last) => last.elapsed() >= debounce_duration,
None => true,
};
if should_reload {
// Wait for debounce period to catch rapid successive changes
tokio::time::sleep(debounce_duration).await;
// Drain any additional events that came in
while rx.try_recv().is_ok() {}
debug!("Config file change detected, reloading...");
last_reload = Some(Instant::now());
// Perform the reload
match config.reload() {
Ok(changes) => {
if changes.is_empty() {
debug!("Config file changed but no configuration changes detected");
} else {
info!(
"Config file watcher triggered configuration reload with {} change(s):",
changes.len()
);
for change in &changes {
info!(
" - {}: '{}' -> '{}'",
change.key, change.old_value, change.new_value
);
}
}
}
Err(e) => {
error!("Failed to reload configuration from file change: {}", e);
}
}
} else {
debug!(
"Debouncing config file change (last reload {}ms ago)",
last_reload.map(|l| l.elapsed().as_millis()).unwrap_or(0)
);
}
}
Err(mpsc::RecvTimeoutError::Timeout) => {
// No events, continue watching
continue;
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
warn!("Config file watcher channel disconnected");
break;
}
}
}
Ok(())
}
}
brokkr-broker::utils::encryption Rust
Encryption utilities for protecting sensitive data at rest.
This module provides AES-256-GCM encryption and decryption functionality for webhook URLs and authentication headers stored in the database.
Structs
brokkr-broker::utils::encryption::EncryptionKey
pub
Encryption key wrapper with AES-256-GCM cipher.
Fields
| Name | Type | Description |
|---|---|---|
key | [u8 ; 32] | The raw 32-byte key. |
cipher | Aes256Gcm | Pre-initialized AES-256-GCM cipher |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (key : [u8 ; 32]) -> Self
}
Creates a new encryption key from raw bytes.
Source
#![allow(unused)]
fn main() {
pub fn new(key: [u8; 32]) -> Self {
let cipher = Aes256Gcm::new_from_slice(&key).expect("valid key size");
Self { key, cipher }
}
}
generate pub
#![allow(unused)]
fn main() {
fn generate () -> Self
}
Creates a new random encryption key.
Source
#![allow(unused)]
fn main() {
pub fn generate() -> Self {
let mut key = [0u8; 32];
rand::thread_rng().fill_bytes(&mut key);
Self::new(key)
}
}
from_hex pub
#![allow(unused)]
fn main() {
fn from_hex (hex : & str) -> Result < Self , String >
}
Creates a key from a hex-encoded string.
Source
#![allow(unused)]
fn main() {
pub fn from_hex(hex: &str) -> Result<Self, String> {
let bytes = hex::decode(hex).map_err(|e| format!("Invalid hex encoding: {}", e))?;
if bytes.len() != 32 {
return Err(format!("Key must be 32 bytes, got {} bytes", bytes.len()));
}
let mut key = [0u8; 32];
key.copy_from_slice(&bytes);
Ok(Self::new(key))
}
}
fingerprint pub
#![allow(unused)]
fn main() {
fn fingerprint (& self) -> String
}
Returns the key as a hex string (for logging key fingerprint only).
Source
#![allow(unused)]
fn main() {
pub fn fingerprint(&self) -> String {
let hash = Sha256::digest(self.key);
hex::encode(&hash[..8])
}
}
encrypt pub
#![allow(unused)]
fn main() {
fn encrypt (& self , plaintext : & [u8]) -> Result < Vec < u8 > , EncryptionError >
}
Encrypts data using AES-256-GCM.
Source
#![allow(unused)]
fn main() {
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, EncryptionError> {
// Generate random nonce
let mut nonce_bytes = [0u8; AES_GCM_NONCE_SIZE];
rand::thread_rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
// Encrypt with AES-256-GCM
let ciphertext = self
.cipher
.encrypt(nonce, plaintext)
.map_err(|_| EncryptionError::EncryptionFailed)?;
// Build output: version || nonce || ciphertext (includes auth tag)
let mut output = Vec::with_capacity(1 + AES_GCM_NONCE_SIZE + ciphertext.len());
output.push(VERSION_AES_GCM);
output.extend_from_slice(&nonce_bytes);
output.extend(ciphertext);
Ok(output)
}
}
decrypt pub
#![allow(unused)]
fn main() {
fn decrypt (& self , data : & [u8]) -> Result < Vec < u8 > , EncryptionError >
}
Decrypts data, automatically detecting the encryption version.
Supports:
- Version 0x01: AES-256-GCM
- Version 0x00 or no version byte: Legacy XOR (for migration)
Source
#![allow(unused)]
fn main() {
pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, EncryptionError> {
if data.is_empty() {
return Err(EncryptionError::InvalidData("Empty data".to_string()));
}
// Check version byte
let version = data[0];
match version {
VERSION_AES_GCM => self.decrypt_aes_gcm(&data[1..]),
VERSION_LEGACY_XOR => self.decrypt_legacy_xor(&data[1..]),
_ => {
// No version byte - assume legacy XOR format
// Legacy format: nonce (16 bytes) || ciphertext
if data.len() >= LEGACY_XOR_NONCE_SIZE {
self.decrypt_legacy_xor(data)
} else {
Err(EncryptionError::InvalidData("Data too short".to_string()))
}
}
}
}
}
decrypt_aes_gcm private
#![allow(unused)]
fn main() {
fn decrypt_aes_gcm (& self , data : & [u8]) -> Result < Vec < u8 > , EncryptionError >
}
Decrypts AES-256-GCM encrypted data.
Source
#![allow(unused)]
fn main() {
fn decrypt_aes_gcm(&self, data: &[u8]) -> Result<Vec<u8>, EncryptionError> {
if data.len() < AES_GCM_NONCE_SIZE {
return Err(EncryptionError::InvalidData(
"Ciphertext too short (missing nonce)".to_string(),
));
}
let (nonce_bytes, ciphertext) = data.split_at(AES_GCM_NONCE_SIZE);
let nonce = Nonce::from_slice(nonce_bytes);
self.cipher
.decrypt(nonce, ciphertext)
.map_err(|_| EncryptionError::DecryptionFailed)
}
}
decrypt_legacy_xor private
#![allow(unused)]
fn main() {
fn decrypt_legacy_xor (& self , data : & [u8]) -> Result < Vec < u8 > , EncryptionError >
}
Decrypts legacy XOR-encrypted data (for migration support).
Source
#![allow(unused)]
fn main() {
fn decrypt_legacy_xor(&self, data: &[u8]) -> Result<Vec<u8>, EncryptionError> {
if data.len() < LEGACY_XOR_NONCE_SIZE {
return Err(EncryptionError::InvalidData(
"Legacy ciphertext too short (missing nonce)".to_string(),
));
}
// Extract nonce and actual ciphertext
let nonce = &data[..LEGACY_XOR_NONCE_SIZE];
let encrypted = &data[LEGACY_XOR_NONCE_SIZE..];
// Derive same mask using SHA-256
let mut hasher = Sha256::new();
hasher.update(self.key);
hasher.update(nonce);
let mask = hasher.finalize();
// XOR to decrypt
let plaintext: Vec<u8> = encrypted
.iter()
.enumerate()
.map(|(i, &b)| b ^ mask[i % mask.len()])
.collect();
Ok(plaintext)
}
}
Enums
brokkr-broker::utils::encryption::EncryptionError pub
Encryption error types
Variants
EncryptionFailed- Encryption operation failedDecryptionFailed- Decryption operation failed (wrong key or corrupted data)InvalidData- Invalid data formatUnsupportedVersion- Unsupported encryption version
Functions
brokkr-broker::utils::encryption::init_encryption_key
pub
#![allow(unused)]
fn main() {
fn init_encryption_key (key_hex : Option < & str >) -> Result < () , String >
}
Initializes the global encryption key from configuration.
This should be called once during broker startup.
Parameters:
| Name | Type | Description |
|---|---|---|
key_hex | - | Optional hex-encoded 32-byte key. If None, a random key is generated. |
Returns:
Ok(()) if initialization succeeded, Err if already initialized or key is invalid.
Source
#![allow(unused)]
fn main() {
pub fn init_encryption_key(key_hex: Option<&str>) -> Result<(), String> {
let key = match key_hex {
Some(hex) if !hex.is_empty() => {
info!("Initializing encryption key from configuration");
EncryptionKey::from_hex(hex)?
}
_ => {
warn!(
"No encryption key configured, generating random key. \
Configure BROKKR__BROKER__WEBHOOK_ENCRYPTION_KEY for production use."
);
EncryptionKey::generate()
}
};
info!("Encryption key fingerprint: {}", key.fingerprint());
ENCRYPTION_KEY
.set(Arc::new(key))
.map_err(|_| "Encryption key already initialized".to_string())
}
}
brokkr-broker::utils::encryption::get_encryption_key
pub
#![allow(unused)]
fn main() {
fn get_encryption_key () -> Arc < EncryptionKey >
}
Gets the global encryption key.
Raises:
| Exception | Description |
|---|---|
Panic | Panics if called before init_encryption_key(). |
Source
#![allow(unused)]
fn main() {
pub fn get_encryption_key() -> Arc<EncryptionKey> {
ENCRYPTION_KEY
.get()
.expect("Encryption key not initialized. Call init_encryption_key() first.")
.clone()
}
}
brokkr-broker::utils::encryption::encrypt_string
pub
#![allow(unused)]
fn main() {
fn encrypt_string (value : & str) -> Result < Vec < u8 > , EncryptionError >
}
Encrypts a string value for storage.
Parameters:
| Name | Type | Description |
|---|---|---|
value | - | The plaintext string to encrypt. |
Returns:
The encrypted bytes, or an error if encryption fails.
Source
#![allow(unused)]
fn main() {
pub fn encrypt_string(value: &str) -> Result<Vec<u8>, EncryptionError> {
get_encryption_key().encrypt(value.as_bytes())
}
}
brokkr-broker::utils::encryption::decrypt_string
pub
#![allow(unused)]
fn main() {
fn decrypt_string (encrypted : & [u8]) -> Result < String , String >
}
Decrypts bytes back to a string.
Parameters:
| Name | Type | Description |
|---|---|---|
encrypted | - | The encrypted bytes. |
Returns:
The decrypted string, or an error if decryption fails.
Source
#![allow(unused)]
fn main() {
pub fn decrypt_string(encrypted: &[u8]) -> Result<String, String> {
let bytes = get_encryption_key()
.decrypt(encrypted)
.map_err(|e| e.to_string())?;
String::from_utf8(bytes).map_err(|e| format!("Decrypted value is not valid UTF-8: {}", e))
}
}
brokkr-broker::utils::event_bus Rust
Event emission utilities for webhook notifications.
This module provides a database-centric approach to event emission. Events are directly inserted into the webhook_deliveries table for matching subscriptions. No in-memory event bus is used.
Functions
brokkr-broker::utils::event_bus::emit_event
pub
#![allow(unused)]
fn main() {
fn emit_event (dal : & DAL , event : & BrokkrEvent) -> usize
}
Emits an event by creating webhook deliveries for all matching subscriptions.
This function:
- Finds all enabled subscriptions matching the event type
- Creates a webhook_delivery record for each matching subscription
- Copies target_labels from subscription to delivery for routing
Parameters:
| Name | Type | Description |
|---|---|---|
dal | - | The Data Access Layer instance. |
event | - | The event to emit. |
Returns:
The number of deliveries created.
Source
#![allow(unused)]
fn main() {
pub fn emit_event(dal: &DAL, event: &BrokkrEvent) -> usize {
// Find all subscriptions matching this event type
let subscriptions = match dal
.webhook_subscriptions()
.get_matching_subscriptions(&event.event_type)
{
Ok(subs) => subs,
Err(e) => {
error!(
"Failed to get matching subscriptions for event {}: {:?}",
event.event_type, e
);
return 0;
}
};
if subscriptions.is_empty() {
debug!(
"No subscriptions match event {} (id: {})",
event.event_type, event.id
);
return 0;
}
debug!(
"Emitting event {} (id: {}) to {} subscription(s)",
event.event_type,
event.id,
subscriptions.len()
);
let mut created_count = 0;
// Create a delivery for each matching subscription
for subscription in subscriptions {
// Copy target_labels from subscription to delivery
let target_labels = subscription.target_labels.clone();
match NewWebhookDelivery::new(subscription.id, event, target_labels) {
Ok(new_delivery) => match dal.webhook_deliveries().create(&new_delivery) {
Ok(delivery) => {
let target = if delivery.target_labels.is_some() {
"agent"
} else {
"broker"
};
debug!(
"Created delivery {} for subscription {} (event: {}, target: {})",
delivery.id, subscription.id, event.event_type, target
);
created_count += 1;
}
Err(e) => {
error!(
"Failed to create delivery for subscription {}: {:?}",
subscription.id, e
);
}
},
Err(e) => {
error!(
"Failed to create NewWebhookDelivery for subscription {}: {}",
subscription.id, e
);
}
}
}
created_count
}
}
brokkr-broker::utils::matching Rust
Template-to-stack matching utilities.
This module provides functions for validating that a template’s labels and annotations are compatible with a target stack before instantiation.
Structs
brokkr-broker::utils::matching::MatchResult
pub
Derives: Debug, Default, Serialize
Result of a template-to-stack matching operation.
Fields
| Name | Type | Description |
|---|---|---|
matches | bool | Whether the template matches the stack. |
missing_labels | Vec < String > | Labels required by the template that are missing from the stack. |
missing_annotations | Vec < (String , String) > | Annotations required by the template that are missing from the stack. |
Functions
brokkr-broker::utils::matching::template_matches_stack
pub
#![allow(unused)]
fn main() {
fn template_matches_stack (template_labels : & [String] , template_annotations : & [(String , String)] , stack_labels : & [String] , stack_annotations : & [(String , String)] ,) -> MatchResult
}
Check if a template can be instantiated into a stack.
Parameters:
| Name | Type | Description |
|---|---|---|
template_labels | - | Labels attached to the template |
template_annotations | - | Annotations attached to the template (key-value pairs) |
stack_labels | - | Labels attached to the target stack |
stack_annotations | - | Annotations attached to the target stack (key-value pairs) |
Returns:
A MatchResult indicating whether the template matches and details about any missing labels or annotations.
Source
#![allow(unused)]
fn main() {
pub fn template_matches_stack(
template_labels: &[String],
template_annotations: &[(String, String)],
stack_labels: &[String],
stack_annotations: &[(String, String)],
) -> MatchResult {
// If template has no labels/annotations, it matches everything (go anywhere)
if template_labels.is_empty() && template_annotations.is_empty() {
return MatchResult {
matches: true,
missing_labels: Vec::new(),
missing_annotations: Vec::new(),
};
}
// Check all template labels exist on stack
let missing_labels: Vec<String> = template_labels
.iter()
.filter(|tl| !stack_labels.contains(tl))
.cloned()
.collect();
// Check all template annotations exist on stack (exact key-value match)
let missing_annotations: Vec<(String, String)> = template_annotations
.iter()
.filter(|ta| !stack_annotations.contains(ta))
.cloned()
.collect();
MatchResult {
matches: missing_labels.is_empty() && missing_annotations.is_empty(),
missing_labels,
missing_annotations,
}
}
}
brokkr-broker::utils::pak Rust
Prefixed API Key (PAK) management utilities.
This module provides functionality for creating, verifying, and managing Prefixed API Keys using a singleton controller pattern.
Functions
brokkr-broker::utils::pak::create_pak_controller
pub
#![allow(unused)]
fn main() {
fn create_pak_controller (config : Option < & Settings > ,) -> Result < Arc < PrefixedApiKeyController < OsRng , Sha256 > > , & 'static str >
}
Creates or retrieves the PAK controller.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Optional settings for initializing the controller. |
Returns:
Returns a Result containing the Arc-wrapped PAK controller or an error message.
Source
#![allow(unused)]
fn main() {
pub fn create_pak_controller(
config: Option<&Settings>,
) -> Result<Arc<PrefixedApiKeyController<OsRng, Sha256>>, &'static str> {
match (PAK_CONTROLLER.get(), config) {
(Some(controller), _) => Ok(controller.clone()),
(None, Some(cfg)) => {
let controller = PAK_CONTROLLER.get_or_init(|| {
info!("Initializing PAK_CONTROLLER for the first time");
Arc::new(create_pak_controller_inner(cfg).expect("Failed to create PAK controller"))
});
Ok(controller.clone())
}
(None, None) => Err("PAK_CONTROLLER not initialized and no config provided"),
}
}
}
brokkr-broker::utils::pak::create_pak_controller_inner
private
#![allow(unused)]
fn main() {
fn create_pak_controller_inner (config : & Settings ,) -> Result < PrefixedApiKeyController < OsRng , Sha256 > , Box < dyn std :: error :: Error > >
}
Internal function to create a new PAK controller.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Settings for configuring the PAK controller. |
Returns:
Returns a Result containing the new PAK controller or an error.
Source
#![allow(unused)]
fn main() {
fn create_pak_controller_inner(
config: &Settings,
) -> Result<PrefixedApiKeyController<OsRng, Sha256>, Box<dyn std::error::Error>> {
// This function remains unchanged
let builder = PrefixedApiKeyController::configure()
.prefix(config.pak.prefix.clone().unwrap())
.short_token_length(config.pak.short_token_length.unwrap())
.short_token_prefix(config.pak.short_token_prefix.clone())
.long_token_length(config.pak.long_token_length.unwrap())
.rng_osrng()
.digest_sha256();
builder.finalize().map_err(|e| e.into())
}
}
brokkr-broker::utils::pak::create_pak
pub
#![allow(unused)]
fn main() {
fn create_pak () -> Result < (String , String) , Box < dyn std :: error :: Error > >
}
Generates a new Prefixed API Key and its hash.
Returns:
Returns a Result containing a tuple of the PAK string and its hash, or an error.
Source
#![allow(unused)]
fn main() {
pub fn create_pak() -> Result<(String, String), Box<dyn std::error::Error>> {
let controller = create_pak_controller(None)?;
// Generate PAK and hash
controller
.try_generate_key_and_hash()
.map(|(pak, hash)| (pak.to_string(), hash))
.map_err(|e| e.into())
}
}
brokkr-broker::utils::pak::verify_pak
pub
#![allow(unused)]
fn main() {
fn verify_pak (pak : String , stored_hash : String) -> bool
}
Verifies a Prefixed API Key against a stored hash.
Parameters:
| Name | Type | Description |
|---|---|---|
pak | - | The Prefixed API Key to verify. |
stored_hash | - | The previously stored hash to compare against. |
Returns:
Returns true if the PAK is valid, false otherwise.
Source
#![allow(unused)]
fn main() {
pub fn verify_pak(pak: String, stored_hash: String) -> bool {
let pak = PrefixedApiKey::from_string(pak.as_str()).expect("Failed to parse PAK");
let controller = create_pak_controller(None).expect("Failed to create PAK controller");
let computed_hash = controller.long_token_hashed(&pak);
stored_hash == computed_hash
}
}
brokkr-broker::utils::pak::generate_pak_hash
pub
#![allow(unused)]
fn main() {
fn generate_pak_hash (pak : String) -> String
}
Generates a hash for a given Prefixed API Key.
Parameters:
| Name | Type | Description |
|---|---|---|
pak | - | The Prefixed API Key to hash. |
Returns:
Returns the generated hash as a String.
Source
#![allow(unused)]
fn main() {
pub fn generate_pak_hash(pak: String) -> String {
let pak = PrefixedApiKey::from_string(pak.as_str()).expect("Failed to parse PAK");
let controller = create_pak_controller(None).expect("Failed to create PAK controller");
controller.long_token_hashed(&pak)
}
}
brokkr-broker::utils::templating Rust
Tera template rendering and JSON Schema validation utilities.
This module provides functions for:
- Validating Tera template syntax at creation time
- Rendering Tera templates with parameters at instantiation time
- Validating JSON Schema definitions at creation time
- Validating parameters against JSON Schema at instantiation time
Structs
brokkr-broker::utils::templating::TemplateError
pub
Derives: Debug, Clone
Error type for templating operations.
Fields
| Name | Type | Description |
|---|---|---|
message | String | |
details | Option < String > |
brokkr-broker::utils::templating::ParameterValidationError
pub
Derives: Debug, Clone
Validation error details for parameter validation.
Fields
| Name | Type | Description |
|---|---|---|
path | String | |
message | String |
Functions
brokkr-broker::utils::templating::validate_tera_syntax
pub
#![allow(unused)]
fn main() {
fn validate_tera_syntax (template_content : & str) -> Result < () , TemplateError >
}
Validate Tera template syntax without rendering.
Called at template creation time to fail fast on invalid templates. Does not require actual parameter values - only checks syntax.
Parameters:
| Name | Type | Description |
|---|---|---|
template_content | - | The Tera template string to validate |
Returns:
Ok(())if the template syntax is valid *Err(TemplateError)with details about the syntax error
Examples:
use brokkr_broker::utils::templating::validate_tera_syntax;
// Valid template
assert!(validate_tera_syntax("Hello, {{ name }}!").is_ok());
// Invalid template - unclosed brace
assert!(validate_tera_syntax("Hello, {{ name !").is_err());
Source
#![allow(unused)]
fn main() {
pub fn validate_tera_syntax(template_content: &str) -> Result<(), TemplateError> {
let mut tera = Tera::default();
// Try to add the template - this validates syntax
tera.add_raw_template("__validation__", template_content)
.map_err(|e| TemplateError {
message: "Invalid Tera syntax".to_string(),
details: Some(e.to_string()),
})?;
Ok(())
}
}
brokkr-broker::utils::templating::render_template
pub
#![allow(unused)]
fn main() {
fn render_template (template_content : & str , parameters : & Value ,) -> Result < String , TemplateError >
}
Render a Tera template with the provided parameters.
Called at template instantiation time to produce the final output.
Parameters:
| Name | Type | Description |
|---|---|---|
template_content | - | The Tera template string to render |
parameters | - | JSON object containing parameter values |
Returns:
Ok(String)with the rendered output *Err(TemplateError)if rendering fails (e.g., missing required variable)
Examples:
use brokkr_broker::utils::templating::render_template;
use serde_json::json;
let template = "name: {{ name }}\nreplicas: {{ replicas }}";
let params = json!({"name": "my-app", "replicas": 3});
let result = render_template(template, ¶ms).unwrap();
assert!(result.contains("name: my-app"));
Source
#![allow(unused)]
fn main() {
pub fn render_template(
template_content: &str,
parameters: &Value,
) -> Result<String, TemplateError> {
let mut tera = Tera::default();
tera.add_raw_template("template", template_content)
.map_err(|e| TemplateError {
message: "Template parse error".to_string(),
details: Some(e.to_string()),
})?;
let mut context = Context::new();
// Flatten JSON parameters into Tera context
if let Value::Object(map) = parameters {
for (key, value) in map {
context.insert(key, value);
}
}
tera.render("template", &context)
.map_err(|e| TemplateError {
message: "Template rendering failed".to_string(),
details: Some(e.to_string()),
})
}
}
brokkr-broker::utils::templating::validate_json_schema
pub
#![allow(unused)]
fn main() {
fn validate_json_schema (schema_str : & str) -> Result < () , TemplateError >
}
Validate that a string is a valid JSON Schema.
Called at template creation time to ensure the schema is valid.
Parameters:
| Name | Type | Description |
|---|---|---|
schema_str | - | The JSON Schema as a string |
Returns:
Ok(())if the schema is valid *Err(TemplateError)with details about the validation error
Examples:
use brokkr_broker::utils::templating::validate_json_schema;
let schema = r#"{"type": "object", "properties": {"name": {"type": "string"}}}"#;
assert!(validate_json_schema(schema).is_ok());
// Invalid JSON
assert!(validate_json_schema("not json").is_err());
Source
#![allow(unused)]
fn main() {
pub fn validate_json_schema(schema_str: &str) -> Result<(), TemplateError> {
let schema: Value = serde_json::from_str(schema_str).map_err(|e| TemplateError {
message: "Invalid JSON".to_string(),
details: Some(e.to_string()),
})?;
JSONSchema::compile(&schema).map_err(|e| TemplateError {
message: "Invalid JSON Schema".to_string(),
details: Some(e.to_string()),
})?;
Ok(())
}
}
brokkr-broker::utils::templating::validate_parameters
pub
#![allow(unused)]
fn main() {
fn validate_parameters (schema_str : & str , parameters : & Value ,) -> Result < () , Vec < ParameterValidationError > >
}
Validate parameters against a JSON Schema.
Called at template instantiation time to ensure parameters match the schema.
Parameters:
| Name | Type | Description |
|---|---|---|
schema_str | - | The JSON Schema as a string |
parameters | - | The parameters to validate |
Returns:
Ok(())if parameters match the schema *Err(Vec<ParameterValidationError>)with all validation errors
Examples:
use brokkr_broker::utils::templating::validate_parameters;
use serde_json::json;
let schema = r#"{"type": "object", "required": ["name"], "properties": {"name": {"type": "string"}}}"#;
// Valid parameters
let params = json!({"name": "test"});
assert!(validate_parameters(schema, ¶ms).is_ok());
// Missing required field
let params = json!({});
assert!(validate_parameters(schema, ¶ms).is_err());
Source
#![allow(unused)]
fn main() {
pub fn validate_parameters(
schema_str: &str,
parameters: &Value,
) -> Result<(), Vec<ParameterValidationError>> {
let schema: Value = serde_json::from_str(schema_str).map_err(|e| {
vec![ParameterValidationError {
path: String::new(),
message: format!("Invalid schema JSON: {}", e),
}]
})?;
let compiled = JSONSchema::compile(&schema).map_err(|e| {
vec![ParameterValidationError {
path: String::new(),
message: format!("Invalid schema: {}", e),
}]
})?;
if !compiled.is_valid(parameters) {
let errors: Vec<ParameterValidationError> = compiled
.validate(parameters)
.err()
.map(|iter| {
iter.map(|e| ParameterValidationError {
path: e.instance_path.to_string(),
message: e.to_string(),
})
.collect()
})
.unwrap_or_default();
return Err(errors);
}
Ok(())
}
}
brokkr-models Rust
Functions
brokkr-models::establish_connection
pub(crate)
#![allow(unused)]
fn main() {
fn establish_connection (database_url : String) -> PgConnection
}
Establishes a connection to the PostgreSQL database.
This function exists to manage migrations and perform basic testing in this crate without a specific Data Access Layer (DAL) in place.
Parameters:
| Name | Type | Description |
|---|---|---|
database_url | - | A string slice that holds the URL of the database to connect to. |
Returns:
PgConnection- A connection to the PostgreSQL database.
Raises:
| Exception | Description |
|---|---|
Panic | This function will panic if it fails to establish a connection to the database. |
Source
#![allow(unused)]
fn main() {
pub(crate) fn establish_connection(database_url: String) -> PgConnection {
PgConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}
}
brokkr-models::models Rust
brokkr-models::models::agent_annotations Rust
Structs
brokkr-models::models::agent_annotations::AgentAnnotation
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents an agent annotation in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the annotation. |
agent_id | Uuid | ID of the agent this annotation belongs to. |
key | String | Key of the annotation (max 64 characters, no whitespace). |
value | String | Value of the annotation (max 64 characters, no whitespace). |
brokkr-models::models::agent_annotations::NewAgentAnnotation
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new agent annotation to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
agent_id | Uuid | ID of the agent this annotation belongs to. |
key | String | Key of the annotation (max 64 characters, no whitespace). |
value | String | Value of the annotation (max 64 characters, no whitespace). |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (agent_id : Uuid , key : String , value : String) -> Result < Self , String >
}
Creates a new NewAgentAnnotation instance.
Parameters:
| Name | Type | Description |
|---|---|---|
agent_id | - | UUID of the agent to associate the annotation with. |
key | - | The key for the annotation. Must be non-empty, max 64 characters, and contain no whitespace. |
value | - | The value for the annotation. Must be non-empty, max 64 characters, and contain no whitespace. |
Returns:
Returns Ok(NewAgentAnnotation) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(agent_id: Uuid, key: String, value: String) -> Result<Self, String> {
// Validate agent_id
if agent_id.is_nil() {
return Err("Invalid agent ID".to_string());
}
// Validate key
if key.is_empty() {
return Err("Key cannot be empty".to_string());
}
if key.len() > 64 {
return Err("Key cannot exceed 64 characters".to_string());
}
if key.contains(char::is_whitespace) {
return Err("Key cannot contain whitespace".to_string());
}
// Validate value
if value.is_empty() {
return Err("Value cannot be empty".to_string());
}
if value.len() > 64 {
return Err("Value cannot exceed 64 characters".to_string());
}
if value.contains(char::is_whitespace) {
return Err("Value cannot contain whitespace".to_string());
}
Ok(NewAgentAnnotation {
agent_id,
key,
value,
})
}
}
brokkr-models::models::agent_events Rust
Structs
brokkr-models::models::agent_events::AgentEvent
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents an agent event in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the event. |
created_at | DateTime < Utc > | Timestamp when the event was created. |
updated_at | DateTime < Utc > | Timestamp when the event was last updated. |
deleted_at | Option < DateTime < Utc > > | Timestamp for soft deletion, if applicable. |
agent_id | Uuid | ID of the agent associated with this event. |
deployment_object_id | Uuid | ID of the deployment object associated with this event. |
event_type | String | Type of the event. |
status | String | Status of the event (e.g., “SUCCESS”, “FAILURE”, “IN_PROGRESS”, “PENDING”). |
message | Option < String > | Optional message providing additional details about the event. |
brokkr-models::models::agent_events::NewAgentEvent
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new agent event to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
agent_id | Uuid | ID of the agent associated with this event. |
deployment_object_id | Uuid | ID of the deployment object associated with this event. |
event_type | String | Type of the event. |
status | String | Status of the event (e.g., “SUCCESS”, “FAILURE”). |
message | Option < String > | Optional message providing additional details about the event. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (agent_id : Uuid , deployment_object_id : Uuid , event_type : String , status : String , message : Option < String > ,) -> Result < Self , String >
}
Creates a new NewAgentEvent instance.
Parameters:
| Name | Type | Description |
|---|---|---|
agent_id | - | UUID of the agent associated with this event. |
deployment_object_id | - | UUID of the deployment object associated with this event. |
event_type | - | Type of the event. Must be a non-empty string. |
status | - | Status of the event. Must be one of: “SUCCESS”, “FAILURE”. |
message | - | Optional message providing additional details about the event. |
Returns:
Returns Ok(NewAgentEvent) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(
agent_id: Uuid,
deployment_object_id: Uuid,
event_type: String,
status: String,
message: Option<String>,
) -> Result<Self, String> {
// Validate agent_id
if agent_id.is_nil() {
return Err("Invalid agent ID".to_string());
}
// Validate deployment_object_id
if deployment_object_id.is_nil() {
return Err("Invalid deployment object ID".to_string());
}
// Validate event_type
if event_type.trim().is_empty() {
return Err("Event type cannot be empty".to_string());
}
// Validate status
let valid_statuses = ["SUCCESS", "FAILURE"];
if !valid_statuses.contains(&status.as_str()) {
return Err(format!(
"Invalid status. Must be one of: {}",
valid_statuses.join(", ")
));
}
Ok(NewAgentEvent {
agent_id,
deployment_object_id,
event_type,
status,
message,
})
}
}
brokkr-models::models::agent_labels Rust
Structs
brokkr-models::models::agent_labels::AgentLabel
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents an agent label in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the agent label. |
agent_id | Uuid | ID of the agent this label is associated with. |
label | String | The label text (max 64 characters, no whitespace). |
brokkr-models::models::agent_labels::NewAgentLabel
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new agent label to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
agent_id | Uuid | ID of the agent this label is associated with. |
label | String | The label text (max 64 characters, no whitespace). |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (agent_id : Uuid , label : String) -> Result < Self , String >
}
Creates a new NewAgentLabel instance.
Parameters:
| Name | Type | Description |
|---|---|---|
agent_id | - | UUID of the agent to associate the label with. |
label | - | The label text. Must be non-empty, max 64 characters, and contain no whitespace. |
Returns:
Returns Ok(NewAgentLabel) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(agent_id: Uuid, label: String) -> Result<Self, String> {
// Validate agent_id
if agent_id.is_nil() {
return Err("Invalid agent ID".to_string());
}
// Validate label
if label.is_empty() {
return Err("Label cannot be empty".to_string());
}
if label.len() > 64 {
return Err("Label cannot exceed 64 characters".to_string());
}
if label.contains(char::is_whitespace) {
return Err("Label cannot contain whitespace".to_string());
}
Ok(NewAgentLabel { agent_id, label })
}
}
brokkr-models::models::agent_targets Rust
Structs
brokkr-models::models::agent_targets::AgentTarget
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents an agent target in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the agent target. |
agent_id | Uuid | ID of the agent associated with this target. |
stack_id | Uuid | ID of the stack associated with this target. |
brokkr-models::models::agent_targets::NewAgentTarget
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new agent target to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
agent_id | Uuid | ID of the agent to associate with a stack. |
stack_id | Uuid | ID of the stack to associate with an agent. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (agent_id : Uuid , stack_id : Uuid) -> Result < Self , String >
}
Creates a new NewAgentTarget instance.
Parameters:
| Name | Type | Description |
|---|---|---|
agent_id | - | UUID of the agent to associate with a stack. |
stack_id | - | UUID of the stack to associate with an agent. |
Returns:
Returns Ok(NewAgentTarget) if both UUIDs are valid and non-nil, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(agent_id: Uuid, stack_id: Uuid) -> Result<Self, String> {
// Validate agent_id
if agent_id.is_nil() {
return Err("Invalid agent ID".to_string());
}
// Validate stack_id
if stack_id.is_nil() {
return Err("Invalid stack ID".to_string());
}
Ok(NewAgentTarget { agent_id, stack_id })
}
}
brokkr-models::models::agents Rust
Structs
brokkr-models::models::agents::Agent
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents an agent in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the agent. |
created_at | DateTime < Utc > | Timestamp when the agent was created. |
updated_at | DateTime < Utc > | Timestamp when the agent was last updated. |
deleted_at | Option < DateTime < Utc > > | Timestamp for soft deletion, if applicable. |
name | String | Name of the agent. |
cluster_name | String | Name of the cluster the agent belongs to. |
last_heartbeat | Option < DateTime < Utc > > | Timestamp of the last heartbeat received from the agent. |
status | String | Current status of the agent. |
pak_hash | String | Hash of the agent’s Pre-shared Authentication Key (PAK). |
brokkr-models::models::agents::NewAgent
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new agent to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
name | String | Name of the agent. |
cluster_name | String | Name of the cluster the agent belongs to. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (name : String , cluster_name : String) -> Result < Self , String >
}
Creates a new NewAgent instance.
Parameters:
| Name | Type | Description |
|---|---|---|
name | - | Name of the agent. Must be a non-empty string. |
cluster_name | - | Name of the cluster the agent belongs to. Must be a non-empty string. |
Returns:
Returns Ok(NewAgent) if both parameters are valid non-empty strings, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(name: String, cluster_name: String) -> Result<Self, String> {
// Validate name
if name.trim().is_empty() {
return Err("Agent name cannot be empty".to_string());
}
// Validate cluster_name
if cluster_name.trim().is_empty() {
return Err("Cluster name cannot be empty".to_string());
}
Ok(NewAgent { name, cluster_name })
}
}
brokkr-models::models::audit_logs Rust
Audit log models for tracking administrative and security-sensitive operations.
Audit logs are immutable records that track who did what to which resource. They are used for compliance, debugging, and security incident investigation.
Structs
brokkr-models::models::audit_logs::AuditLog
pub
Derives: Debug, Clone, Queryable, Selectable, Identifiable, Serialize, Deserialize, ToSchema
An audit log record from the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the log entry. |
timestamp | DateTime < Utc > | When the event occurred. |
actor_type | String | Type of actor: admin, agent, generator, system. |
actor_id | Option < Uuid > | ID of the actor (NULL for system or unauthenticated). |
action | String | The action performed (e.g., “agent.created”, “auth.failed”). |
resource_type | String | Type of resource affected. |
resource_id | Option < Uuid > | ID of the affected resource (NULL if not applicable). |
details | Option < serde_json :: Value > | Additional structured details. |
ip_address | Option < String > | Client IP address. |
user_agent | Option < String > | Client user agent string. |
created_at | DateTime < Utc > | When the record was created. |
brokkr-models::models::audit_logs::NewAuditLog
pub
Derives: Debug, Clone, Insertable, Serialize, Deserialize
A new audit log entry to be inserted.
Fields
| Name | Type | Description |
|---|---|---|
actor_type | String | Type of actor. |
actor_id | Option < Uuid > | ID of the actor. |
action | String | The action performed. |
resource_type | String | Type of resource affected. |
resource_id | Option < Uuid > | ID of the affected resource. |
details | Option < serde_json :: Value > | Additional structured details. |
ip_address | Option < String > | Client IP address. |
user_agent | Option < String > | Client user agent string. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (actor_type : & str , actor_id : Option < Uuid > , action : & str , resource_type : & str , resource_id : Option < Uuid > ,) -> Result < Self , String >
}
Creates a new audit log entry.
Parameters:
| Name | Type | Description |
|---|---|---|
actor_type | - | Type of actor (admin, agent, generator, system). |
actor_id | - | ID of the actor (None for system). |
action | - | The action performed. |
resource_type | - | Type of resource affected. |
resource_id | - | ID of the affected resource (None if not applicable). |
Source
#![allow(unused)]
fn main() {
pub fn new(
actor_type: &str,
actor_id: Option<Uuid>,
action: &str,
resource_type: &str,
resource_id: Option<Uuid>,
) -> Result<Self, String> {
// Validate actor type
if !VALID_ACTOR_TYPES.contains(&actor_type) {
return Err(format!(
"Invalid actor_type '{}'. Must be one of: {:?}",
actor_type, VALID_ACTOR_TYPES
));
}
// Validate action is not empty
if action.trim().is_empty() {
return Err("Action cannot be empty".to_string());
}
// Validate resource_type is not empty
if resource_type.trim().is_empty() {
return Err("Resource type cannot be empty".to_string());
}
Ok(Self {
actor_type: actor_type.to_string(),
actor_id,
action: action.to_string(),
resource_type: resource_type.to_string(),
resource_id,
details: None,
ip_address: None,
user_agent: None,
})
}
}
with_details pub
#![allow(unused)]
fn main() {
fn with_details (mut self , details : serde_json :: Value) -> Self
}
Adds details to the audit log entry.
Source
#![allow(unused)]
fn main() {
pub fn with_details(mut self, details: serde_json::Value) -> Self {
self.details = Some(details);
self
}
}
with_ip_address pub
#![allow(unused)]
fn main() {
fn with_ip_address (mut self , ip : impl Into < String >) -> Self
}
Adds client IP address to the audit log entry.
Source
#![allow(unused)]
fn main() {
pub fn with_ip_address(mut self, ip: impl Into<String>) -> Self {
self.ip_address = Some(ip.into());
self
}
}
with_user_agent pub
#![allow(unused)]
fn main() {
fn with_user_agent (mut self , user_agent : String) -> Self
}
Adds user agent to the audit log entry.
Source
#![allow(unused)]
fn main() {
pub fn with_user_agent(mut self, user_agent: String) -> Self {
self.user_agent = Some(user_agent);
self
}
}
brokkr-models::models::audit_logs::AuditLogFilter
pub
Derives: Debug, Clone, Default, Serialize, Deserialize, ToSchema
Filters for querying audit logs.
Fields
| Name | Type | Description |
|---|---|---|
actor_type | Option < String > | Filter by actor type. |
actor_id | Option < Uuid > | Filter by actor ID. |
action | Option < String > | Filter by action (exact match or prefix with *). |
resource_type | Option < String > | Filter by resource type. |
resource_id | Option < Uuid > | Filter by resource ID. |
from | Option < DateTime < Utc > > | Filter by start time (inclusive). |
to | Option < DateTime < Utc > > | Filter by end time (exclusive). |
brokkr-models::models::deployment_health Rust
Structs
brokkr-models::models::deployment_health::DeploymentHealth
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents a deployment health record in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the health record. |
agent_id | Uuid | ID of the agent that reported this health status. |
deployment_object_id | Uuid | ID of the deployment object this health status applies to. |
status | String | Health status: healthy, degraded, failing, or unknown. |
summary | Option < String > | JSON-encoded summary with pod counts, conditions, and resource details. |
checked_at | DateTime < Utc > | Timestamp when the agent last checked health. |
created_at | DateTime < Utc > | Timestamp when the record was created. |
updated_at | DateTime < Utc > | Timestamp when the record was last updated. |
brokkr-models::models::deployment_health::NewDeploymentHealth
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new deployment health record to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
agent_id | Uuid | ID of the agent reporting this health status. |
deployment_object_id | Uuid | ID of the deployment object this health status applies to. |
status | String | Health status: healthy, degraded, failing, or unknown. |
summary | Option < String > | JSON-encoded summary with pod counts, conditions, and resource details. |
checked_at | DateTime < Utc > | Timestamp when the agent checked health. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (agent_id : Uuid , deployment_object_id : Uuid , status : String , summary : Option < String > , checked_at : DateTime < Utc > ,) -> Result < Self , String >
}
Creates a new NewDeploymentHealth instance.
Parameters:
| Name | Type | Description |
|---|---|---|
agent_id | - | UUID of the agent reporting health. |
deployment_object_id | - | UUID of the deployment object. |
status | - | Health status (healthy, degraded, failing, unknown). |
summary | - | Optional JSON-encoded health summary. |
checked_at | - | When the health was checked. |
Returns:
Returns Ok(NewDeploymentHealth) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(
agent_id: Uuid,
deployment_object_id: Uuid,
status: String,
summary: Option<String>,
checked_at: DateTime<Utc>,
) -> Result<Self, String> {
// Validate agent_id
if agent_id.is_nil() {
return Err("Invalid agent ID".to_string());
}
// Validate deployment_object_id
if deployment_object_id.is_nil() {
return Err("Invalid deployment object ID".to_string());
}
// Validate status
if !VALID_HEALTH_STATUSES.contains(&status.as_str()) {
return Err(format!(
"Invalid health status. Must be one of: {}",
VALID_HEALTH_STATUSES.join(", ")
));
}
Ok(NewDeploymentHealth {
agent_id,
deployment_object_id,
status,
summary,
checked_at,
})
}
}
brokkr-models::models::deployment_health::UpdateDeploymentHealth
pub
Derives: AsChangeset, Debug, Clone, Serialize, Deserialize
Represents an update to an existing deployment health record.
Fields
| Name | Type | Description |
|---|---|---|
status | String | Updated health status. |
summary | Option < String > | Updated JSON-encoded summary. |
checked_at | DateTime < Utc > | Updated check timestamp. |
brokkr-models::models::deployment_health::HealthSummary
pub
Derives: Debug, Clone, Serialize, Deserialize, ToSchema
Structured health summary for serialization/deserialization.
Fields
| Name | Type | Description |
|---|---|---|
pods_ready | i32 | Number of pods in ready state. |
pods_total | i32 | Total number of pods. |
conditions | Vec < String > | List of detected problematic conditions (e.g., ImagePullBackOff). |
resources | Option < Vec < ResourceHealth > > | Optional detailed resource status. |
brokkr-models::models::deployment_health::ResourceHealth
pub
Derives: Debug, Clone, Serialize, Deserialize, ToSchema
Health status for an individual Kubernetes resource.
Fields
| Name | Type | Description |
|---|---|---|
kind | String | Resource kind (e.g., Deployment, StatefulSet). |
name | String | Resource name. |
namespace | String | Resource namespace. |
ready | bool | Whether the resource is ready. |
message | Option < String > | Optional status message. |
brokkr-models::models::deployment_objects Rust
Structs
brokkr-models::models::deployment_objects::DeploymentObject
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents a deployment object in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the deployment object. |
created_at | DateTime < Utc > | Timestamp when the deployment object was created. |
updated_at | DateTime < Utc > | Timestamp when the deployment object was last updated. |
deleted_at | Option < DateTime < Utc > > | Timestamp for soft deletion, if applicable. |
sequence_id | i64 | Auto-incrementing sequence number for ordering. |
stack_id | Uuid | ID of the stack this deployment object belongs to. |
yaml_content | String | YAML content of the deployment. |
yaml_checksum | String | SHA-256 checksum of the YAML content. |
submitted_at | DateTime < Utc > | Timestamp when the deployment was submitted. |
is_deletion_marker | bool | Indicates if this object marks a deletion. |
brokkr-models::models::deployment_objects::NewDeploymentObject
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new deployment object to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
stack_id | Uuid | ID of the stack this deployment object belongs to. |
yaml_content | String | YAML content of the deployment. |
yaml_checksum | String | SHA-256 checksum of the YAML content. |
is_deletion_marker | bool | Indicates if this object marks a deletion. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (stack_id : Uuid , yaml_content : String , is_deletion_marker : bool ,) -> Result < Self , String >
}
Creates a new NewDeploymentObject instance.
Parameters:
| Name | Type | Description |
|---|---|---|
stack_id | - | UUID of the stack this deployment object belongs to. |
yaml_content | - | YAML content of the deployment. Must be a non-empty string. |
is_deletion_marker | - | Boolean indicating if this object marks a deletion. |
generator_id | - | UUID of the generator associated with this deployment object. |
Returns:
Returns Ok(NewDeploymentObject) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(
stack_id: Uuid,
yaml_content: String,
is_deletion_marker: bool,
) -> Result<Self, String> {
// Validate stack_id
if stack_id.is_nil() {
return Err("Invalid stack ID".to_string());
}
// Validate yaml_content
if yaml_content.trim().is_empty() {
return Err("YAML content cannot be empty".to_string());
}
// Generate SHA-256 checksum
let yaml_checksum = generate_checksum(&yaml_content);
Ok(NewDeploymentObject {
stack_id,
yaml_content,
yaml_checksum,
is_deletion_marker,
})
}
}
Functions
brokkr-models::models::deployment_objects::generate_checksum
private
#![allow(unused)]
fn main() {
fn generate_checksum (content : & str) -> String
}
Helper function to generate SHA-256 checksum for YAML content.
Source
#![allow(unused)]
fn main() {
fn generate_checksum(content: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
format!("{:x}", hasher.finalize())
}
}
brokkr-models::models::diagnostic_requests Rust
Diagnostic Request model for on-demand diagnostic requests.
Diagnostic requests are created by operators to request detailed diagnostic information from agents about specific deployment objects.
Structs
brokkr-models::models::diagnostic_requests::DiagnosticRequest
pub
Derives: Debug, Clone, Queryable, Selectable, Identifiable, Serialize, Deserialize, ToSchema
A diagnostic request record from the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the diagnostic request. |
agent_id | Uuid | The agent that should handle this request. |
deployment_object_id | Uuid | The deployment object to gather diagnostics for. |
status | String | Status: pending, claimed, completed, failed, expired. |
requested_by | Option < String > | Who requested the diagnostics (e.g., operator username). |
created_at | DateTime < Utc > | When the request was created. |
claimed_at | Option < DateTime < Utc > > | When the agent claimed the request. |
completed_at | Option < DateTime < Utc > > | When the request was completed. |
expires_at | DateTime < Utc > | When the request expires and should be cleaned up. |
brokkr-models::models::diagnostic_requests::NewDiagnosticRequest
pub
Derives: Debug, Clone, Insertable, Serialize, Deserialize
A new diagnostic request to be inserted.
Fields
| Name | Type | Description |
|---|---|---|
agent_id | Uuid | The agent that should handle this request. |
deployment_object_id | Uuid | The deployment object to gather diagnostics for. |
status | String | Status (defaults to “pending”). |
requested_by | Option < String > | Who requested the diagnostics. |
expires_at | DateTime < Utc > | When the request expires. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (agent_id : Uuid , deployment_object_id : Uuid , requested_by : Option < String > , retention_minutes : Option < i64 > ,) -> Result < Self , String >
}
Creates a new diagnostic request.
Parameters:
| Name | Type | Description |
|---|---|---|
agent_id | - | The agent that should handle this request. |
deployment_object_id | - | The deployment object to gather diagnostics for. |
requested_by | - | Optional identifier of who requested the diagnostics. |
retention_minutes | - | How long the request should be retained (default 60). |
Returns:
A Result containing the new diagnostic request or an error.
Source
#![allow(unused)]
fn main() {
pub fn new(
agent_id: Uuid,
deployment_object_id: Uuid,
requested_by: Option<String>,
retention_minutes: Option<i64>,
) -> Result<Self, String> {
// Validate UUIDs are not nil
if agent_id.is_nil() {
return Err("Agent ID cannot be nil".to_string());
}
if deployment_object_id.is_nil() {
return Err("Deployment object ID cannot be nil".to_string());
}
let retention = retention_minutes.unwrap_or(60);
if !(1..=1440).contains(&retention) {
return Err("Retention must be between 1 and 1440 minutes".to_string());
}
let expires_at = Utc::now() + Duration::minutes(retention);
Ok(Self {
agent_id,
deployment_object_id,
status: "pending".to_string(),
requested_by,
expires_at,
})
}
}
brokkr-models::models::diagnostic_requests::UpdateDiagnosticRequest
pub
Derives: Debug, Clone, AsChangeset
Changeset for updating a diagnostic request.
Fields
| Name | Type | Description |
|---|---|---|
status | Option < String > | New status. |
claimed_at | Option < DateTime < Utc > > | When claimed. |
completed_at | Option < DateTime < Utc > > | When completed. |
brokkr-models::models::diagnostic_results Rust
Diagnostic Result model for storing collected diagnostic data.
Diagnostic results contain the pod statuses, events, and log tails collected by agents in response to diagnostic requests.
Structs
brokkr-models::models::diagnostic_results::DiagnosticResult
pub
Derives: Debug, Clone, Queryable, Selectable, Identifiable, Serialize, Deserialize, ToSchema
A diagnostic result record from the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the diagnostic result. |
request_id | Uuid | The diagnostic request this result belongs to. |
pod_statuses | String | JSON-encoded pod statuses. |
events | String | JSON-encoded Kubernetes events. |
log_tails | Option < String > | JSON-encoded log tails (optional). |
collected_at | DateTime < Utc > | When the diagnostics were collected by the agent. |
created_at | DateTime < Utc > | When the result was created in the database. |
brokkr-models::models::diagnostic_results::NewDiagnosticResult
pub
Derives: Debug, Clone, Insertable, Serialize, Deserialize
A new diagnostic result to be inserted.
Fields
| Name | Type | Description |
|---|---|---|
request_id | Uuid | The diagnostic request this result belongs to. |
pod_statuses | String | JSON-encoded pod statuses. |
events | String | JSON-encoded Kubernetes events. |
log_tails | Option < String > | JSON-encoded log tails (optional). |
collected_at | DateTime < Utc > | When the diagnostics were collected by the agent. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (request_id : Uuid , pod_statuses : String , events : String , log_tails : Option < String > , collected_at : DateTime < Utc > ,) -> Result < Self , String >
}
Creates a new diagnostic result.
Parameters:
| Name | Type | Description |
|---|---|---|
request_id | - | The diagnostic request this result belongs to. |
pod_statuses | - | JSON-encoded pod statuses. |
events | - | JSON-encoded Kubernetes events. |
log_tails | - | Optional JSON-encoded log tails. |
collected_at | - | When the diagnostics were collected. |
Returns:
A Result containing the new diagnostic result or an error.
Source
#![allow(unused)]
fn main() {
pub fn new(
request_id: Uuid,
pod_statuses: String,
events: String,
log_tails: Option<String>,
collected_at: DateTime<Utc>,
) -> Result<Self, String> {
// Validate request_id is not nil
if request_id.is_nil() {
return Err("Request ID cannot be nil".to_string());
}
// Validate pod_statuses is not empty
if pod_statuses.is_empty() {
return Err("Pod statuses cannot be empty".to_string());
}
// Validate events is not empty
if events.is_empty() {
return Err("Events cannot be empty".to_string());
}
Ok(Self {
request_id,
pod_statuses,
events,
log_tails,
collected_at,
})
}
}
brokkr-models::models::generator Rust
Structs
brokkr-models::models::generator::Generator
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents a generator in the Brokkr system.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the generator. |
created_at | DateTime < Utc > | Timestamp of when the generator was created. |
updated_at | DateTime < Utc > | Timestamp of when the generator was last updated. |
deleted_at | Option < DateTime < Utc > > | Timestamp of when the generator was deleted, if applicable. |
name | String | Name of the generator. |
description | Option < String > | Optional description of the generator. |
pak_hash | Option < String > | Hash of the Pre-Authentication Key (PAK) for the generator. |
last_active_at | Option < DateTime < Utc > > | Timestamp of when the generator was last active. |
is_active | bool | Indicates whether the generator is currently active. |
brokkr-models::models::generator::NewGenerator
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents the data required to create a new generator.
Fields
| Name | Type | Description |
|---|---|---|
name | String | Name of the new generator. |
description | Option < String > | Optional description of the new generator. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (name : String , description : Option < String >) -> Result < Self , String >
}
Creates a new NewGenerator instance.
Parameters:
| Name | Type | Description |
|---|---|---|
name | - | The name for the generator. Must be non-empty and not exceed 255 characters. |
description | - | An optional description for the generator. |
Returns:
Returns Ok(NewGenerator) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(name: String, description: Option<String>) -> Result<Self, String> {
if name.trim().is_empty() {
return Err("Generator name cannot be empty".to_string());
}
if name.len() > 255 {
return Err("Generator name cannot exceed 255 characters".to_string());
}
Ok(NewGenerator { name, description })
}
}
brokkr-models::models::rendered_deployment_objects Rust
Structs
brokkr-models::models::rendered_deployment_objects::RenderedDeploymentObject
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents a rendered deployment object provenance record in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for this provenance record. |
deployment_object_id | Uuid | ID of the deployment object that was created. |
template_id | Uuid | ID of the template used to create the deployment object. |
template_version | i32 | Version of the template at the time of rendering (snapshot). |
template_parameters | String | JSON string of parameters used for rendering. |
created_at | DateTime < Utc > | Timestamp when the rendering occurred. |
brokkr-models::models::rendered_deployment_objects::NewRenderedDeploymentObject
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new rendered deployment object provenance record to be inserted.
Fields
| Name | Type | Description |
|---|---|---|
deployment_object_id | Uuid | ID of the deployment object that was created. |
template_id | Uuid | ID of the template used to create the deployment object. |
template_version | i32 | Version of the template at the time of rendering (snapshot). |
template_parameters | String | JSON string of parameters used for rendering. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (deployment_object_id : Uuid , template_id : Uuid , template_version : i32 , template_parameters : String ,) -> Result < Self , String >
}
Creates a new NewRenderedDeploymentObject instance.
Parameters:
| Name | Type | Description |
|---|---|---|
deployment_object_id | - | UUID of the deployment object created from this rendering. |
template_id | - | UUID of the template used for rendering. |
template_version | - | Version number of the template used. |
template_parameters | - | JSON string of the parameters used for rendering. |
Returns:
Returns Ok(NewRenderedDeploymentObject) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(
deployment_object_id: Uuid,
template_id: Uuid,
template_version: i32,
template_parameters: String,
) -> Result<Self, String> {
// Validate deployment_object_id
if deployment_object_id.is_nil() {
return Err("Invalid deployment object ID".to_string());
}
// Validate template_id
if template_id.is_nil() {
return Err("Invalid template ID".to_string());
}
// Validate template_version
if template_version < 1 {
return Err("Template version must be at least 1".to_string());
}
// Validate template_parameters is valid JSON
if serde_json::from_str::<serde_json::Value>(&template_parameters).is_err() {
return Err("Template parameters must be valid JSON".to_string());
}
Ok(NewRenderedDeploymentObject {
deployment_object_id,
template_id,
template_version,
template_parameters,
})
}
}
brokkr-models::models::stack_annotations Rust
Structs
brokkr-models::models::stack_annotations::StackAnnotation
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ``
Represents a stack annotation in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the annotation. |
stack_id | Uuid | ID of the stack this annotation belongs to. |
key | String | Key of the annotation (max 64 characters, no whitespace). |
value | String | Value of the annotation (max 64 characters, no whitespace). |
brokkr-models::models::stack_annotations::NewStackAnnotation
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize
Represents a new stack annotation to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
stack_id | Uuid | ID of the stack this annotation belongs to. |
key | String | Key of the annotation (max 64 characters, no whitespace). |
value | String | Value of the annotation (max 64 characters, no whitespace). |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (stack_id : Uuid , key : String , value : String) -> Result < Self , String >
}
Creates a new NewStackAnnotation instance.
Parameters:
| Name | Type | Description |
|---|---|---|
stack_id | - | UUID of the stack to associate the annotation with. |
key | - | The key for the annotation. Must be non-empty, max 64 characters, and contain no whitespace. |
value | - | The value for the annotation. Must be non-empty, max 64 characters, and contain no whitespace. |
Returns:
Returns Ok(NewStackAnnotation) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(stack_id: Uuid, key: String, value: String) -> Result<Self, String> {
// Validate stack_id
if stack_id.is_nil() {
return Err("Invalid stack ID".to_string());
}
// Validate key
if key.is_empty() {
return Err("Key cannot be empty".to_string());
}
if key.len() > 64 {
return Err("Key cannot exceed 64 characters".to_string());
}
if key.contains(char::is_whitespace) {
return Err("Key cannot contain whitespace".to_string());
}
// Validate value
if value.is_empty() {
return Err("Value cannot be empty".to_string());
}
if value.len() > 64 {
return Err("Value cannot exceed 64 characters".to_string());
}
if value.contains(char::is_whitespace) {
return Err("Value cannot contain whitespace".to_string());
}
Ok(NewStackAnnotation {
stack_id,
key,
value,
})
}
}
brokkr-models::models::stack_labels Rust
Structs
brokkr-models::models::stack_labels::StackLabel
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ``
Represents a stack label in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the stack label. |
stack_id | Uuid | ID of the stack this label is associated with. |
label | String | The label text (max 64 characters, no whitespace). |
brokkr-models::models::stack_labels::NewStackLabel
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize
Represents a new stack label to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
stack_id | Uuid | ID of the stack this label is associated with. |
label | String | The label text (max 64 characters, no whitespace). |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (stack_id : Uuid , label : String) -> Result < Self , String >
}
Creates a new NewStackLabel instance.
Parameters:
| Name | Type | Description |
|---|---|---|
stack_id | - | UUID of the stack to associate the label with. |
label | - | The label text. Must be non-empty, max 64 characters, and contain no whitespace. |
Returns:
Returns Ok(NewStackLabel) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(stack_id: Uuid, label: String) -> Result<Self, String> {
// Validate stack_id
if stack_id.is_nil() {
return Err("Invalid stack ID".to_string());
}
// Validate label
if label.trim().is_empty() {
return Err("Label cannot be empty".to_string());
}
// Check label length
if label.len() > 64 {
return Err("Label cannot exceed 64 characters".to_string());
}
// Check whitespace
if label.contains(char::is_whitespace) {
return Err("Label cannot contain whitespace".to_string());
}
Ok(NewStackLabel { stack_id, label })
}
}
brokkr-models::models::stack_templates Rust
Structs
brokkr-models::models::stack_templates::StackTemplate
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents a stack template in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the template. |
created_at | DateTime < Utc > | Timestamp when the template was created. |
updated_at | DateTime < Utc > | Timestamp when the template was last updated. |
deleted_at | Option < DateTime < Utc > > | Timestamp for soft deletion, if applicable. |
generator_id | Option < Uuid > | Generator ID - NULL for system templates (admin-only). |
name | String | Name of the template. |
description | Option < String > | Optional description of the template. |
version | i32 | Version number (auto-incremented per name+generator_id). |
template_content | String | Tera template content. |
parameters_schema | String | JSON Schema for parameter validation. |
checksum | String | SHA-256 checksum of template_content. |
brokkr-models::models::stack_templates::NewStackTemplate
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new stack template to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
generator_id | Option < Uuid > | Generator ID - NULL for system templates (admin-only). |
name | String | Name of the template. |
description | Option < String > | Optional description of the template. |
version | i32 | Version number. |
template_content | String | Tera template content. |
parameters_schema | String | JSON Schema for parameter validation. |
checksum | String | SHA-256 checksum of template_content. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (generator_id : Option < Uuid > , name : String , description : Option < String > , version : i32 , template_content : String , parameters_schema : String ,) -> Result < Self , String >
}
Creates a new NewStackTemplate instance.
Parameters:
| Name | Type | Description |
|---|---|---|
generator_id | - | Optional generator ID. NULL means system template. |
name | - | Name of the template. Must be non-empty. |
description | - | Optional description. If provided, must not be empty. |
version | - | Version number for this template. |
template_content | - | Tera template content. |
parameters_schema | - | JSON Schema as a string. |
Returns:
Returns Ok(NewStackTemplate) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(
generator_id: Option<Uuid>,
name: String,
description: Option<String>,
version: i32,
template_content: String,
parameters_schema: String,
) -> Result<Self, String> {
// Validate name
if name.trim().is_empty() {
return Err("Template name cannot be empty".to_string());
}
// Validate description (if provided)
if let Some(desc) = &description {
if desc.trim().is_empty() {
return Err("Template description cannot be empty if provided".to_string());
}
}
// Validate template_content is not empty
if template_content.trim().is_empty() {
return Err("Template content cannot be empty".to_string());
}
// Validate parameters_schema is not empty
if parameters_schema.trim().is_empty() {
return Err("Parameters schema cannot be empty".to_string());
}
// Validate version is positive
if version < 1 {
return Err("Version must be at least 1".to_string());
}
// Generate checksum
let checksum = generate_checksum(&template_content);
Ok(NewStackTemplate {
generator_id,
name,
description,
version,
template_content,
parameters_schema,
checksum,
})
}
}
Functions
brokkr-models::models::stack_templates::generate_checksum
pub
#![allow(unused)]
fn main() {
fn generate_checksum (content : & str) -> String
}
Generates a SHA-256 checksum for the given content.
Source
#![allow(unused)]
fn main() {
pub fn generate_checksum(content: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
format!("{:x}", hasher.finalize())
}
}
brokkr-models::models::stacks Rust
Structs
brokkr-models::models::stacks::Stack
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents a stack in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the stack. |
created_at | DateTime < Utc > | Timestamp when the stack was created. |
updated_at | DateTime < Utc > | Timestamp when the stack was last updated. |
deleted_at | Option < DateTime < Utc > > | Timestamp for soft deletion, if applicable. |
name | String | Name of the stack. |
description | Option < String > | Optional description of the stack. |
generator_id | Uuid | Optional generator ID. |
brokkr-models::models::stacks::NewStack
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new stack to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
name | String | Name of the stack. |
description | Option < String > | Optional description of the stack. |
generator_id | Uuid | Optional generator ID. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (name : String , description : Option < String > , generator_id : Uuid ,) -> Result < Self , String >
}
Creates a new NewStack instance.
Parameters:
| Name | Type | Description |
|---|---|---|
name | - | Name of the stack. Must be a non-empty string. |
description | - | Optional description of the stack. If provided, must not be an empty string. |
generator_id | - | Optional generator ID. |
Returns:
Returns Ok(NewStack) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(
name: String,
description: Option<String>,
generator_id: Uuid,
) -> Result<Self, String> {
// Validate name
if name.trim().is_empty() {
return Err("Stack name cannot be empty".to_string());
}
// Validate description (if provided)
if let Some(desc) = &description {
if desc.trim().is_empty() {
return Err("Stack description cannot be empty if provided".to_string());
}
}
Ok(NewStack {
name,
description,
generator_id,
})
}
}
brokkr-models::models::template_annotations Rust
Structs
brokkr-models::models::template_annotations::TemplateAnnotation
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents a template annotation in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the template annotation. |
template_id | Uuid | ID of the template this annotation is associated with. |
key | String | The annotation key (max 64 characters, no whitespace). |
value | String | The annotation value (max 64 characters, no whitespace). |
created_at | DateTime < Utc > | Timestamp when the annotation was created. |
brokkr-models::models::template_annotations::NewTemplateAnnotation
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new template annotation to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
template_id | Uuid | ID of the template this annotation is associated with. |
key | String | The annotation key (max 64 characters, no whitespace). |
value | String | The annotation value (max 64 characters, no whitespace). |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (template_id : Uuid , key : String , value : String) -> Result < Self , String >
}
Creates a new NewTemplateAnnotation instance.
Parameters:
| Name | Type | Description |
|---|---|---|
template_id | - | UUID of the template to associate the annotation with. |
key | - | The annotation key. Must be non-empty, max 64 characters, no whitespace. |
value | - | The annotation value. Must be non-empty, max 64 characters, no whitespace. |
Returns:
Returns Ok(NewTemplateAnnotation) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(template_id: Uuid, key: String, value: String) -> Result<Self, String> {
// Validate template_id
if template_id.is_nil() {
return Err("Invalid template ID".to_string());
}
// Validate key
if key.trim().is_empty() {
return Err("Annotation key cannot be empty".to_string());
}
if key.len() > 64 {
return Err("Annotation key cannot exceed 64 characters".to_string());
}
if key.contains(char::is_whitespace) {
return Err("Annotation key cannot contain whitespace".to_string());
}
// Validate value
if value.trim().is_empty() {
return Err("Annotation value cannot be empty".to_string());
}
if value.len() > 64 {
return Err("Annotation value cannot exceed 64 characters".to_string());
}
if value.contains(char::is_whitespace) {
return Err("Annotation value cannot contain whitespace".to_string());
}
Ok(NewTemplateAnnotation {
template_id,
key,
value,
})
}
}
brokkr-models::models::template_labels Rust
Structs
brokkr-models::models::template_labels::TemplateLabel
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents a template label in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the template label. |
template_id | Uuid | ID of the template this label is associated with. |
label | String | The label text (max 64 characters, no whitespace). |
created_at | DateTime < Utc > | Timestamp when the label was created. |
brokkr-models::models::template_labels::NewTemplateLabel
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new template label to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
template_id | Uuid | ID of the template this label is associated with. |
label | String | The label text (max 64 characters, no whitespace). |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (template_id : Uuid , label : String) -> Result < Self , String >
}
Creates a new NewTemplateLabel instance.
Parameters:
| Name | Type | Description |
|---|---|---|
template_id | - | UUID of the template to associate the label with. |
label | - | The label text. Must be non-empty, max 64 characters, no whitespace. |
Returns:
Returns Ok(NewTemplateLabel) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(template_id: Uuid, label: String) -> Result<Self, String> {
// Validate template_id
if template_id.is_nil() {
return Err("Invalid template ID".to_string());
}
// Validate label
if label.trim().is_empty() {
return Err("Label cannot be empty".to_string());
}
// Check label length
if label.len() > 64 {
return Err("Label cannot exceed 64 characters".to_string());
}
// Check whitespace
if label.contains(char::is_whitespace) {
return Err("Label cannot contain whitespace".to_string());
}
Ok(NewTemplateLabel { template_id, label })
}
}
brokkr-models::models::template_targets Rust
Structs
brokkr-models::models::template_targets::TemplateTarget
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents a template target in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the template target. |
template_id | Uuid | ID of the template associated with this target. |
stack_id | Uuid | ID of the stack associated with this target. |
created_at | DateTime < Utc > | Timestamp when the target association was created. |
brokkr-models::models::template_targets::NewTemplateTarget
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new template target to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
template_id | Uuid | ID of the template to associate with a stack. |
stack_id | Uuid | ID of the stack to associate with a template. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (template_id : Uuid , stack_id : Uuid) -> Result < Self , String >
}
Creates a new NewTemplateTarget instance.
Parameters:
| Name | Type | Description |
|---|---|---|
template_id | - | UUID of the template to associate with a stack. |
stack_id | - | UUID of the stack to associate with a template. |
Returns:
Returns Ok(NewTemplateTarget) if both UUIDs are valid and non-nil, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(template_id: Uuid, stack_id: Uuid) -> Result<Self, String> {
// Validate template_id
if template_id.is_nil() {
return Err("Invalid template ID".to_string());
}
// Validate stack_id
if stack_id.is_nil() {
return Err("Invalid stack ID".to_string());
}
Ok(NewTemplateTarget {
template_id,
stack_id,
})
}
}
brokkr-models::models::webhooks Rust
Webhook models for event notifications.
This module provides models for webhook subscriptions and deliveries, enabling external systems to receive notifications when events occur in Brokkr.
Structs
brokkr-models::models::webhooks::BrokkrEvent
pub
Derives: Debug, Clone, Serialize, Deserialize, ToSchema
A Brokkr event that can trigger webhook deliveries.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for this event (idempotency key). |
event_type | String | Event type (e.g., “deployment.applied”). |
timestamp | DateTime < Utc > | When the event occurred. |
data | serde_json :: Value | Event-specific data. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (event_type : & str , data : serde_json :: Value) -> Self
}
Creates a new event.
Source
#![allow(unused)]
fn main() {
pub fn new(event_type: &str, data: serde_json::Value) -> Self {
Self {
id: Uuid::new_v4(),
event_type: event_type.to_string(),
timestamp: Utc::now(),
data,
}
}
}
brokkr-models::models::webhooks::WebhookFilters
pub
Derives: Debug, Clone, Default, Serialize, Deserialize, ToSchema
Filters for webhook subscriptions.
Fields
| Name | Type | Description |
|---|---|---|
agent_id | Option < Uuid > | Filter by specific agent ID. |
stack_id | Option < Uuid > | Filter by specific stack ID. |
labels | Option < std :: collections :: HashMap < String , String > > | Filter by labels (all must match). |
brokkr-models::models::webhooks::WebhookSubscription
pub
Derives: Debug, Clone, Queryable, Selectable, Identifiable, Serialize, Deserialize, ToSchema
A webhook subscription record from the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the subscription. |
name | String | Human-readable name for the subscription. |
url_encrypted | Vec < u8 > | Encrypted webhook URL. |
auth_header_encrypted | Option < Vec < u8 > > | Encrypted Authorization header value. |
event_types | Vec < Option < String > > | Event types to subscribe to (supports wildcards like “deployment.*”). |
filters | Option < String > | JSON-encoded filters. |
target_labels | Option < Vec < Option < String > > > | Labels for delivery targeting (NULL = broker delivers). |
enabled | bool | Whether the subscription is active. |
max_retries | i32 | Maximum delivery retry attempts. |
timeout_seconds | i32 | HTTP request timeout in seconds. |
created_at | DateTime < Utc > | When the subscription was created. |
updated_at | DateTime < Utc > | When the subscription was last updated. |
created_by | Option < String > | Who created the subscription. |
brokkr-models::models::webhooks::NewWebhookSubscription
pub
Derives: Debug, Clone, Insertable, Serialize, Deserialize
A new webhook subscription to be inserted.
Fields
| Name | Type | Description |
|---|---|---|
name | String | Human-readable name. |
url_encrypted | Vec < u8 > | Encrypted webhook URL. |
auth_header_encrypted | Option < Vec < u8 > > | Encrypted Authorization header value. |
event_types | Vec < Option < String > > | Event types to subscribe to. |
filters | Option < String > | JSON-encoded filters. |
target_labels | Option < Vec < Option < String > > > | Labels for delivery targeting (NULL = broker delivers). |
enabled | bool | Whether the subscription is active (defaults to true). |
max_retries | i32 | Maximum retry attempts (defaults to 5). |
timeout_seconds | i32 | HTTP timeout in seconds (defaults to 30). |
created_by | Option < String > | Who created the subscription. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (name : String , url_encrypted : Vec < u8 > , auth_header_encrypted : Option < Vec < u8 > > , event_types : Vec < String > , filters : Option < WebhookFilters > , target_labels : Option < Vec < String > > , created_by : Option < String > ,) -> Result < Self , String >
}
Creates a new webhook subscription.
Parameters:
| Name | Type | Description |
|---|---|---|
name | - | Human-readable name for the subscription. |
url_encrypted | - | Pre-encrypted webhook URL. |
auth_header_encrypted | - | Pre-encrypted Authorization header (optional). |
event_types | - | List of event types to subscribe to. |
filters | - | Optional filters as WebhookFilters struct. |
target_labels | - | Optional labels for delivery targeting. |
created_by | - | Who is creating the subscription. |
Returns:
A Result containing the new subscription or an error.
Source
#![allow(unused)]
fn main() {
pub fn new(
name: String,
url_encrypted: Vec<u8>,
auth_header_encrypted: Option<Vec<u8>>,
event_types: Vec<String>,
filters: Option<WebhookFilters>,
target_labels: Option<Vec<String>>,
created_by: Option<String>,
) -> Result<Self, String> {
// Validate name
if name.trim().is_empty() {
return Err("Name cannot be empty".to_string());
}
if name.len() > 255 {
return Err("Name cannot exceed 255 characters".to_string());
}
// Validate event types
if event_types.is_empty() {
return Err("At least one event type is required".to_string());
}
// Serialize filters to JSON if provided
let filters_json = filters
.map(|f| serde_json::to_string(&f))
.transpose()
.map_err(|e| format!("Failed to serialize filters: {}", e))?;
Ok(Self {
name,
url_encrypted,
auth_header_encrypted,
event_types: event_types.into_iter().map(Some).collect(),
filters: filters_json,
target_labels: target_labels.map(|labels| labels.into_iter().map(Some).collect()),
enabled: true,
max_retries: 5,
timeout_seconds: 30,
created_by,
})
}
}
brokkr-models::models::webhooks::UpdateWebhookSubscription
pub
Derives: Debug, Clone, Default, AsChangeset
Changeset for updating a webhook subscription.
Fields
| Name | Type | Description |
|---|---|---|
name | Option < String > | New name. |
url_encrypted | Option < Vec < u8 > > | New encrypted URL. |
auth_header_encrypted | Option < Option < Vec < u8 > > > | New encrypted auth header. |
event_types | Option < Vec < Option < String > > > | New event types. |
filters | Option < Option < String > > | New filters. |
target_labels | Option < Option < Vec < Option < String > > > > | New target labels for delivery. |
enabled | Option < bool > | Enable/disable. |
max_retries | Option < i32 > | New max retries. |
timeout_seconds | Option < i32 > | New timeout. |
brokkr-models::models::webhooks::WebhookDelivery
pub
Derives: Debug, Clone, Queryable, Selectable, Identifiable, Serialize, Deserialize, ToSchema
A webhook delivery record from the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the delivery. |
subscription_id | Uuid | The subscription this delivery belongs to. |
event_type | String | The event type being delivered. |
event_id | Uuid | The event ID (idempotency key). |
payload | String | JSON-encoded event payload. |
target_labels | Option < Vec < Option < String > > > | Labels for delivery targeting (copied from subscription). |
status | String | Delivery status: pending, acquired, success, failed, dead. |
acquired_by | Option < Uuid > | Agent ID that acquired this delivery (NULL = broker). |
acquired_until | Option < DateTime < Utc > > | TTL for the acquisition - release if exceeded. |
attempts | i32 | Number of delivery attempts. |
last_attempt_at | Option < DateTime < Utc > > | When the last delivery attempt was made. |
next_retry_at | Option < DateTime < Utc > > | When to retry after failure. |
last_error | Option < String > | Error message from last failed attempt. |
created_at | DateTime < Utc > | When the delivery was created. |
completed_at | Option < DateTime < Utc > > | When the delivery completed (success or dead). |
brokkr-models::models::webhooks::NewWebhookDelivery
pub
Derives: Debug, Clone, Insertable, Serialize, Deserialize
A new webhook delivery to be inserted.
Fields
| Name | Type | Description |
|---|---|---|
subscription_id | Uuid | The subscription to deliver to. |
event_type | String | The event type. |
event_id | Uuid | The event ID. |
payload | String | JSON-encoded payload. |
target_labels | Option < Vec < Option < String > > > | Labels for delivery targeting (copied from subscription). |
status | String | Initial status (pending). |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (subscription_id : Uuid , event : & BrokkrEvent , target_labels : Option < Vec < Option < String > > > ,) -> Result < Self , String >
}
Creates a new webhook delivery.
Parameters:
| Name | Type | Description |
|---|---|---|
subscription_id | - | The subscription to deliver to. |
event | - | The event to deliver. |
target_labels | - | Labels for delivery targeting (from subscription). |
Returns:
A Result containing the new delivery or an error.
Source
#![allow(unused)]
fn main() {
pub fn new(
subscription_id: Uuid,
event: &BrokkrEvent,
target_labels: Option<Vec<Option<String>>>,
) -> Result<Self, String> {
if subscription_id.is_nil() {
return Err("Subscription ID cannot be nil".to_string());
}
let payload = serde_json::to_string(event)
.map_err(|e| format!("Failed to serialize event: {}", e))?;
Ok(Self {
subscription_id,
event_type: event.event_type.clone(),
event_id: event.id,
payload,
target_labels,
status: DELIVERY_STATUS_PENDING.to_string(),
})
}
}
brokkr-models::models::webhooks::UpdateWebhookDelivery
pub
Derives: Debug, Clone, Default, AsChangeset
Changeset for updating a webhook delivery.
Fields
| Name | Type | Description |
|---|---|---|
status | Option < String > | New status. |
acquired_by | Option < Option < Uuid > > | Agent that acquired this delivery. |
acquired_until | Option < Option < DateTime < Utc > > > | TTL for the acquisition. |
attempts | Option < i32 > | Increment attempts. |
last_attempt_at | Option < DateTime < Utc > > | When last attempted. |
next_retry_at | Option < Option < DateTime < Utc > > > | When to retry. |
last_error | Option < Option < String > > | Error message. |
completed_at | Option < DateTime < Utc > > | When completed. |
brokkr-models::models::work_order_annotations Rust
Structs
brokkr-models::models::work_order_annotations::WorkOrderAnnotation
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ``
Represents a work order annotation in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the annotation. |
work_order_id | Uuid | ID of the work order this annotation belongs to. |
key | String | Key of the annotation (max 64 characters, no whitespace). |
value | String | Value of the annotation (max 64 characters, no whitespace). |
created_at | chrono :: DateTime < chrono :: Utc > | Timestamp when the annotation was created. |
brokkr-models::models::work_order_annotations::NewWorkOrderAnnotation
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize
Represents a new work order annotation to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
work_order_id | Uuid | ID of the work order this annotation belongs to. |
key | String | Key of the annotation (max 64 characters, no whitespace). |
value | String | Value of the annotation (max 64 characters, no whitespace). |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (work_order_id : Uuid , key : String , value : String) -> Result < Self , String >
}
Creates a new NewWorkOrderAnnotation instance.
Parameters:
| Name | Type | Description |
|---|---|---|
work_order_id | - | UUID of the work order to associate the annotation with. |
key | - | The key for the annotation. Must be non-empty, max 64 characters, and contain no whitespace. |
value | - | The value for the annotation. Must be non-empty, max 64 characters, and contain no whitespace. |
Returns:
Returns Ok(NewWorkOrderAnnotation) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(work_order_id: Uuid, key: String, value: String) -> Result<Self, String> {
// Validate work_order_id
if work_order_id.is_nil() {
return Err("Invalid work order ID".to_string());
}
// Validate key
if key.is_empty() {
return Err("Key cannot be empty".to_string());
}
if key.len() > 64 {
return Err("Key cannot exceed 64 characters".to_string());
}
if key.contains(char::is_whitespace) {
return Err("Key cannot contain whitespace".to_string());
}
// Validate value
if value.is_empty() {
return Err("Value cannot be empty".to_string());
}
if value.len() > 64 {
return Err("Value cannot exceed 64 characters".to_string());
}
if value.contains(char::is_whitespace) {
return Err("Value cannot contain whitespace".to_string());
}
Ok(NewWorkOrderAnnotation {
work_order_id,
key,
value,
})
}
}
brokkr-models::models::work_order_labels Rust
Structs
brokkr-models::models::work_order_labels::WorkOrderLabel
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ``
Represents a work order label in the database.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the work order label. |
work_order_id | Uuid | ID of the work order this label is associated with. |
label | String | The label text (max 64 characters, no whitespace). |
created_at | chrono :: DateTime < chrono :: Utc > | Timestamp when the label was created. |
brokkr-models::models::work_order_labels::NewWorkOrderLabel
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize
Represents a new work order label to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
work_order_id | Uuid | ID of the work order this label is associated with. |
label | String | The label text (max 64 characters, no whitespace). |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (work_order_id : Uuid , label : String) -> Result < Self , String >
}
Creates a new NewWorkOrderLabel instance.
Parameters:
| Name | Type | Description |
|---|---|---|
work_order_id | - | UUID of the work order to associate the label with. |
label | - | The label text. Must be non-empty, max 64 characters, and contain no whitespace. |
Returns:
Returns Ok(NewWorkOrderLabel) if all parameters are valid, otherwise returns an Err with a description of the validation failure.
Source
#![allow(unused)]
fn main() {
pub fn new(work_order_id: Uuid, label: String) -> Result<Self, String> {
// Validate work_order_id
if work_order_id.is_nil() {
return Err("Invalid work order ID".to_string());
}
// Validate label
if label.trim().is_empty() {
return Err("Label cannot be empty".to_string());
}
// Check label length
if label.len() > 64 {
return Err("Label cannot exceed 64 characters".to_string());
}
// Check whitespace
if label.contains(char::is_whitespace) {
return Err("Label cannot contain whitespace".to_string());
}
Ok(NewWorkOrderLabel {
work_order_id,
label,
})
}
}
brokkr-models::models::work_orders Rust
Structs
brokkr-models::models::work_orders::WorkOrder
pub
Derives: Queryable, Selectable, Identifiable, AsChangeset, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents an active work order in the queue.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the work order. |
created_at | DateTime < Utc > | Timestamp when the work order was created. |
updated_at | DateTime < Utc > | Timestamp when the work order was last updated. |
work_type | String | Type of work (e.g., “build”, “test”, “backup”). |
yaml_content | String | Multi-document YAML content (e.g., Build + WorkOrder definitions). |
status | String | Queue status: PENDING, CLAIMED, or RETRY_PENDING. |
claimed_by | Option < Uuid > | ID of the agent that claimed this work order (if any). |
claimed_at | Option < DateTime < Utc > > | Timestamp when the work order was claimed. |
claim_timeout_seconds | i32 | Seconds before a claimed work order is considered stale. |
max_retries | i32 | Maximum number of retry attempts. |
retry_count | i32 | Current retry count. |
backoff_seconds | i32 | Base backoff seconds for exponential retry calculation. |
next_retry_after | Option < DateTime < Utc > > | Timestamp when RETRY_PENDING work order becomes PENDING again. |
last_error | Option < String > | Most recent error message from failed execution attempt. |
last_error_at | Option < DateTime < Utc > > | Timestamp of the most recent failure. |
brokkr-models::models::work_orders::NewWorkOrder
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new work order to be inserted into the database.
Fields
| Name | Type | Description |
|---|---|---|
work_type | String | Type of work (e.g., “build”, “test”, “backup”). |
yaml_content | String | Multi-document YAML content. |
max_retries | i32 | Maximum number of retry attempts. |
backoff_seconds | i32 | Base backoff seconds for exponential retry calculation. |
claim_timeout_seconds | i32 | Seconds before a claimed work order is considered stale. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (work_type : String , yaml_content : String , max_retries : Option < i32 > , backoff_seconds : Option < i32 > , claim_timeout_seconds : Option < i32 > ,) -> Result < Self , String >
}
Creates a new NewWorkOrder instance with validation.
Parameters:
| Name | Type | Description |
|---|---|---|
work_type | - | Type of work (e.g., “build”, “test”). |
yaml_content | - | Multi-document YAML content. |
max_retries | - | Maximum retry attempts (optional, defaults to 3). |
backoff_seconds | - | Base backoff for retries (optional, defaults to 60). |
claim_timeout_seconds | - | Claim timeout (optional, defaults to 3600). |
Returns:
Returns Ok(NewWorkOrder) if valid, otherwise Err with validation error.
Source
#![allow(unused)]
fn main() {
pub fn new(
work_type: String,
yaml_content: String,
max_retries: Option<i32>,
backoff_seconds: Option<i32>,
claim_timeout_seconds: Option<i32>,
) -> Result<Self, String> {
// Validate work_type
if work_type.trim().is_empty() {
return Err("Work type cannot be empty".to_string());
}
// Validate yaml_content
if yaml_content.trim().is_empty() {
return Err("YAML content cannot be empty".to_string());
}
let max_retries = max_retries.unwrap_or(3);
let backoff_seconds = backoff_seconds.unwrap_or(60);
let claim_timeout_seconds = claim_timeout_seconds.unwrap_or(3600);
if max_retries < 0 {
return Err("max_retries must be non-negative".to_string());
}
if backoff_seconds <= 0 {
return Err("backoff_seconds must be positive".to_string());
}
if claim_timeout_seconds <= 0 {
return Err("claim_timeout_seconds must be positive".to_string());
}
Ok(NewWorkOrder {
work_type,
yaml_content,
max_retries,
backoff_seconds,
claim_timeout_seconds,
})
}
}
brokkr-models::models::work_orders::WorkOrderLog
pub
Derives: Queryable, Selectable, Identifiable, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents a completed work order in the audit log.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Original work order ID. |
work_type | String | Type of work. |
created_at | DateTime < Utc > | Timestamp when the work order was created. |
claimed_at | Option < DateTime < Utc > > | Timestamp when the work order was claimed. |
completed_at | DateTime < Utc > | Timestamp when the work order completed. |
claimed_by | Option < Uuid > | ID of the agent that executed this work order. |
success | bool | Whether the work completed successfully. |
retries_attempted | i32 | Number of retry attempts before completion. |
result_message | Option < String > | Result message (image digest on success, error details on failure). |
yaml_content | String | Original YAML content for debugging/reconstruction. |
brokkr-models::models::work_orders::NewWorkOrderLog
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize
Represents a new work order log entry to be inserted.
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Original work order ID. |
work_type | String | Type of work. |
created_at | DateTime < Utc > | Timestamp when the work order was created. |
claimed_at | Option < DateTime < Utc > > | Timestamp when the work order was claimed. |
claimed_by | Option < Uuid > | ID of the agent that executed this work order. |
success | bool | Whether the work completed successfully. |
retries_attempted | i32 | Number of retry attempts before completion. |
result_message | Option < String > | Result message. |
yaml_content | String | Original YAML content. |
Methods
from_work_order pub
#![allow(unused)]
fn main() {
fn from_work_order (work_order : & WorkOrder , success : bool , result_message : Option < String > ,) -> Self
}
Creates a new log entry from a completed work order.
Source
#![allow(unused)]
fn main() {
pub fn from_work_order(
work_order: &WorkOrder,
success: bool,
result_message: Option<String>,
) -> Self {
NewWorkOrderLog {
id: work_order.id,
work_type: work_order.work_type.clone(),
created_at: work_order.created_at,
claimed_at: work_order.claimed_at,
claimed_by: work_order.claimed_by,
success,
retries_attempted: work_order.retry_count,
result_message,
yaml_content: work_order.yaml_content.clone(),
}
}
}
brokkr-models::models::work_orders::WorkOrderTarget
pub
Derives: Queryable, Selectable, Identifiable, Associations, Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, ToSchema, ``
Represents a work order target (agent routing).
Fields
| Name | Type | Description |
|---|---|---|
id | Uuid | Unique identifier for the target entry. |
work_order_id | Uuid | ID of the work order. |
agent_id | Uuid | ID of the eligible agent. |
created_at | DateTime < Utc > | Timestamp when the target was created. |
brokkr-models::models::work_orders::NewWorkOrderTarget
pub
Derives: Insertable, Debug, Clone, Serialize, Deserialize, ToSchema
Represents a new work order target to be inserted.
Fields
| Name | Type | Description |
|---|---|---|
work_order_id | Uuid | ID of the work order. |
agent_id | Uuid | ID of the eligible agent. |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (work_order_id : Uuid , agent_id : Uuid) -> Result < Self , String >
}
Creates a new work order target.
Source
#![allow(unused)]
fn main() {
pub fn new(work_order_id: Uuid, agent_id: Uuid) -> Result<Self, String> {
if work_order_id.is_nil() {
return Err("Invalid work order ID".to_string());
}
if agent_id.is_nil() {
return Err("Invalid agent ID".to_string());
}
Ok(NewWorkOrderTarget {
work_order_id,
agent_id,
})
}
}
Functions
brokkr-models::models::work_orders::default_max_retries
private
#![allow(unused)]
fn main() {
fn default_max_retries () -> i32
}
Source
#![allow(unused)]
fn main() {
fn default_max_retries() -> i32 {
3
}
}
brokkr-models::models::work_orders::default_backoff_seconds
private
#![allow(unused)]
fn main() {
fn default_backoff_seconds () -> i32
}
Source
#![allow(unused)]
fn main() {
fn default_backoff_seconds() -> i32 {
60
}
}
brokkr-models::models::work_orders::default_claim_timeout_seconds
private
#![allow(unused)]
fn main() {
fn default_claim_timeout_seconds () -> i32
}
Source
#![allow(unused)]
fn main() {
fn default_claim_timeout_seconds() -> i32 {
3600
}
}
brokkr-models::schema Rust
brokkr-utils Rust
brokkr-utils::config Rust
Structs
brokkr-utils::config::Settings
pub
Derives: Debug, Deserialize, Clone
Represents the main settings structure for the application
Fields
| Name | Type | Description |
|---|---|---|
database | Database | Database configuration |
log | Log | Logging configuration |
pak | PAK | PAK configuration |
agent | Agent | Agent configuration |
broker | Broker | Broker configuration |
cors | Cors | CORS configuration |
telemetry | Telemetry | Telemetry configuration |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (file : Option < String >) -> Result < Self , ConfigError >
}
Creates a new Settings instance
Parameters:
| Name | Type | Description |
|---|---|---|
file | - | An optional path to a configuration file |
Returns:
Returns a Result containing the Settings instance or a ConfigError
Source
#![allow(unused)]
fn main() {
pub fn new(file: Option<String>) -> Result<Self, ConfigError> {
// Start with default settings from the embedded TOML file
let mut s = Config::builder()
.add_source(File::from_str(DEFAULT_SETTINGS, config::FileFormat::Toml));
// If a configuration file is provided, add it as a source
s = match file {
Some(x) => s.add_source(File::with_name(x.as_str())),
None => s,
};
// Add environment variables as a source, prefixed with "BROKKR" and using "__" as a separator
s = s.add_source(Environment::with_prefix("BROKKR").separator("__"));
// Build the configuration
let settings = s.build().unwrap();
// Deserialize the configuration into a Settings instance
settings.try_deserialize()
}
}
brokkr-utils::config::Cors
pub
Derives: Debug, Deserialize, Clone
Represents the CORS configuration
Fields
| Name | Type | Description |
|---|---|---|
allowed_origins | Vec < String > | Allowed origins for CORS requests |
| Use “*” to allow all origins (not recommended for production) | ||
| Can be set as comma-separated string via env var: “origin1,origin2” | ||
allowed_methods | Vec < String > | Allowed HTTP methods |
| Can be set as comma-separated string via env var: “GET,POST,PUT” | ||
allowed_headers | Vec < String > | Allowed HTTP headers |
| Can be set as comma-separated string via env var: “Authorization,Content-Type” | ||
max_age_seconds | u64 | Max age for preflight cache in seconds |
brokkr-utils::config::Broker
pub
Derives: Debug, Deserialize, Clone
Fields
| Name | Type | Description |
|---|---|---|
pak_hash | Option < String > | PAK Hash |
diagnostic_cleanup_interval_seconds | Option < u64 > | Interval for diagnostic cleanup task in seconds (default: 900 = 15 minutes) |
diagnostic_max_age_hours | Option < i64 > | Maximum age for completed/expired diagnostics before deletion in hours (default: 1) |
webhook_encryption_key | Option < String > | Webhook encryption key (hex-encoded, 32 bytes for AES-256) |
| If not provided, a random key will be generated on startup (not recommended for production) | ||
webhook_delivery_interval_seconds | Option < u64 > | Webhook delivery worker interval in seconds (default: 5) |
webhook_delivery_batch_size | Option < i64 > | Webhook delivery batch size (default: 50) |
webhook_cleanup_retention_days | Option < i64 > | Webhook delivery cleanup retention in days (default: 7) |
audit_log_retention_days | Option < i64 > | Audit log retention in days (default: 90) |
auth_cache_ttl_seconds | Option < u64 > | Auth cache TTL in seconds (default: 60). Set to 0 to disable caching. |
brokkr-utils::config::Agent
pub
Derives: Debug, Deserialize, Clone
Represents the agent configuration
Fields
| Name | Type | Description |
|---|---|---|
broker_url | String | Broker URL |
polling_interval | u64 | Polling interval in seconds |
kubeconfig_path | Option < String > | Kubeconfig path |
max_retries | u32 | Max number of retries |
pak | String | PAK |
agent_name | String | Agent name |
cluster_name | String | Cluster name |
max_event_message_retries | usize | Max number of retries for event messages |
event_message_retry_delay | u64 | Delay between event message retries in seconds |
health_port | Option < u16 > | Health check HTTP server port |
deployment_health_enabled | Option < bool > | Whether deployment health checking is enabled |
deployment_health_interval | Option < u64 > | Interval for deployment health checks in seconds |
brokkr-utils::config::Database
pub
Derives: Debug, Deserialize, Clone
Represents the database configuration
Fields
| Name | Type | Description |
|---|---|---|
url | String | Database connection URL |
schema | Option < String > | Optional schema name for multi-tenant isolation |
brokkr-utils::config::Log
pub
Derives: Debug, Deserialize, Clone
Represents the logging configuration
Fields
| Name | Type | Description |
|---|---|---|
level | String | Log level (e.g., “info”, “debug”, “warn”, “error”) |
format | String | Log format: “text” for human-readable, “json” for structured JSON |
brokkr-utils::config::Telemetry
pub
Derives: Debug, Deserialize, Clone
Represents the telemetry (OpenTelemetry) configuration with hierarchical overrides
Fields
| Name | Type | Description |
|---|---|---|
enabled | bool | Whether telemetry is enabled (base default) |
otlp_endpoint | String | OTLP endpoint for trace export (gRPC) |
service_name | String | Service name for traces |
sampling_rate | f64 | Sampling rate (0.0 to 1.0) |
broker | TelemetryOverride | Broker-specific overrides |
agent | TelemetryOverride | Agent-specific overrides |
Methods
for_broker pub
#![allow(unused)]
fn main() {
fn for_broker (& self) -> ResolvedTelemetry
}
Get resolved telemetry config for broker (base merged with broker overrides)
Source
#![allow(unused)]
fn main() {
pub fn for_broker(&self) -> ResolvedTelemetry {
ResolvedTelemetry {
enabled: self.broker.enabled.unwrap_or(self.enabled),
otlp_endpoint: self
.broker
.otlp_endpoint
.clone()
.unwrap_or_else(|| self.otlp_endpoint.clone()),
service_name: self
.broker
.service_name
.clone()
.unwrap_or_else(|| self.service_name.clone()),
sampling_rate: self.broker.sampling_rate.unwrap_or(self.sampling_rate),
}
}
}
for_agent pub
#![allow(unused)]
fn main() {
fn for_agent (& self) -> ResolvedTelemetry
}
Get resolved telemetry config for agent (base merged with agent overrides)
Source
#![allow(unused)]
fn main() {
pub fn for_agent(&self) -> ResolvedTelemetry {
ResolvedTelemetry {
enabled: self.agent.enabled.unwrap_or(self.enabled),
otlp_endpoint: self
.agent
.otlp_endpoint
.clone()
.unwrap_or_else(|| self.otlp_endpoint.clone()),
service_name: self
.agent
.service_name
.clone()
.unwrap_or_else(|| self.service_name.clone()),
sampling_rate: self.agent.sampling_rate.unwrap_or(self.sampling_rate),
}
}
}
brokkr-utils::config::TelemetryOverride
pub
Derives: Debug, Deserialize, Clone, Default
Component-specific telemetry overrides (all fields optional)
Fields
| Name | Type | Description |
|---|---|---|
enabled | Option < bool > | Override enabled flag |
otlp_endpoint | Option < String > | Override OTLP endpoint |
service_name | Option < String > | Override service name |
sampling_rate | Option < f64 > | Override sampling rate |
brokkr-utils::config::ResolvedTelemetry
pub
Derives: Debug, Clone
Resolved telemetry configuration after merging base with overrides
Fields
| Name | Type | Description |
|---|---|---|
enabled | bool | |
otlp_endpoint | String | |
service_name | String | |
sampling_rate | f64 |
brokkr-utils::config::PAK
pub
Derives: Debug, Deserialize, Clone
Represents the PAK configuration
Fields
| Name | Type | Description |
|---|---|---|
prefix | Option < String > | PAK prefix |
digest | Option < String > | Digest algorithm for PAK |
rng | Option < String > | RNG type for PAK |
short_token_length | Option < usize > | Short token length for PAK |
short_token_length_str | Option < String > | Short token length as a string |
short_token_prefix | Option < String > | Prefix for short tokens |
long_token_length | Option < usize > | Long token length for PAK |
long_token_length_str | Option < String > | Long token length as a string |
Methods
short_length_as_str pub
#![allow(unused)]
fn main() {
fn short_length_as_str (& mut self)
}
Convert short token length to string
Source
#![allow(unused)]
fn main() {
pub fn short_length_as_str(&mut self) {
self.short_token_length_str = self.short_token_length.map(|v| v.to_string());
}
}
long_length_as_str pub
#![allow(unused)]
fn main() {
fn long_length_as_str (& mut self)
}
Convert long token length to string
Source
#![allow(unused)]
fn main() {
pub fn long_length_as_str(&mut self) {
self.long_token_length_str = self.long_token_length.map(|v| v.to_string());
}
}
brokkr-utils::config::DynamicConfig
pub
Derives: Debug, Clone
Dynamic configuration values that can be hot-reloaded at runtime.
These settings can be updated without restarting the application. Changes are applied atomically via the RwLock in ReloadableConfig.
Fields
| Name | Type | Description |
|---|---|---|
log_level | String | Log level (e.g., “info”, “debug”, “warn”, “error”) |
diagnostic_cleanup_interval_seconds | u64 | Interval for diagnostic cleanup task in seconds |
diagnostic_max_age_hours | i64 | Maximum age for completed/expired diagnostics before deletion in hours |
webhook_delivery_interval_seconds | u64 | Webhook delivery worker interval in seconds |
webhook_delivery_batch_size | i64 | Webhook delivery batch size |
webhook_cleanup_retention_days | i64 | Webhook delivery cleanup retention in days |
cors_allowed_origins | Vec < String > | Allowed origins for CORS requests |
cors_max_age_seconds | u64 | Max age for CORS preflight cache in seconds |
Methods
from_settings pub
#![allow(unused)]
fn main() {
fn from_settings (settings : & Settings) -> Self
}
Create DynamicConfig from Settings
Source
#![allow(unused)]
fn main() {
pub fn from_settings(settings: &Settings) -> Self {
Self {
log_level: settings.log.level.clone(),
diagnostic_cleanup_interval_seconds: settings
.broker
.diagnostic_cleanup_interval_seconds
.unwrap_or(900),
diagnostic_max_age_hours: settings.broker.diagnostic_max_age_hours.unwrap_or(1),
webhook_delivery_interval_seconds: settings
.broker
.webhook_delivery_interval_seconds
.unwrap_or(5),
webhook_delivery_batch_size: settings.broker.webhook_delivery_batch_size.unwrap_or(50),
webhook_cleanup_retention_days: settings
.broker
.webhook_cleanup_retention_days
.unwrap_or(7),
cors_allowed_origins: settings.cors.allowed_origins.clone(),
cors_max_age_seconds: settings.cors.max_age_seconds,
}
}
}
brokkr-utils::config::ConfigChange
pub
Derives: Debug, Clone
Represents a configuration change detected during reload
Fields
| Name | Type | Description |
|---|---|---|
key | String | The configuration key that changed |
old_value | String | The old value (as string for display) |
new_value | String | The new value (as string for display) |
brokkr-utils::config::ReloadableConfig
pub
Derives: Clone
Configuration wrapper that separates static (restart-required) settings from dynamic (hot-reloadable) settings.
Static settings are immutable after creation and require an application
restart to change. Dynamic settings can be updated at runtime via the
reload() method.
Examples:
use brokkr_utils::config::ReloadableConfig;
let config = ReloadableConfig::new(None)?;
// Read dynamic config (thread-safe)
let log_level = config.log_level();
// Reload config from sources
let changes = config.reload()?;
for change in changes {
println!("Changed {}: {} -> {}", change.key, change.old_value, change.new_value);
}
Fields
| Name | Type | Description |
|---|---|---|
static_config | Settings | Static configuration that requires restart to change |
dynamic | Arc < RwLock < DynamicConfig > > | Dynamic configuration that can be hot-reloaded |
config_file | Option < String > | Optional path to config file for reloading |
Methods
new pub
#![allow(unused)]
fn main() {
fn new (file : Option < String >) -> Result < Self , ConfigError >
}
Creates a new ReloadableConfig instance
Parameters:
| Name | Type | Description |
|---|---|---|
file | - | An optional path to a configuration file |
Returns:
Returns a Result containing the ReloadableConfig instance or a ConfigError
Source
#![allow(unused)]
fn main() {
pub fn new(file: Option<String>) -> Result<Self, ConfigError> {
let settings = Settings::new(file.clone())?;
let dynamic = DynamicConfig::from_settings(&settings);
Ok(Self {
static_config: settings,
dynamic: Arc::new(RwLock::new(dynamic)),
config_file: file,
})
}
}
from_settings pub
#![allow(unused)]
fn main() {
fn from_settings (settings : Settings , config_file : Option < String >) -> Self
}
Creates a ReloadableConfig from an existing Settings instance
Parameters:
| Name | Type | Description |
|---|---|---|
settings | - | The Settings instance to wrap |
config_file | - | An optional path to the config file for future reloads |
Returns:
Returns a ReloadableConfig instance
Source
#![allow(unused)]
fn main() {
pub fn from_settings(settings: Settings, config_file: Option<String>) -> Self {
let dynamic = DynamicConfig::from_settings(&settings);
Self {
static_config: settings,
dynamic: Arc::new(RwLock::new(dynamic)),
config_file,
}
}
}
static_config pub
#![allow(unused)]
fn main() {
fn static_config (& self) -> & Settings
}
Get a reference to the static (immutable) settings
These settings require an application restart to change.
Source
#![allow(unused)]
fn main() {
pub fn static_config(&self) -> &Settings {
&self.static_config
}
}
reload pub
#![allow(unused)]
fn main() {
fn reload (& self) -> Result < Vec < ConfigChange > , ConfigError >
}
Reload dynamic configuration from sources (file + environment)
Returns a list of configuration changes that were applied. Thread-safe: blocks writers during reload.
Source
#![allow(unused)]
fn main() {
pub fn reload(&self) -> Result<Vec<ConfigChange>, ConfigError> {
// Load fresh settings from sources
let new_settings = Settings::new(self.config_file.clone())?;
let new_dynamic = DynamicConfig::from_settings(&new_settings);
// Acquire write lock and compute changes
let mut dynamic = self
.dynamic
.write()
.map_err(|e| ConfigError::Message(format!("Failed to acquire write lock: {}", e)))?;
let mut changes = Vec::new();
// Check each field for changes
if dynamic.log_level != new_dynamic.log_level {
changes.push(ConfigChange {
key: "log.level".to_string(),
old_value: dynamic.log_level.clone(),
new_value: new_dynamic.log_level.clone(),
});
}
if dynamic.diagnostic_cleanup_interval_seconds
!= new_dynamic.diagnostic_cleanup_interval_seconds
{
changes.push(ConfigChange {
key: "broker.diagnostic_cleanup_interval_seconds".to_string(),
old_value: dynamic.diagnostic_cleanup_interval_seconds.to_string(),
new_value: new_dynamic.diagnostic_cleanup_interval_seconds.to_string(),
});
}
if dynamic.diagnostic_max_age_hours != new_dynamic.diagnostic_max_age_hours {
changes.push(ConfigChange {
key: "broker.diagnostic_max_age_hours".to_string(),
old_value: dynamic.diagnostic_max_age_hours.to_string(),
new_value: new_dynamic.diagnostic_max_age_hours.to_string(),
});
}
if dynamic.webhook_delivery_interval_seconds
!= new_dynamic.webhook_delivery_interval_seconds
{
changes.push(ConfigChange {
key: "broker.webhook_delivery_interval_seconds".to_string(),
old_value: dynamic.webhook_delivery_interval_seconds.to_string(),
new_value: new_dynamic.webhook_delivery_interval_seconds.to_string(),
});
}
if dynamic.webhook_delivery_batch_size != new_dynamic.webhook_delivery_batch_size {
changes.push(ConfigChange {
key: "broker.webhook_delivery_batch_size".to_string(),
old_value: dynamic.webhook_delivery_batch_size.to_string(),
new_value: new_dynamic.webhook_delivery_batch_size.to_string(),
});
}
if dynamic.webhook_cleanup_retention_days != new_dynamic.webhook_cleanup_retention_days {
changes.push(ConfigChange {
key: "broker.webhook_cleanup_retention_days".to_string(),
old_value: dynamic.webhook_cleanup_retention_days.to_string(),
new_value: new_dynamic.webhook_cleanup_retention_days.to_string(),
});
}
if dynamic.cors_allowed_origins != new_dynamic.cors_allowed_origins {
changes.push(ConfigChange {
key: "cors.allowed_origins".to_string(),
old_value: format!("{:?}", dynamic.cors_allowed_origins),
new_value: format!("{:?}", new_dynamic.cors_allowed_origins),
});
}
if dynamic.cors_max_age_seconds != new_dynamic.cors_max_age_seconds {
changes.push(ConfigChange {
key: "cors.max_age_seconds".to_string(),
old_value: dynamic.cors_max_age_seconds.to_string(),
new_value: new_dynamic.cors_max_age_seconds.to_string(),
});
}
// Apply the new configuration
*dynamic = new_dynamic;
Ok(changes)
}
}
log_level pub
#![allow(unused)]
fn main() {
fn log_level (& self) -> String
}
Get current log level
Source
#![allow(unused)]
fn main() {
pub fn log_level(&self) -> String {
self.dynamic
.read()
.map(|d| d.log_level.clone())
.unwrap_or_else(|_| "info".to_string())
}
}
diagnostic_cleanup_interval_seconds pub
#![allow(unused)]
fn main() {
fn diagnostic_cleanup_interval_seconds (& self) -> u64
}
Get diagnostic cleanup interval in seconds
Source
#![allow(unused)]
fn main() {
pub fn diagnostic_cleanup_interval_seconds(&self) -> u64 {
self.dynamic
.read()
.map(|d| d.diagnostic_cleanup_interval_seconds)
.unwrap_or(900)
}
}
diagnostic_max_age_hours pub
#![allow(unused)]
fn main() {
fn diagnostic_max_age_hours (& self) -> i64
}
Get diagnostic max age in hours
Source
#![allow(unused)]
fn main() {
pub fn diagnostic_max_age_hours(&self) -> i64 {
self.dynamic
.read()
.map(|d| d.diagnostic_max_age_hours)
.unwrap_or(1)
}
}
webhook_delivery_interval_seconds pub
#![allow(unused)]
fn main() {
fn webhook_delivery_interval_seconds (& self) -> u64
}
Get webhook delivery interval in seconds
Source
#![allow(unused)]
fn main() {
pub fn webhook_delivery_interval_seconds(&self) -> u64 {
self.dynamic
.read()
.map(|d| d.webhook_delivery_interval_seconds)
.unwrap_or(5)
}
}
webhook_delivery_batch_size pub
#![allow(unused)]
fn main() {
fn webhook_delivery_batch_size (& self) -> i64
}
Get webhook delivery batch size
Source
#![allow(unused)]
fn main() {
pub fn webhook_delivery_batch_size(&self) -> i64 {
self.dynamic
.read()
.map(|d| d.webhook_delivery_batch_size)
.unwrap_or(50)
}
}
webhook_cleanup_retention_days pub
#![allow(unused)]
fn main() {
fn webhook_cleanup_retention_days (& self) -> i64
}
Get webhook cleanup retention in days
Source
#![allow(unused)]
fn main() {
pub fn webhook_cleanup_retention_days(&self) -> i64 {
self.dynamic
.read()
.map(|d| d.webhook_cleanup_retention_days)
.unwrap_or(7)
}
}
cors_allowed_origins pub
#![allow(unused)]
fn main() {
fn cors_allowed_origins (& self) -> Vec < String >
}
Get CORS allowed origins
Source
#![allow(unused)]
fn main() {
pub fn cors_allowed_origins(&self) -> Vec<String> {
self.dynamic
.read()
.map(|d| d.cors_allowed_origins.clone())
.unwrap_or_else(|_| vec!["*".to_string()])
}
}
cors_max_age_seconds pub
#![allow(unused)]
fn main() {
fn cors_max_age_seconds (& self) -> u64
}
Get CORS max age in seconds
Source
#![allow(unused)]
fn main() {
pub fn cors_max_age_seconds(&self) -> u64 {
self.dynamic
.read()
.map(|d| d.cors_max_age_seconds)
.unwrap_or(3600)
}
}
dynamic_snapshot pub
#![allow(unused)]
fn main() {
fn dynamic_snapshot (& self) -> Option < DynamicConfig >
}
Get a snapshot of all dynamic config values
Source
#![allow(unused)]
fn main() {
pub fn dynamic_snapshot(&self) -> Option<DynamicConfig> {
self.dynamic.read().ok().map(|d| d.clone())
}
}
Functions
brokkr-utils::config::deserialize_string_or_vec
private
#![allow(unused)]
fn main() {
fn deserialize_string_or_vec < 'de , D > (deserializer : D) -> Result < Vec < String > , D :: Error > where D : Deserializer < 'de > ,
}
Deserializes a comma-separated string or array into Vec
Source
#![allow(unused)]
fn main() {
fn deserialize_string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, SeqAccess, Visitor};
use std::fmt;
struct StringOrVec;
impl<'de> Visitor<'de> for StringOrVec {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string or sequence of strings")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
// Split by comma and trim whitespace
Ok(value.split(',').map(|s| s.trim().to_string()).collect())
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut vec = Vec::new();
while let Some(item) = seq.next_element::<String>()? {
vec.push(item);
}
Ok(vec)
}
}
deserializer.deserialize_any(StringOrVec)
}
}
brokkr-utils::config::default_log_format
private
#![allow(unused)]
fn main() {
fn default_log_format () -> String
}
Source
#![allow(unused)]
fn main() {
fn default_log_format() -> String {
"text".to_string()
}
}
brokkr-utils::config::default_otlp_endpoint
private
#![allow(unused)]
fn main() {
fn default_otlp_endpoint () -> String
}
Source
#![allow(unused)]
fn main() {
fn default_otlp_endpoint() -> String {
"http://localhost:4317".to_string()
}
}
brokkr-utils::config::default_service_name
private
#![allow(unused)]
fn main() {
fn default_service_name () -> String
}
Source
#![allow(unused)]
fn main() {
fn default_service_name() -> String {
"brokkr".to_string()
}
}
brokkr-utils::config::default_sampling_rate
private
#![allow(unused)]
fn main() {
fn default_sampling_rate () -> f64
}
Source
#![allow(unused)]
fn main() {
fn default_sampling_rate() -> f64 {
0.1
}
}
brokkr-utils::logging Rust
Structs
brokkr-utils::logging::BrokkrLogger
pub
Custom logger for the Brokkr application
Functions
brokkr-utils::logging::init
pub
#![allow(unused)]
fn main() {
fn init (level : & str) -> Result < () , SetLoggerError >
}
Initializes the Brokkr logging system with the specified log level.
Sets up a custom logger that handles structured logging with timestamps, log levels, and module paths. Supports multiple output formats and concurrent logging from multiple threads.
Parameters:
| Name | Type | Description |
|---|---|---|
level | - | String representation of the log level (“debug”, “info”, “warn”, “error”) |
Returns:
Result<(), Box<dyn Error>>- Success/failure of logger initialization
Examples:
use brokkr_utils::logging;
logging::init("debug")?;
log::info!("Logger initialized successfully");
Source
#![allow(unused)]
fn main() {
pub fn init(level: &str) -> Result<(), SetLoggerError> {
init_with_format(level, "text")
}
}
brokkr-utils::logging::init_with_format
pub
#![allow(unused)]
fn main() {
fn init_with_format (level : & str , format : & str) -> Result < () , SetLoggerError >
}
Initializes the Brokkr logging system with the specified log level and format.
Parameters:
| Name | Type | Description |
|---|---|---|
level | - | String representation of the log level (“debug”, “info”, “warn”, “error”) |
format | - | Log output format (“text” for human-readable, “json” for structured JSON) |
Returns:
Result<(), SetLoggerError>- Success/failure of logger initialization
Source
#![allow(unused)]
fn main() {
pub fn init_with_format(level: &str, format: &str) -> Result<(), SetLoggerError> {
let level_filter = str_to_level_filter(level);
let use_json = format.eq_ignore_ascii_case("json");
INIT.get_or_init(|| {
log::set_logger(&LOGGER)
.map(|()| log::set_max_level(LevelFilter::Trace))
.expect("Failed to set logger");
});
JSON_FORMAT.store(use_json, Ordering::Relaxed);
CURRENT_LEVEL.store(level_filter as usize, Ordering::Relaxed);
log::set_max_level(level_filter);
Ok(())
}
}
brokkr-utils::logging::update_log_level
pub
#![allow(unused)]
fn main() {
fn update_log_level (level : & str) -> Result < () , String >
}
Updates the current log level.
Parameters:
| Name | Type | Description |
|---|---|---|
level | - | A string slice that holds the new desired log level. |
Returns:
Ok(())if the log level was successfully updated. *Err(String)if there was an error updating the log level.
Examples:
use brokkr_logger;
brokkr_logger::init("info").expect("Failed to initialize logger");
info!("This will be logged");
brokkr_logger::update_log_level("warn").expect("Failed to update log level");
info!("This will not be logged");
warn!("But this will be logged");
Source
#![allow(unused)]
fn main() {
pub fn update_log_level(level: &str) -> Result<(), String> {
let new_level = str_to_level_filter(level);
CURRENT_LEVEL.store(new_level as usize, Ordering::Relaxed);
log::set_max_level(new_level);
Ok(())
}
}
brokkr-utils::logging::str_to_level_filter
private
#![allow(unused)]
fn main() {
fn str_to_level_filter (level : & str) -> LevelFilter
}
Source
#![allow(unused)]
fn main() {
fn str_to_level_filter(level: &str) -> LevelFilter {
match level.to_lowercase().as_str() {
"off" => LevelFilter::Off,
"error" => LevelFilter::Error,
"warn" => LevelFilter::Warn,
"info" => LevelFilter::Info,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
_ => LevelFilter::Info,
}
}
}
brokkr-utils::logging::level_filter_from_u8
private
#![allow(unused)]
fn main() {
fn level_filter_from_u8 (v : u8) -> LevelFilter
}
Source
#![allow(unused)]
fn main() {
fn level_filter_from_u8(v: u8) -> LevelFilter {
match v {
0 => LevelFilter::Off,
1 => LevelFilter::Error,
2 => LevelFilter::Warn,
3 => LevelFilter::Info,
4 => LevelFilter::Debug,
5 => LevelFilter::Trace,
_ => LevelFilter::Off,
}
}
}
brokkr-utils::telemetry Rust
Enums
brokkr-utils::telemetry::TelemetryError pub
Error type for telemetry initialization
Variants
ExporterError- Failed to create OTLP exporterTracerError- Failed to initialize tracerSubscriberError- Failed to set global subscriber
Functions
brokkr-utils::telemetry::init
pub
#![allow(unused)]
fn main() {
fn init (config : & ResolvedTelemetry , log_level : & str , log_format : & str ,) -> Result < () , TelemetryError >
}
Initialize OpenTelemetry tracing with the given configuration.
If telemetry is disabled in the config, this function sets up a basic tracing subscriber without OpenTelemetry export.
Parameters:
| Name | Type | Description |
|---|---|---|
config | - | Resolved telemetry configuration (from Telemetry::for_broker() or for_agent()) |
log_level | - | Log level filter string (e.g., “info”, “debug”) |
log_format | - | Log format (“text” or “json”) |
Returns:
Ok(())on success *Err(TelemetryError)if initialization fails
Source
#![allow(unused)]
fn main() {
pub fn init(
config: &ResolvedTelemetry,
log_level: &str,
log_format: &str,
) -> Result<(), TelemetryError> {
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level));
if !config.enabled {
// Telemetry disabled - just set up basic tracing subscriber
let subscriber = tracing_subscriber::registry().with(env_filter);
if log_format.eq_ignore_ascii_case("json") {
subscriber
.with(tracing_subscriber::fmt::layer().json())
.try_init()
.map_err(|e| TelemetryError::SubscriberError(e.to_string()))?;
} else {
subscriber
.with(tracing_subscriber::fmt::layer())
.try_init()
.map_err(|e| TelemetryError::SubscriberError(e.to_string()))?;
}
return Ok(());
}
// Create OTLP exporter
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(&config.otlp_endpoint)
.build()
.map_err(|e| TelemetryError::ExporterError(e.to_string()))?;
// Create sampler based on sampling rate
let sampler = if config.sampling_rate >= 1.0 {
Sampler::AlwaysOn
} else if config.sampling_rate <= 0.0 {
Sampler::AlwaysOff
} else {
Sampler::TraceIdRatioBased(config.sampling_rate)
};
// Create tracer provider with resource attributes
let tracer_provider = opentelemetry_sdk::trace::TracerProvider::builder()
.with_batch_exporter(exporter, runtime::Tokio)
.with_sampler(sampler)
.with_resource(Resource::new(vec![
KeyValue::new(
opentelemetry_semantic_conventions::resource::SERVICE_NAME,
config.service_name.clone(),
),
KeyValue::new(
opentelemetry_semantic_conventions::resource::SERVICE_VERSION,
env!("CARGO_PKG_VERSION"),
),
]))
.build();
// Get tracer from provider
let tracer = tracer_provider.tracer(config.service_name.clone());
// Set global tracer provider
opentelemetry::global::set_tracer_provider(tracer_provider);
// Create OpenTelemetry tracing layer
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
// Build subscriber with OpenTelemetry layer
let subscriber = tracing_subscriber::registry()
.with(env_filter)
.with(otel_layer);
if log_format.eq_ignore_ascii_case("json") {
subscriber
.with(tracing_subscriber::fmt::layer().json())
.try_init()
.map_err(|e| TelemetryError::SubscriberError(e.to_string()))?;
} else {
subscriber
.with(tracing_subscriber::fmt::layer())
.try_init()
.map_err(|e| TelemetryError::SubscriberError(e.to_string()))?;
}
Ok(())
}
}
brokkr-utils::telemetry::shutdown
pub
#![allow(unused)]
fn main() {
fn shutdown ()
}
Shutdown OpenTelemetry, flushing any pending traces.
Should be called during graceful shutdown to ensure all traces are exported.
Source
#![allow(unused)]
fn main() {
pub fn shutdown() {
opentelemetry::global::shutdown_tracer_provider();
}
}