Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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
  • kubectl installed and configured
  • Rust toolchain (for building from source)
  • Docker (for container deployments)

Quick Navigation

  1. Installation - Install Brokkr on your system
  2. Quick Start - Get up and running quickly
  3. Configuration - Configure Brokkr for your environment

What’s Next?

After completing the getting started guide, you can:

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:

ParameterDescriptionDefault
postgresql.enabledEnable bundled PostgreSQLtrue
postgresql.auth.passwordPostgreSQL password (bundled)brokkr
postgresql.external.hostExternal database host""
postgresql.external.portExternal database port5432
postgresql.external.databaseDatabase namebrokkr
postgresql.external.usernameDatabase usernamebrokkr
postgresql.external.passwordDatabase passwordbrokkr
postgresql.external.schemaPostgreSQL schema (multi-tenant)""
replicaCountNumber of broker replicas1
image.tagImage tag to uselatest
broker.logLevelLog levelinfo
resources.limits.cpuCPU limit500m
resources.limits.memoryMemory limit512Mi
tls.enabledEnable TLSfalse

Agent Values

Key configuration options for the agent chart:

ParameterDescriptionDefault
broker.urlBroker URLRequired
broker.pakAgent PAK (Prefixed API Key)Required
broker.agentNameHuman-readable agent name""
broker.clusterNameName of the managed cluster""
agent.pollingIntervalSeconds between broker polls30
agent.deploymentHealth.enabledEnable deployment health checkstrue
agent.deploymentHealth.intervalSecondsHealth check interval60
rbac.createCreate RBAC resourcestrue
rbac.clusterWideCluster-wide RBAC (vs namespaced)true
rbac.secretAccess.enabledEnable secret accessfalse
resources.limits.cpuCPU limit200m
resources.limits.memoryMemory limit256Mi
image.tagImage tag to uselatest

For complete configuration options, see the chart values files:

Next Steps

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


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:

  1. Default values embedded in the application from default.toml
  2. Configuration file (optional) specified at startup
  3. 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.

VariableTypeDefaultDescription
BROKKR__DATABASE__URLstringpostgres://brokkr:brokkr@localhost:5433/brokkrPostgreSQL connection URL
BROKKR__DATABASE__SCHEMAstringNoneSchema 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.

VariableTypeDefaultDescription
BROKKR__LOG__LEVELstringdebugLog level: trace, debug, info, warn, error
BROKKR__LOG__FORMATstringtextLog 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

VariableTypeDefaultDescription
BROKKR__BROKER__PAK_HASHstringNonePre-computed PAK hash for admin authentication

Webhook Settings

Webhooks deliver event notifications to external systems. These settings control the delivery worker’s behavior.

VariableTypeDefaultDescription
BROKKR__BROKER__WEBHOOK_ENCRYPTION_KEYstringRandom64-character hex string for AES-256 encryption
BROKKR__BROKER__WEBHOOK_DELIVERY_INTERVAL_SECONDSinteger5Polling interval for pending webhook deliveries
BROKKR__BROKER__WEBHOOK_DELIVERY_BATCH_SIZEinteger50Maximum deliveries processed per batch
BROKKR__BROKER__WEBHOOK_CLEANUP_RETENTION_DAYSinteger7Days 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.

VariableTypeDefaultDescription
BROKKR__BROKER__DIAGNOSTIC_CLEANUP_INTERVAL_SECONDSinteger900Cleanup task interval (15 minutes)
BROKKR__BROKER__DIAGNOSTIC_MAX_AGE_HOURSinteger1Maximum age for diagnostic results

Audit Log Settings

Audit logs record all significant actions for security and compliance.

VariableTypeDefaultDescription
BROKKR__BROKER__AUDIT_LOG_RETENTION_DAYSinteger90Days 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.

VariableTypeDefaultDescription
BROKKR__BROKER__AUTH_CACHE_TTL_SECONDSinteger60TTL 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.

VariableTypeDefaultDescription
BROKKR__CORS__ALLOWED_ORIGINSlist["http://localhost:3001"]Allowed origins (use * for all)
BROKKR__CORS__ALLOWED_METHODSlist["GET", "POST", "PUT", "DELETE", "OPTIONS"]Allowed HTTP methods
BROKKR__CORS__ALLOWED_HEADERSlist["Content-Type", "Authorization"]Allowed request headers
BROKKR__CORS__MAX_AGE_SECONDSinteger3600Preflight 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

VariableTypeRequiredDescription
BROKKR__AGENT__BROKER_URLstringYesBroker API URL
BROKKR__AGENT__PAKstringYesPrefixed API Key for broker communication
BROKKR__AGENT__AGENT_NAMEstringYesHuman-readable agent name
BROKKR__AGENT__CLUSTER_NAMEstringYesName of the managed Kubernetes cluster

Polling Settings

VariableTypeDefaultDescription
BROKKR__AGENT__POLLING_INTERVALinteger10Seconds between broker polls
BROKKR__AGENT__MAX_RETRIESinteger60Maximum operation retry attempts
BROKKR__AGENT__MAX_EVENT_MESSAGE_RETRIESinteger2Maximum event reporting retry attempts
BROKKR__AGENT__EVENT_MESSAGE_RETRY_DELAYinteger5Seconds between event retry attempts

Health and Monitoring

VariableTypeDefaultDescription
BROKKR__AGENT__HEALTH_PORTinteger8080HTTP port for health endpoints
BROKKR__AGENT__DEPLOYMENT_HEALTH_ENABLEDbooleantrueEnable deployment health monitoring
BROKKR__AGENT__DEPLOYMENT_HEALTH_INTERVALinteger60Seconds between health checks

Kubernetes Settings

VariableTypeDefaultDescription
BROKKR__AGENT__KUBECONFIG_PATHstringNonePath 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

VariableTypeDefaultDescription
BROKKR__TELEMETRY__ENABLEDbooleanfalseEnable telemetry export
BROKKR__TELEMETRY__OTLP_ENDPOINTstringhttp://localhost:4317OTLP collector endpoint (gRPC)
BROKKR__TELEMETRY__SERVICE_NAMEstringbrokkrService name for traces
BROKKR__TELEMETRY__SAMPLING_RATEfloat0.1Sampling rate (0.0 to 1.0)

Component Overrides

The broker and agent can have independent telemetry configurations that override the base settings.

VariableDescription
BROKKR__TELEMETRY__BROKER__ENABLEDOverride enabled for broker
BROKKR__TELEMETRY__BROKER__OTLP_ENDPOINTOverride endpoint for broker
BROKKR__TELEMETRY__BROKER__SERVICE_NAMEOverride service name for broker
BROKKR__TELEMETRY__BROKER__SAMPLING_RATEOverride sampling rate for broker
BROKKR__TELEMETRY__AGENT__ENABLEDOverride enabled for agent
BROKKR__TELEMETRY__AGENT__OTLP_ENDPOINTOverride endpoint for agent
BROKKR__TELEMETRY__AGENT__SERVICE_NAMEOverride service name for agent
BROKKR__TELEMETRY__AGENT__SAMPLING_RATEOverride 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.

VariableTypeDefaultDescription
BROKKR__PAK__PREFIXstringbrokkrPAK string prefix
BROKKR__PAK__SHORT_TOKEN_PREFIXstringBRShort token prefix
BROKKR__PAK__SHORT_TOKEN_LENGTHinteger8Short token character count
BROKKR__PAK__LONG_TOKEN_LENGTHinteger24Long token character count
BROKKR__PAK__RNGstringosrngRandom number generator type
BROKKR__PAK__DIGESTinteger8Digest 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 verbosity
  • cors.allowed_origins - CORS origins
  • cors.max_age_seconds - CORS preflight cache
  • broker.diagnostic_cleanup_interval_seconds - Diagnostic cleanup interval
  • broker.diagnostic_max_age_hours - Diagnostic retention
  • broker.webhook_delivery_interval_seconds - Webhook delivery interval
  • broker.webhook_delivery_batch_size - Webhook batch size
  • broker.webhook_cleanup_retention_days - Webhook retention

Static Settings (Require Restart)

These settings require an application restart to change:

  • database.url - Database connection
  • database.schema - Database schema
  • broker.webhook_encryption_key - Encryption key
  • broker.pak_hash - Admin PAK hash
  • broker.auth_cache_ttl_seconds - Auth cache TTL
  • telemetry.* - All telemetry settings
  • pak.* - 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:

  1. Check the logs for detailed error messages
  2. Verify all required configuration values are set
  3. Test connectivity to external dependencies (database, Kubernetes API)
  4. 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.

TutorialWhat You’ll Learn
Deploy Your First ApplicationCreate a stack, add a deployment object, register an agent, and watch Kubernetes resources get applied
Multi-Cluster TargetingUse labels and annotations to direct deployments to specific agents
CI/CD with GeneratorsCreate a generator and use it from a CI/CD pipeline to push deployments
Standardized Deployments with TemplatesCreate 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 Guideangreal local up starts the full stack)
  • The admin PAK (Pre-Authentication Key) printed during first startup (check broker logs if you missed it)
  • curl and jq installed

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 like brokkr_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_at timestamp 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

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:

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

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:

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 curl POST 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

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:

Step 1: Understand the Template Concept

A Brokkr template has two parts:

  1. Template content — Kubernetes YAML with Tera placeholders (e.g., {{ replicas }}, {{ image_tag }})
  2. 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

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

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

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:

  1. Deliveries are queued for agents matching ALL specified labels
  2. The matching agent fetches pending deliveries during its polling loop
  3. The agent delivers the webhook from inside the cluster
  4. 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 events
  • workorder.* - All work order events
  • agent.* - All agent events
  • stack.* - 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_retries failures, 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 delivered
  • acquired - Claimed by broker or agent, delivery in progress
  • success - Successfully delivered
  • failed - Delivery failed, will retry
  • dead - 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

  1. Check if the subscription is enabled:

    curl "http://broker:3000/api/v1/webhooks/{id}" \
      -H "Authorization: Bearer $ADMIN_PAK"
    
  2. Check delivery status for failures:

    curl "http://broker:3000/api/v1/webhooks/{id}/deliveries?status=failed" \
      -H "Authorization: Bearer $ADMIN_PAK"
    
  3. Verify endpoint is reachable from broker/agent

Agent-Delivered Webhooks Failing

  1. Verify agent has matching labels:

    curl "http://broker:3000/api/v1/agents/{agent_id}" \
      -H "Authorization: Bearer $ADMIN_PAK"
    
  2. Check agent logs for delivery errors:

    kubectl logs -l app=brokkr-agent -c agent
    
  3. 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.

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:

PurposeExample Labels
Environmentproduction, staging, development
Regionus-east, eu-west, apac
Tierfrontend, backend, data
Criticalitycritical, 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:

PurposeExample
Cost allocationcost-center=team-alpha
Owner trackingowner=platform-team
SLA classificationsla-tier=gold
External referencesjira-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:

  1. Add labels to your stacks that represent their characteristics
  2. Add corresponding labels to agents that should manage those stacks
  3. 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.

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:

  1. The old PAK is immediately invalidated
  2. Update all CI/CD systems with the new PAK
  3. 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:

OperationAdmin PAKGenerator PAK
Create generatorsYesNo
List all generatorsYesNo
View own generatorYesYes
Update own generatorYesYes
Delete own generatorYesYes
Rotate own PAKYesYes
Create stacksYesYes
View own stacksYesYes
View other generators’ stacksYesNo
Manage agentsYesNo
Manage webhooksYesNo

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 Actions
  • gitlab-ci-staging - Staging pipeline in GitLab CI
  • jenkins-nightly-builds - Nightly build automation
  • team-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:

  1. Verify the PAK is correct and not expired
  2. Check if the PAK was rotated
  3. 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:

  1. Verify the resources were created with this generator’s PAK
  2. Resources created by other generators are not visible
  3. Use admin PAK to view all resources across generators

PAK Lost

If you’ve lost a generator’s PAK:

  1. Use the admin PAK to rotate: POST /api/v1/generators/{id}/rotate-pak
  2. Store the new PAK securely
  3. Update all systems using the old PAK

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:

StatusDescription
healthyAll pods are ready and running without issues
degradedSome pods have issues but the deployment is partially functional
failingThe deployment has failed or all pods are in error states
unknownHealth 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 image
  • ErrImagePull - Error pulling container image
  • CrashLoopBackOff - Container repeatedly crashing
  • CreateContainerConfigError - Invalid container configuration
  • InvalidImageName - Malformed image reference
  • RunContainerError - Error starting container
  • ContainerCannotRun - Container failed to run

Resource Issues:

  • OOMKilled - Container killed due to memory limits
  • Error - 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\""
    }
  ]
}
FieldDescription
pods_readyNumber of pods in Ready state
pods_totalTotal number of pods found
conditionsList of detected problematic conditions
resourcesPer-resource details (optional)

Common Scenarios

ImagePullBackOff

When the agent reports ImagePullBackOff:

  1. Verify the image name and tag are correct
  2. Check that the image exists in the registry
  3. Verify the cluster has network access to the registry
  4. 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:

  1. Check container logs for error messages:

    kubectl logs <pod-name> -n <namespace> --previous
    
  2. Verify the application configuration is correct

  3. Check resource limits aren’t too restrictive

  4. Ensure required environment variables and secrets are present

OOMKilled

When containers are killed for memory:

  1. Increase memory limits:

    resources:
      limits:
        memory: "512Mi"  # Increase as needed
    
  2. Investigate application memory usage

  3. Consider memory profiling to identify leaks

Unknown Status

When status shows as unknown:

  1. Verify pods exist for the deployment object
  2. Check the agent has RBAC permissions to list pods
  3. 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:

  1. Check the agent is running and connected:

    kubectl get pods -l app=brokkr-agent
    
  2. Verify health monitoring is enabled:

    kubectl get configmap brokkr-agent-config -o yaml
    
  3. 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:

  1. Verify pods have the correct deployment object ID label
  2. Check the health check interval - status may be stale
  3. Confirm the agent has permission to list pods across namespaces

High API Load

If health monitoring causes excessive Kubernetes API load:

  1. Increase the check interval
  2. Consider reducing the number of deployment objects per agent
  3. Monitor agent metrics for API call rates

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:

  1. Priority Resource Application: Namespaces and CustomResourceDefinitions are applied first. These resources must exist before namespaced resources can be validated or created.

  2. 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.

  3. 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.

  4. 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)
  5. 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:

  1. The agent identifies all cluster resources belonging to that stack
  2. Each resource is deleted from the cluster
  3. 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:

  1. Check agent status: Verify the agent is running and in ACTIVE status. Inactive agents skip deployment object requests.

  2. Check targeting: Confirm the stack is targeted to the agent via GET /api/v1/agents/{id}/targets.

  3. Check agent logs: Look for validation errors or API failures in the agent container logs.

  4. 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:

  1. Verify deletion marker: Check that a deletion marker deployment object was created for the stack.

  2. Check labels: Verify the resources have the k8s.brokkr.io/stack label. Resources without this label aren’t managed by Brokkr.

  3. 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:

  1. Check YAML syntax: Ensure the YAML in your deployment object is valid.

  2. Check API versions: Verify the apiVersion and kind are correct for your Kubernetes version.

  3. Check namespaces: If referencing a namespace, ensure it’s either included in the deployment object or already exists in the cluster.

  4. 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)

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:

  1. Name: Identifier for the template
  2. Template Content: Tera-templated YAML
  3. 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:

ConstraintTypeDescription
minLength, maxLengthstringString length limits
patternstringRegex pattern
minimum, maximumnumberNumeric bounds
enumanyAllowed values
minItems, maxItemsarrayArray 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:

  1. Validate template labels match the stack
  2. Validate parameters against the JSON Schema
  3. Render the template with Tera
  4. 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

TemplateStackResult
No labelsAny labelsMatches
env=prodenv=prod, team=platformMatches
env=prodenv=stagingNo match
env=prod, tier=1env=prodNo 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 degraded or failing health 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_id of the resource you want to diagnose
  • The agent_id of 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: pendingclaimedcompleted

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: Pending or Failed indicates problems
  • Conditions: Check Ready=False with the reason
  • Containers: Look for restart_count > 0, state=waiting with reasons like CrashLoopBackOff, or state=terminated with reason OOMKilled

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_minutes and 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).

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

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:

  1. Go to Settings → Secrets and variables → Actions
  2. Update the BROKKR_GENERATOR_PAK secret with the new value

GitLab CI:

  1. Go to Settings → CI/CD → Variables
  2. Update the BROKKR_GENERATOR_PAK variable

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.

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 brokkr user 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:

  1. Creates the schema (CREATE SCHEMA IF NOT EXISTS tenant_acme)
  2. Runs all database migrations within the schema
  3. Creates the admin role and generates an admin PAK
  4. 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.

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

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 MethodExample Use Case
Direct AssignmentAgent A manages Stack X specifically
Label-BasedAll “prod” agents manage all “prod” stacks
Annotation-BasedAgents 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:

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

ComponentCPU RequestMemory RequestCPU LimitMemory Limit
Broker100m256Mi500m512Mi
Agent50m128Mi200m256Mi
PostgreSQL250m256Mi500m512Mi

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 output
  • RUST_LOG=brokkr_broker=trace - Detailed broker tracing
  • RUST_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:

EndpointMethodPurpose
/api/v1/auth/pakPOSTVerify PAK and retrieve agent identity
/api/v1/agents/{id}/target-stateGETFetch deployment objects to apply
/api/v1/agents/{id}/eventsPOSTReport deployment outcomes
/api/v1/agents/{id}/heartbeatPOSTSend periodic heartbeat
/api/v1/agents/{id}/health-statusPATCHReport deployment health
/api/v1/agents/{id}/work-orders/pendingGETFetch claimable work orders
/api/v1/agents/{id}/diagnostics/pendingGETFetch 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:

  1. Namespaces are applied first as other resources may depend on them
  2. CustomResourceDefinitions are applied second as custom resources require their definitions
  3. 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:

EndpointPurpose
/healthzLiveness probe - returns 200 if process is alive
/readyzReadiness probe - returns 200 if agent can serve traffic
/healthDetailed health status with JSON response
/metricsPrometheus 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

  1. Configuration Loading - Parse environment variables and configuration files
  2. Database Connection - Establish r2d2 connection pool to PostgreSQL
  3. Migration Check - Verify database schema is current
  4. Encryption Initialization - Load or generate webhook encryption key
  5. Event Bus Initialization - Start event dispatcher with mpsc channel
  6. Audit Logger Initialization - Start background writer with batching
  7. Background Tasks - Spawn diagnostic cleanup, work order, webhook, and audit tasks
  8. API Server - Bind to configured port and start accepting requests

Agent Startup Sequence

  1. Configuration Loading - Parse environment variables
  2. PAK Verification - Authenticate with broker and retrieve agent identity
  3. Kubernetes Client - Initialize kube-rs client with in-cluster or kubeconfig credentials
  4. Health Server - Start HTTP server for probes and metrics
  5. Control Loop - Enter main loop with polling, health checks, and work order processing

Graceful Shutdown

Both components handle SIGTERM and SIGINT for graceful shutdown:

  1. Stop accepting new requests
  2. Complete in-flight operations
  3. Flush pending data (audit logs, events)
  4. Close database connections
  5. 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_at for 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.

SourceDestinationPortProtocolDirectionRequiredPurpose
Admin/UIBroker3000HTTPSInboundYesAPI access, management operations
GeneratorBroker3000HTTPSInboundYesStack and deployment object creation
AgentBroker3000HTTPSOutboundYesFetch deployments, report events
BrokerPostgreSQL5432TCPInternalYesDatabase operations
AgentK8s API6443HTTPSLocalYesResource management
BrokerWebhook endpoints443HTTPSOutboundOptionalEvent notifications
PrometheusBroker3000HTTPInboundOptionalMetrics scraping at /metrics
PrometheusAgent8080HTTPInboundOptionalMetrics scraping at /metrics
BrokerOTLP Collector4317gRPCOutboundOptionalDistributed tracing
AgentOTLP Collector4317gRPCOutboundOptionalDistributed 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.

DestinationPortProtocolPurpose
Broker API3000HTTPSFetch deployments, report events
Kubernetes API6443HTTPSManage cluster resources
OTLP Collector4317gRPCTelemetry (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:

DirectionPortProtocolSource/DestinationPurpose
Inbound3000 (or 443 via ingress)TCPAgents, Admins, GeneratorsAPI access
Outbound5432TCPPostgreSQL databaseDatabase connectivity
Outbound443TCPWebhook endpointsEvent delivery

For the agent host:

DirectionPortProtocolSource/DestinationPurpose
Outbound3000 or 443TCPBrokerAPI communication
Outbound6443TCPKubernetes API serverCluster 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 TypeTriggerData Included
APPLIEDResource successfully appliedResource details, timestamp
UPDATEDResource successfully updatedResource details, changes
DELETEDResource successfully deletedResource details
FAILEDOperation failedError message, resource details
HEALTH_CHECKPeriodic health statusDeployment 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.

CategoryEvent TypesDescription
Agentagent.registered, agent.deregisteredAgent lifecycle events
Stackstack.created, stack.deletedStack lifecycle events
Deploymentdeployment.created, deployment.applied, deployment.failed, deployment.deletedDeployment object lifecycle and application results
Work Orderworkorder.created, workorder.claimed, workorder.completed, workorder.failedWork 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.

StateDescriptionTransitions To
PENDINGAwaiting claim by an agentCLAIMED
CLAIMEDAgent is processingSUCCESS (to log), RETRY_PENDING
RETRY_PENDINGScheduled for retry after failurePENDING (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 TypeDefault RetentionCleanup Method
Deployment objectsPermanentSoft delete only
Agent eventsPermanentSoft delete only
Webhook deliveries7 daysBackground cleanup task
Audit logs90 daysBackground cleanup task
Diagnostic results1 hourBackground 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:

  1. 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.

  2. A separate random long token is generated with sufficient entropy for cryptographic security. This token never leaves the generation response.

  3. The long token is hashed using SHA-256, producing a fixed-size digest that represents the secret without revealing it.

  4. The database stores only the hash. The original long token exists only in the complete PAK string returned to the caller.

  5. 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

PropertyImplementation
SecrecyLong token never stored; only SHA-256 hash persisted
Non-repudiationPAK uniquely identifies the acting entity
RevocationEntity can be disabled; PAK immediately invalid
RotationNew PAK generated via rotate endpoint; old one invalidated
PerformanceIndexed 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

RoleAuthenticationCapabilities
AgentPAK via agents tableRead targeted deployments, report events, claim work orders
GeneratorPAK via generators tableManage own stacks and deployment objects
AdminPAK via admin_role tableFull system access including configuration and audit logs
SystemInternal onlyBackground tasks, automated cleanup

Endpoint Authorization

The following table summarizes which roles can access each API endpoint category:

Endpoint PatternAgentGeneratorAdmin
/api/v1/agents/{id}/target-stateOwn ID onlyNoYes
/api/v1/agents/{id}/eventsOwn ID onlyNoYes
/api/v1/agents/{id}/work-orders/*Own ID onlyNoYes
/api/v1/stacks/*NoOwn stacksYes
/api/v1/agents/* (management)NoNoYes
/api/v1/admin/*NoNoYes
/api/v1/webhooks/*NoNoYes
/healthz, /readyzYesYesYes
/metricsNoNoYes

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 TypeStorage LocationProtection
PAK hashesPostgreSQLSHA-256 hash (plaintext never stored)
Webhook URLsPostgreSQLAES-256-GCM encryption
Webhook auth headersPostgreSQLAES-256-GCM encryption
Database passwordKubernetes SecretBase64 encoding (use sealed-secrets in production)
Webhook encryption keyEnvironment variableShould 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:

FieldDescription
timestampWhen the action occurred (UTC)
actor_typeIdentity type: admin, agent, generator, or system
actor_idUUID of the acting entity (if applicable)
actionWhat happened (e.g., agent.created, pak.rotated)
resource_typeType of affected resource
resource_idUUID of affected resource (if applicable)
detailsStructured JSON with action-specific data
ip_addressClient IP address (when available)
user_agentClient 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 authentication
  • auth.failed - Failed authentication attempt
  • pak.created - New PAK generated
  • pak.rotated - PAK rotated
  • pak.deleted - PAK revoked

Resource Lifecycle tracks creation, modification, and deletion:

  • agent.created, agent.updated, agent.deleted
  • stack.created, stack.updated, stack.deleted
  • generator.created, generator.updated, generator.deleted
  • webhook.created, webhook.updated, webhook.deleted

Operational Events record system activities:

  • workorder.created, workorder.claimed, workorder.completed, workorder.failed
  • config.reloaded - Configuration hot-reload triggered
  • webhook.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:

IndicatorAlert ThresholdPotential Issue
Failed authentication rate> 10/minuteBrute force attack
Unexpected agent disconnectionsAnyPossible compromise or network attack
Webhook delivery failure rate> 50%Network issues or endpoint compromise
Audit log volume spike10x normalUnusual activity, possible attack
Admin action from unknown IPAnyCredential theft

Incident Response

Suspected Agent Compromise

If you suspect an agent’s credentials have been compromised:

  1. Revoke immediately: Delete or disable the agent via the admin API
  2. Review audit logs: Search for unusual actions by the agent’s actor_id
  3. Inspect cluster: Review resources the agent may have created or modified
  4. Rotate secrets: Generate new PAK if re-enabling the agent
  5. Investigate: Determine how the compromise occurred

Suspected Broker Compromise

If you suspect the broker itself has been compromised:

  1. Isolate: Remove external network access to the broker
  2. Preserve evidence: Capture logs, database state, and container images
  3. Rotate all credentials: Generate new PAKs for all agents, generators, and admins
  4. Review webhooks: Check for unauthorized webhook subscriptions
  5. Audit database: Look for unauthorized modifications to stacks or agents
  6. 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

RequirementBrokkr Feature
Access controlPAK authentication + implicit RBAC
Audit trailImmutable audit logs with comprehensive action recording
Data encryptionTLS in transit, AES-256-GCM for secrets at rest
Least privilegeScoped agent and generator access
MonitoringMetrics endpoint, audit log queries
Incident responseCredential 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 pull open-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.3 for no automatic updates
  • Use v1.2 to get patch updates automatically
  • Use v1 to track the major version
  • Use latest for 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_TOKEN with automatic permissions
  • Manual publishing: Requires Personal Access Token with write:packages scope
  • 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 → main tag and SHA tags
  • Develop branch pushes → develop tag 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

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:

  1. Admin creates a work order in the broker
  2. Broker determines eligible agents via targeting rules
  3. Agents poll for pending work orders they’re authorized to claim
  4. One agent claims and executes the work order
  5. 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

RetryBackoff (60s base)Wait
1st2¹ × 602 minutes
2nd2² × 604 minutes
3rd2³ × 608 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 TableLog Table
work_orderswork_order_log
Mutable (status changes)Immutable (write-once)
Current/pending workHistorical record
Cleaned up on completionRetained 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.

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:

  1. Creates a Tera context from the JSON parameters (flat key-value mapping)
  2. Adds the parameter values to the context
  3. Renders the template content through Tera
  4. 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 used
  • rendered_deployment_objects.template_version — which version
  • rendered_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:

FeatureTeraGo templatesJinja2Handlebars
LanguageRust-nativeGoPythonJS/Rust
Syntax{{ var }}, {% if %}{{ .Var }}, {{ if }}{{ var }}, {% if %}{{ var }}, {{#if}}
FiltersRich built-inLimitedRichLimited
Whitespace controlYesYesYesYes
Safe by defaultYes (auto-escape)NoYes (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 description field in each property serves as parameter documentation
  • Client-side validation — CI/CD systems can validate parameters before hitting the API

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 check
  • GET /readyz - Readiness check
  • GET /api/v1/health - Detailed health diagnostics

Agent Management

  • POST /api/v1/agents - Create a new agent
  • GET /api/v1/agents - List all agents
  • GET /api/v1/agents/{agent_id} - Get agent details
  • PUT /api/v1/agents/{agent_id} - Update an agent
  • DELETE /api/v1/agents/{agent_id} - Delete an agent

Stack Management

  • POST /api/v1/stacks - Create a new stack
  • GET /api/v1/stacks - List all stacks
  • GET /api/v1/stacks/{stack_id} - Get stack details
  • PUT /api/v1/stacks/{stack_id} - Update a stack
  • DELETE /api/v1/stacks/{stack_id} - Delete a stack

Deployment Object Management

  • POST /api/v1/stacks/{stack_id}/deployment-objects - Create a deployment object
  • GET /api/v1/stacks/{stack_id}/deployment-objects - List deployment objects

Event Management

  • POST /api/v1/events - Report a deployment event
  • GET /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:

View Rust API Documentation

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.

MethodEndpointDescription
GET/stacksList all stacks
POST/stacksCreate a new stack
GET/stacks/:idGet stack by ID
PUT/stacks/:idUpdate a stack
DELETE/stacks/:idDelete a stack
GET/stacks/:id/labelsList stack labels
POST/stacks/:id/labelsAdd label to stack
DELETE/stacks/:id/labels/:labelRemove label from stack
GET/stacks/:id/annotationsList stack annotations
POST/stacks/:id/annotationsAdd annotation to stack
DELETE/stacks/:id/annotations/:keyRemove annotation
GET/stacks/:id/deployment-objectsList deployment objects
POST/stacks/:id/deployment-objectsCreate deployment object
POST/stacks/:id/deployment-objects/from-templateInstantiate template

Agents

Agents run in Kubernetes clusters and apply deployment objects.

MethodEndpointDescription
GET/agentsList all agents
POST/agentsRegister a new agent
GET/agents/:idGet agent by ID
PUT/agents/:idUpdate an agent
DELETE/agents/:idDelete an agent
GET/agents/:id/target-stateGet agent’s target state
POST/agents/:id/heartbeatRecord agent heartbeat
GET/agents/:id/labelsList agent labels
POST/agents/:id/labelsAdd label to agent
GET/agents/:id/annotationsList agent annotations
POST/agents/:id/annotationsAdd annotation to agent
GET/agents/:id/stacksList agent’s associated stacks
GET/agents/:id/targetsList agent’s stack targets
POST/agents/:id/targetsAdd stack target
DELETE/agents/:id/targets/:stack_idRemove stack target
POST/agents/:id/rotate-pakRotate agent PAK

Templates

Reusable stack templates with Tera templating and JSON Schema validation.

MethodEndpointDescription
GET/templatesList all templates
POST/templatesCreate a new template
GET/templates/:idGet template by ID
PUT/templates/:idUpdate a template
DELETE/templates/:idDelete a template
GET/templates/:id/labelsList template labels
POST/templates/:id/labelsAdd label to template
GET/templates/:id/annotationsList template annotations
POST/templates/:id/annotationsAdd annotation to template

Work Orders

Transient operations like container builds routed to agents.

MethodEndpointDescription
GET/work-ordersList all work orders
POST/work-ordersCreate a new work order
GET/work-orders/:idGet work order by ID
DELETE/work-orders/:idCancel a work order
POST/work-orders/:id/claimClaim a work order (agent)
POST/work-orders/:id/completeComplete a work order (agent)
GET/agents/:id/work-orders/pendingGet pending work orders for agent
GET/work-order-logList completed work orders
GET/work-order-log/:idGet completed work order details

Generators

External systems that create deployment objects.

MethodEndpointDescription
GET/generatorsList all generators
POST/generatorsCreate a new generator
GET/generators/:idGet generator by ID
PUT/generators/:idUpdate a generator
DELETE/generators/:idDelete a generator
POST/generators/:id/rotate-pakRotate generator PAK

Other Endpoints

MethodEndpointDescription
GET/agent-eventsList agent events
GET/agent-events/:idGet agent event by ID
GET/deployment-objects/:idGet deployment object by ID
POST/auth/pakVerify a PAK

Webhooks

MethodEndpointDescription
GET/webhooksList webhook subscriptions
POST/webhooksCreate webhook subscription
GET/webhooks/event-typesList available event types
GET/webhooks/:idGet webhook subscription
PUT/webhooks/:idUpdate webhook subscription
DELETE/webhooks/:idDelete webhook subscription
POST/webhooks/:id/testTest webhook delivery
GET/webhooks/:id/deliveriesList webhook deliveries

Admin

MethodEndpointDescription
GET/admin/audit-logsQuery audit logs
POST/admin/config/reloadReload broker configuration

Health Monitoring

MethodEndpointDescription
GET/deployment-objects/:id/healthGet deployment health
GET/deployment-objects/:id/diagnosticsGet diagnostics
POST/deployment-objects/:id/diagnosticsRequest diagnostic

Health Endpoints

The broker exposes health endpoints (not under /api/v1/):

EndpointDescription
/healthzBasic health check
/readyzReadiness probe
/metricsPrometheus 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 found
  • 422 - 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:

PathPurpose
/api/v1/*REST API (see API Reference)
/healthzLiveness probe
/readyzReadiness probe
/metricsPrometheus metrics
/swagger-uiInteractive 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:

FlagRequiredDescription
--nameYesHuman-readable agent name
--cluster-nameYesName 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:

FlagRequiredDescription
--nameYesGenerator name (1-255 characters)
--descriptionNoOptional 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:

FlagRequiredDescription
--uuidYesThe agent’s UUID

brokkr-broker rotate generator

Rotates a generator’s PAK.

brokkr-broker rotate generator --uuid <uuid>

Flags:

FlagRequiredDescription
--uuidYesThe 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):

PathPurpose
/healthzLiveness probe (always 200 OK)
/readyzReadiness probe (checks K8s + broker connectivity)
/healthDetailed health status (JSON)
/metricsPrometheus metrics

Configuration

Both binaries read configuration from the same layered system:

  1. Embedded defaults (default.toml compiled into the binary)
  2. Configuration file (optional, path passed at startup or via BROKKR_CONFIG_FILE)
  3. 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

CodeMeaning
0Clean shutdown
1Startup failure (database, config, migration error)
130SIGINT (Ctrl+C) received
143SIGTERM 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

VariableTypeDefaultDescription
BROKKR__DATABASE__URLStringpostgres://brokkr:brokkr@localhost:5432/brokkrPostgreSQL connection URL
BROKKR__DATABASE__SCHEMAString(none)Schema name for multi-tenant isolation. When set, all queries use this schema.

Logging

VariableTypeDefaultDescription
BROKKR__LOG__LEVELStringdebugLog level: trace, debug, info, warn, error
BROKKR__LOG__FORMATStringtextLog format: text (human-readable) or json (structured)

The log level is hot-reloadable — changes take effect without restarting.

Broker

VariableTypeDefaultDescription
BROKKR__BROKER__PAK_HASHString(generated)Admin PAK hash (set during first startup)
BROKKR__BROKER__DIAGNOSTIC_CLEANUP_INTERVAL_SECONDSInteger900Interval for diagnostic cleanup task (seconds)
BROKKR__BROKER__DIAGNOSTIC_MAX_AGE_HOURSInteger1Max age for completed diagnostics before deletion (hours)
BROKKR__BROKER__WEBHOOK_ENCRYPTION_KEYString(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_SECONDSInteger5Webhook delivery worker poll interval (seconds)
BROKKR__BROKER__WEBHOOK_DELIVERY_BATCH_SIZEInteger50Max webhook deliveries processed per batch
BROKKR__BROKER__WEBHOOK_CLEANUP_RETENTION_DAYSInteger7How long to keep completed/dead webhook deliveries (days)
BROKKR__BROKER__AUDIT_LOG_RETENTION_DAYSInteger90How long to keep audit log entries (days)
BROKKR__BROKER__AUTH_CACHE_TTL_SECONDSInteger60TTL for PAK authentication cache (seconds). Set to 0 to disable caching.

Agent

VariableTypeDefaultDescription
BROKKR__AGENT__BROKER_URLStringhttp://localhost:3000Broker API base URL
BROKKR__AGENT__POLLING_INTERVALInteger10How often to poll broker for updates (seconds)
BROKKR__AGENT__KUBECONFIG_PATHString(in-cluster)Path to kubeconfig file. If unset, uses in-cluster configuration.
BROKKR__AGENT__MAX_RETRIESInteger60Max retries when waiting for broker on startup
BROKKR__AGENT__PAKString(required)Agent’s PAK for broker authentication
BROKKR__AGENT__AGENT_NAMEStringDEFAULTAgent name (must match broker registration)
BROKKR__AGENT__CLUSTER_NAMEStringDEFAULTCluster name (must match broker registration)
BROKKR__AGENT__MAX_EVENT_MESSAGE_RETRIESInteger2Max retries for event message delivery
BROKKR__AGENT__EVENT_MESSAGE_RETRY_DELAYInteger5Delay between event message retries (seconds)
BROKKR__AGENT__HEALTH_PORTInteger8080Port for agent health check HTTP server
BROKKR__AGENT__DEPLOYMENT_HEALTH_ENABLEDBooleantrueEnable deployment health checking
BROKKR__AGENT__DEPLOYMENT_HEALTH_INTERVALInteger60Interval for deployment health checks (seconds)

PAK (Pre-Authentication Key) Generation

VariableTypeDefaultDescription
BROKKR__PAK__PREFIXStringbrokkrPrefix for generated PAKs
BROKKR__PAK__RNGStringosrngRandom number generator type
BROKKR__PAK__DIGESTInteger8Digest algorithm identifier
BROKKR__PAK__SHORT_TOKEN_LENGTHInteger8Length of the short token portion
BROKKR__PAK__LONG_TOKEN_LENGTHInteger24Length of the long token portion
BROKKR__PAK__SHORT_TOKEN_PREFIXStringBRPrefix for the short token

Generated PAK format: {prefix}_{short_token_prefix}{short_token}_{long_token}

Example: brokkr_BR3rVsDa_GK3QN7CDUzYc6iKgMkJ98M2WSimM5t6U8

CORS

VariableTypeDefaultDescription
BROKKR__CORS__ALLOWED_ORIGINSString (comma-separated)http://localhost:3001Allowed CORS origins. Use * to allow all (not recommended for production).
BROKKR__CORS__ALLOWED_METHODSString (comma-separated)GET,POST,PUT,DELETE,OPTIONSAllowed HTTP methods
BROKKR__CORS__ALLOWED_HEADERSString (comma-separated)Authorization,Content-TypeAllowed request headers
BROKKR__CORS__MAX_AGE_SECONDSInteger3600Preflight 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

VariableTypeDefaultDescription
BROKKR__TELEMETRY__ENABLEDBooleanfalseEnable OpenTelemetry tracing
BROKKR__TELEMETRY__OTLP_ENDPOINTStringhttp://localhost:4317OTLP gRPC endpoint for trace export
BROKKR__TELEMETRY__SERVICE_NAMEStringbrokkrService name for traces
BROKKR__TELEMETRY__SAMPLING_RATEFloat0.1Sampling 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.

VariableTypeDefaultDescription
BROKKR__TELEMETRY__BROKER__ENABLEDBoolean(inherits)Override enabled for broker
BROKKR__TELEMETRY__BROKER__OTLP_ENDPOINTString(inherits)Override OTLP endpoint for broker
BROKKR__TELEMETRY__BROKER__SERVICE_NAMEStringbrokkr-brokerOverride service name for broker
BROKKR__TELEMETRY__BROKER__SAMPLING_RATEFloat(inherits)Override sampling rate for broker

Agent-Specific Overrides

VariableTypeDefaultDescription
BROKKR__TELEMETRY__AGENT__ENABLEDBoolean(inherits)Override enabled for agent
BROKKR__TELEMETRY__AGENT__OTLP_ENDPOINTString(inherits)Override OTLP endpoint for agent
BROKKR__TELEMETRY__AGENT__SERVICE_NAMEStringbrokkr-agentOverride service name for agent
BROKKR__TELEMETRY__AGENT__SAMPLING_RATEFloat(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:

VariableTypeDefaultDescription
BROKKR_CONFIG_FILEString(none)Path to TOML configuration file
BROKKR_CONFIG_WATCHER_ENABLEDBoolean(auto)Enable/disable ConfigMap hot-reload watcher
BROKKR_CONFIG_WATCHER_DEBOUNCE_SECONDSInteger5Debounce 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.level
  • broker.diagnostic_cleanup_interval_seconds
  • broker.diagnostic_max_age_hours
  • broker.webhook_delivery_interval_seconds
  • broker.webhook_delivery_batch_size
  • broker.webhook_cleanup_retention_days
  • cors.allowed_origins
  • cors.max_age_seconds

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

FieldTypeDescription
idUUIDUnique identifier
created_atDateTimeCreation timestamp
updated_atDateTimeLast update timestamp
deleted_atDateTime?Soft deletion timestamp
generator_idUUID?Owning generator (NULL = system template, admin-only)
nameStringTemplate name (1-255 characters)
descriptionString?Optional description
versionIntegerVersion number (starts at 1, auto-increments)
template_contentStringTera template (Kubernetes YAML with placeholders)
parameters_schemaStringJSON Schema defining valid parameters
checksumStringSHA-256 hash of template_content

Constraints:

  • Unique combination of (generator_id, name, version)
  • version must be >= 1
  • name, template_content, and parameters_schema cannot be empty
  • checksum is auto-computed on creation

Template Types

Typegenerator_idCreated ByVisible To
System templateNULLAdminAdmin + all generators
Generator templateUUIDGeneratorAdmin + owning generator

RenderedDeploymentObject

When a template is instantiated, Brokkr records the provenance:

FieldTypeDescription
idUUIDUnique identifier
deployment_object_idUUIDResulting deployment object
template_idUUIDSource template
template_versionIntegerVersion used
template_parametersString (JSON)Parameters provided
created_atDateTimeInstantiation timestamp

API Endpoints

List Templates

GET /api/v1/templates

Auth: Admin sees all templates. Generator sees system templates + own templates.

Response: 200 OKStackTemplate[]


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 CreatedStackTemplate


Get Template

GET /api/v1/templates/{id}

Auth: Admin or owning generator.

Response: 200 OKStackTemplate


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 name field is not accepted on update — it is preserved from the existing template.

Response: 200 OKStackTemplate (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:

  1. Fetches the latest version of the template
  2. Validates parameters against the JSON Schema
  3. Checks template-to-stack matching rules (labels/annotations)
  4. Renders the Tera template with the provided parameters
  5. Creates a deployment object with the rendered YAML
  6. Records the rendered deployment object provenance

Response: 200 OKDeploymentObject[]


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

FilterUsageResult
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:

  1. Template with no labels and no annotations → matches any stack (universal)
  2. Template with labels → stack must have all of the template’s labels
  3. Template with annotations → stack must have all of the template’s annotations (key-value match)
  4. 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 (missing tier:frontend)
  • Stack with ["env:staging", "tier:frontend"]no match (wrong env)

Versioning Behavior

  • Creating a template starts at version 1
  • Updating via PUT auto-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_objects table records which version was used

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

AspectDeployment ObjectWork Order
PurposePersistent stateOne-time operation
LifecycleApplied, reconciled, deletedCreated, claimed, completed
ExamplesDeployments, ConfigMapsContainer builds, tests
StoragePermanent in stackMoved to log after completion

Work Order Lifecycle

PENDING -> CLAIMED -> (success) -> work_order_log
                  \-> (failure) -> RETRY_PENDING -> PENDING (retry)
                                \-> work_order_log (max retries)
  1. PENDING: Work order created, waiting for an agent to claim
  2. CLAIMED: Agent has claimed the work order and is executing
  3. RETRY_PENDING: Execution failed, waiting for retry backoff
  4. 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:

FieldTypeRequiredDefaultDescription
work_typestringYes-Type of work (e.g., “build”)
yaml_contentstringYes-YAML content for the work
max_retriesintegerNo3Maximum retry attempts
backoff_secondsintegerNo60Base backoff for exponential retry
claim_timeout_secondsintegerNo3600Seconds before claimed work is considered stale
targetingobjectYes-Targeting configuration
targeting.agent_idsarrayNo-Direct agent UUIDs
targeting.labelsarrayNo-Agent labels to match
targeting.annotationsobjectNo-Agent annotations to match

List Work Orders

GET /api/v1/work-orders?status=PENDING&work_type=build
Authorization: Bearer <admin-pak>

Query Parameters:

ParameterDescription
statusFilter by status (PENDING, CLAIMED, RETRY_PENDING)
work_typeFilter 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:

FieldTypeDescription
successbooleanWhether the work completed successfully
messagestringOptional result message (image digest on success, error on failure)
retryablebooleanWhether 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:

FieldTypeDescription
last_errorstringError message from the most recent failed attempt (null if no failures)
last_error_attimestampWhen the last error occurred (null if no failures)
retry_countintegerNumber of retry attempts so far
next_retry_aftertimestampWhen 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:

ParameterDescription
work_typeFilter by work type
successFilter by success status (true/false)
agent_idFilter by agent that executed
limitMaximum results to return

Retry Behavior

When a work order fails:

  1. Agent reports failure via /complete with success: false
  2. Broker increments retry_count
  3. If retry_count < max_retries:
    • Status set to RETRY_PENDING
    • next_retry_after calculated with exponential backoff
    • After backoff period, status returns to PENDING
  4. If retry_count >= max_retries:
    • Work order moved to work_order_log with success: false

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:

  1. The work order’s claimed_at timestamp is compared against the current time
  2. If the elapsed time exceeds claim_timeout_seconds, the claim is released
  3. The work order status returns to PENDING
  4. The claimed_by field is cleared, allowing any eligible agent to claim it
  5. The retry_count is 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_seconds value

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 management
  • work_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 TypeDescriptionPayload Fields
agent.registeredAgent registered with brokeragent_id, name, cluster
agent.deregisteredAgent deregisteredagent_id, name

Stack Events

Event TypeDescriptionPayload Fields
stack.createdNew stack createdstack_id, name, created_at
stack.deletedStack soft-deletedstack_id, deleted_at

Deployment Events

Event TypeDescriptionPayload Fields
deployment.createdNew deployment object createddeployment_object_id, stack_id, sequence_id
deployment.appliedDeployment successfully applied by agentdeployment_object_id, agent_id, status
deployment.failedDeployment failed to applydeployment_object_id, agent_id, error
deployment.deletedDeployment object soft-deleteddeployment_object_id, stack_id

Work Order Events

Event TypeDescriptionPayload Fields
workorder.createdNew work order createdwork_order_id, work_type, status
workorder.claimedWork order claimed by agentwork_order_id, agent_id, claimed_at
workorder.completedWork order completed successfullywork_order_log_id, work_type, success, result_message
workorder.failedWork order failedwork_order_log_id, work_type, success, result_message

Wildcard Patterns

PatternMatches
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
}
FieldTypeDefaultDescription
namestringrequiredHuman-readable subscription name
urlstringrequiredWebhook endpoint URL (encrypted at rest)
auth_headerstringnullAuthorization header value (encrypted at rest)
event_typesstring[]requiredEvent types to subscribe to
filtersobjectnullFilter events by agent/stack/labels
target_labelsstring[]nullLabels for agent-based delivery
max_retriesint5Maximum delivery retry attempts
timeout_secondsint30HTTP request timeout
validateboolfalseSend 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:

ParameterTypeDefaultDescription
statusstringnullFilter by status
limitint50Maximum results
offsetint0Pagination 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:

  1. Event occurs and is emitted
  2. Broker matches event to subscriptions
  3. Broker creates delivery records
  4. Background task claims and delivers via HTTP POST
  5. Success/failure is recorded

Use for external endpoints accessible from the broker.

Agent Delivery

When target_labels is set, matching agents deliver webhooks:

  1. Event occurs and is emitted
  2. Broker creates delivery with target_labels
  3. Agent polls for pending deliveries during heartbeat loop
  4. Agent claims deliveries matching its labels
  5. Agent delivers via HTTP POST from inside cluster
  6. 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 LabelsAgent LabelsCan Claim?
["env:prod"]["env:prod", "region:us"]Yes
["env:prod", "region:us"]["env:prod"]No
["env:prod"]["env:staging"]No

Delivery Status

StatusDescription
pendingWaiting to be claimed and delivered
acquiredClaimed by broker or agent, delivery in progress
successSuccessfully delivered (HTTP 2xx)
failedDelivery failed, will retry after backoff
deadMax 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

ColumnTypeDescription
idUUIDPrimary key
nameVARCHAR(255)Subscription name
url_encryptedBYTEAEncrypted webhook URL
auth_header_encryptedBYTEAEncrypted auth header (nullable)
event_typesTEXT[]Event type patterns
filtersTEXTJSON-encoded filters (nullable)
target_labelsTEXT[]Labels for agent delivery (nullable)
enabledBOOLEANWhether subscription is active
max_retriesINTMax delivery attempts
timeout_secondsINTHTTP timeout
created_atTIMESTAMPCreation timestamp
updated_atTIMESTAMPLast update timestamp
created_byVARCHAR(255)Creator identifier

webhook_deliveries

ColumnTypeDescription
idUUIDPrimary key
subscription_idUUIDForeign key to subscription
event_typeVARCHAR(100)Event type
event_idUUIDIdempotency key
payloadTEXTJSON event payload
target_labelsTEXT[]Copied from subscription
statusVARCHAR(20)Delivery status
acquired_byUUIDAgent ID (nullable, NULL = broker)
acquired_untilTIMESTAMPTTL for claim
attemptsINTNumber of attempts
last_attempt_atTIMESTAMPLast attempt time
next_retry_atTIMESTAMPNext retry time
last_errorTEXTError from last attempt
created_atTIMESTAMPCreation timestamp
completed_atTIMESTAMPCompletion 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_KEY environment variable
  • Fields encrypted: url_encrypted, auth_header_encrypted
  • Response handling: API responses show has_url: true and has_auth_header: true/false rather 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

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

FieldTypeDescription
idUUIDUnique identifier
namestringHuman-readable name (unique, non-null)
descriptionstringOptional description
pak_hashstringHashed PAK (never returned in API responses)
created_attimestampCreation timestamp
updated_attimestampLast update timestamp
deleted_attimestampSoft-delete timestamp (null if active)
last_active_attimestampLast activity timestamp (null if never active)
is_activebooleanWhether the generator is currently active

NewGenerator Object

Used when creating a generator:

FieldTypeRequiredDescription
namestringYesUnique name for the generator
descriptionstringNoOptional 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:

StatusDescription
403Admin access required
500Internal 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"
}
FieldTypeRequiredDescription
namestringYesUnique name (max 255 characters)
descriptionstringNoOptional 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:

StatusDescription
400Invalid generator data (e.g., duplicate name)
403Admin access required
500Internal 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:

ParameterTypeDescription
idUUIDGenerator 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:

StatusDescription
403Unauthorized access (not admin and not the generator)
404Generator not found
500Internal 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:

ParameterTypeDescription
idUUIDGenerator 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:

StatusDescription
403Unauthorized access
404Generator not found
500Internal 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:

ParameterTypeDescription
idUUIDGenerator 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:

StatusDescription
403Unauthorized access
404Generator not found
500Internal 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:

ParameterTypeDescription
idUUIDGenerator 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:

StatusDescription
403Unauthorized access
404Generator not found
500Internal 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

OperationAdmin PAKGenerator PAK (own)Generator PAK (other)
List generatorsYesNoNo
Create generatorYesNoNo
Get generatorYesYesNo
Update generatorYesYesNo
Delete generatorYesYesNo
Rotate PAKYesYesNo

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

ColumnTypeConstraints
idUUIDPRIMARY KEY, DEFAULT uuid_generate_v4()
nameVARCHAR(255)NOT NULL, UNIQUE
descriptionTEXT
pak_hashVARCHAR(255)
created_atTIMESTAMPNOT NULL, DEFAULT NOW()
updated_atTIMESTAMPNOT NULL, DEFAULT NOW()
deleted_atTIMESTAMPNULL (soft delete)
last_active_atTIMESTAMPNULL
is_activeBOOLEANNOT 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.

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

StatusDescription
pendingRequest created, waiting for agent to claim
claimedAgent has claimed the request and is collecting data
completedAgent submitted diagnostic results
failedAgent encountered an error during collection
expiredRequest exceeded its retention period without completion

Data Model

DiagnosticRequest

FieldTypeDescription
idUUIDUnique identifier
agent_idUUIDTarget agent to collect from
deployment_object_idUUIDDeployment object to diagnose
statusStringCurrent status (see above)
requested_byString?Who requested the diagnostic (free-text)
created_atDateTimeRequest creation time
claimed_atDateTime?When agent claimed the request
completed_atDateTime?When result was submitted
expires_atDateTimeWhen the request expires

DiagnosticResult

FieldTypeDescription
idUUIDUnique identifier
request_idUUIDAssociated diagnostic request
pod_statusesString (JSON)Pod status information
eventsString (JSON)Kubernetes events
log_tailsString? (JSON)Container log tails (last 100 lines per container)
collected_atDateTimeWhen data was collected on the agent
created_atDateTimeRecord 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
}
FieldTypeRequiredDefaultConstraints
agent_idUUIDYesMust be a valid agent
requested_byStringNonullFree-text identifier
retention_minutesIntegerNo601-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 OKDiagnosticRequest[]


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:

FieldTypeDescription
nameStringPod name
namespaceStringPod namespace
phaseStringPod phase (Running, Pending, Failed, etc.)
conditionsArrayPod conditions (Ready, Initialized, etc.)
containersArrayContainer statuses

Container status fields:

FieldTypeDescription
nameStringContainer name
readyBooleanWhether the container is ready
restart_countIntegerNumber of restarts
stateStringCurrent state (running, waiting, terminated)
state_reasonString?Reason for waiting/terminated state
state_messageString?Message for waiting/terminated state

Events

FieldTypeDescription
event_typeString?Normal or Warning
reasonString?Short reason string
messageString?Human-readable message
involved_object_kindString?Kind of involved object (Pod, ReplicaSet, etc.)
involved_object_nameString?Name of involved object
countInteger?Number of occurrences
first_timestampString?First occurrence
last_timestampString?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:

SettingDefaultDescription
broker.diagnostic_cleanup_interval_seconds900 (15 min)How often cleanup runs
broker.diagnostic_max_age_hours1Max age for completed/expired/failed diagnostics

The cleanup task:

  1. Expires pending requests past their expires_at time
  2. Deletes completed, expired, and failed requests older than diagnostic_max_age_hours
  3. Deletes associated diagnostic results

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

AspectIsolation Level
TablesFull — each schema has its own tables
SequencesFull — sequence counters are per-schema
MigrationsFull — each schema migrates independently
Admin PAKFull — each tenant has its own admin
AgentsFull — agents belong to one tenant
GeneratorsFull — 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

StartupWhat Happens
FirstAll migrations + admin role creation + admin PAK generation
SubsequentPending 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

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_at field
  • Unique constraints are scoped to only active (non-deleted) records

Entities Supporting Soft Deletion

EntityCascade BehaviorAPI Endpoint
AgentsSoft deletes agent; cascade soft-deletes agent eventsDELETE /api/v1/agents/{id}
StacksCascades to deployment objects; creates deletion markerDELETE /api/v1/stacks/{id}
GeneratorsCascades to stacks and deployment objectsDELETE /api/v1/generators/{id}
TemplatesSoft deletes template onlyDELETE /api/v1/templates/{id}
Agent EventsCascade soft-deleted when parent agent is soft-deletedNot directly exposed
Deployment ObjectsSoft deletes object onlyGenerally not exposed directly

Cascade Behavior

Stack Deletion

When a stack is soft-deleted, the system triggers several cascading operations through database triggers:

  1. All deployment objects belonging to the stack are soft-deleted
  2. A special deletion marker deployment object is created with is_deletion_marker: true
  3. 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:

  1. All stacks owned by the generator are soft-deleted
  2. All deployment objects in those stacks are soft-deleted
  3. 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:

EntityUnique Fields
Agents(name, cluster_name)
Stacksname
Generatorsname
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:

  • NULL for 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:

TriggerTableEventFunction
trigger_handle_stack_soft_deletestacksAFTER UPDATE of deleted_athandle_stack_soft_delete()
cascade_soft_delete_generatorsgeneratorsAFTER UPDATEcascade_soft_delete_generators()
trigger_stack_hard_deletestacksBEFORE DELETEhandle_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.

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:

FieldTypeDescription
idUUIDUnique identifier for the log entry
timestamptimestampWhen the event occurred
actor_typestringType of actor: admin, agent, generator, system
actor_idUUIDID of the actor (null for system or unauthenticated)
actionstringThe action performed (e.g., agent.created)
resource_typestringType of resource affected (e.g., agent, stack)
resource_idUUIDID of the affected resource (null if not applicable)
detailsJSONStructured details about the action
ip_addressstringClient IP address
user_agentstringClient user agent string
created_attimestampWhen the record was created

Actor Types

The actor_type field identifies what kind of entity performed the action:

TypeDescription
adminAdministrator using an admin PAK
agentAn agent performing its own operations
generatorA generator creating or managing resources
systemSystem-initiated operations (background tasks, scheduled jobs)

Actions

Actions follow a resource.verb naming convention. The following actions are currently logged:

Authentication

ActionDescription
pak.createdA new PAK was generated
pak.rotatedAn existing PAK was rotated
pak.deletedA PAK was invalidated
auth.failedAuthentication attempt failed
auth.successAuthentication succeeded

Resource Management

ActionDescription
agent.createdNew agent registered
agent.updatedAgent details modified
agent.deletedAgent removed
stack.createdNew stack created
stack.updatedStack details modified
stack.deletedStack removed
generator.createdNew generator created
generator.updatedGenerator details modified
generator.deletedGenerator removed
template.createdNew template created
template.updatedTemplate modified
template.deletedTemplate removed

Webhooks

ActionDescription
webhook.createdNew webhook subscription created
webhook.updatedWebhook subscription modified
webhook.deletedWebhook subscription removed
webhook.delivery_failedWebhook delivery failed after retries

Work Orders

ActionDescription
workorder.createdNew work order created
workorder.claimedWork order claimed by an agent
workorder.completedWork order completed successfully
workorder.failedWork order failed
workorder.retryWork order returned for retry

Administration

ActionDescription
config.reloadedConfiguration hot-reload performed

Resource Types

The resource_type field identifies what kind of resource was affected:

TypeDescription
agentAn agent resource
stackA stack resource
generatorA generator resource
templateA stack template
webhook_subscriptionA webhook subscription
work_orderA work order
pakA PAK (authentication key)
configSystem configuration
systemSystem-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

ParameterTypeDescription
actor_typestringFilter by actor type
actor_idUUIDFilter by actor ID
actionstringFilter by action (exact match or prefix with *)
resource_typestringFilter by resource type
resource_idUUIDFilter by resource ID
fromtimestampStart time (inclusive, ISO 8601)
totimestampEnd time (exclusive, ISO 8601)
limitintegerMaximum results (default 100, max 1000)
offsetintegerResults 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_at column 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:

IndexColumnsPurpose
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"

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:

  1. /healthz - Liveness probe: Simple check that the process is alive
  2. /readyz - Readiness probe: Validates that the service is ready to accept traffic
  3. /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 OK with 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 OK if 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 OK with 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 OK if ready, 503 Service Unavailable if 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 OK if healthy, 503 Service Unavailable if 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 connectivity
  • kubernetes.error: Optional error message if connection failed
  • broker.connected: Boolean indicating broker connectivity
  • broker.last_heartbeat: ISO 8601 timestamp of last successful heartbeat
  • uptime_seconds: Service uptime in seconds
  • version: Application version from Cargo.toml
  • timestamp: 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 connection
    • periodSeconds: 10 - Check every 10 seconds
    • failureThreshold: 3 - Restart after 30 seconds of failures
  • Readiness:
    • initialDelaySeconds: 10 - Quick readiness check after startup
    • periodSeconds: 5 - Check frequently to minimize downtime
    • failureThreshold: 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 connection
    • periodSeconds: 10 - Check every 10 seconds
    • failureThreshold: 3 - Restart after 30 seconds of failures
  • Readiness:
    • initialDelaySeconds: 10 - Quick readiness check after startup
    • periodSeconds: 5 - Check frequently for K8s API issues
    • failureThreshold: 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:

  • initialDelaySeconds too low for startup time
  • timeoutSeconds too low for slow responses
  • failureThreshold too 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
  • initialDelaySeconds on 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

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

  1. Use all three endpoint types appropriately:

    • /healthz for liveness probes only
    • /readyz for readiness probes only
    • /health for monitoring and debugging (not for probes)
  2. Set appropriate timeouts:

    • Account for slow network conditions
    • Consider cold start performance
    • Test probe timing in staging before production
  3. Monitor probe failures:

    • Alert on excessive readiness probe failures
    • Track liveness probe failure rate
    • Use Prometheus to monitor probe success rate
  4. Tune for your environment:

    • Adjust initialDelaySeconds based on actual startup time
    • Increase periodSeconds if probes cause excessive load
    • Increase failureThreshold in high-latency environments
  5. Test probe configurations:

    • Simulate failures in staging
    • Verify restarts work as expected
    • Ensure startup timing is adequate
  6. Use /health endpoint for operational visibility:

    • Monitor detailed status in dashboards
    • Parse JSON response for alerting
    • Track component dependencies (K8s API, broker)
  7. Avoid common mistakes:

    • Don’t use /health for Kubernetes probes (too detailed, may cause false positives)
    • Don’t set timeouts shorter than actual endpoint latency
    • Don’t set initialDelaySeconds too low for startup dependencies

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

ComponentRepositoryPurpose
Brokerghcr.io/colliery-io/brokkr-brokerCentral management service
Agentghcr.io/colliery-io/brokkr-agentKubernetes cluster agent
UIghcr.io/colliery-io/brokkr-uiAdministrative web interface

Supported Architectures

All images support the following platforms:

  • linux/amd64 - x86_64 architecture
  • linux/arm64 - ARM64/aarch64 architecture

Tag Format Specifications

Semantic Version Tags

Created when a git tag matching v*.*.* is pushed.

Tag FormatExampleDescriptionMutable
{major}.{minor}.{patch}1.2.3Full semantic versionNo
{major}.{minor}1.2Latest patch in minor versionYes
{major}1Latest minor in major versionYes
latestlatestMost recent stable releaseYes

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 FormatExampleDescriptionMutable
{branch}-sha-{short-sha}develop-sha-abc1234Branch-prefixed 7-character commit SHANo

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 FormatExampleDescriptionMutable
{branch-name}mainBranch name (sanitized)Yes
developdevelopDevelopment branchYes

Example: Push to develop branch creates:

ghcr.io/colliery-io/brokkr-broker:develop

Pull Request Tags

Optionally created for pull request builds.

Tag FormatExampleDescriptionMutable
pr-{number}pr-123Pull request numberYes

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, or all
  • --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

  1. Planner stage: Generates cargo-chef recipe
  2. Cacher stage: Builds dependencies (cached layer)
  3. Builder stage: Compiles Rust binaries
  4. Final stage: Minimal Debian slim with runtime dependencies

UI Image

  1. 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
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:

ComponentAMD64ARM64
Broker~60 MB~58 MB
Agent~65 MB~62 MB
UI~40 MB~38 MB

Note: Sizes vary by release and dependency versions

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 path
    • method - 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 path
    • method - 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 UUID
    • agent_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

  1. Use ServiceMonitors when possible for automatic discovery
  2. Set appropriate scrape intervals (30s is recommended)
  3. Configure alerting rules for critical metrics
  4. Monitor resource usage in high-traffic environments
  5. Use recording rules for frequently queried expensive PromQL expressions
  6. Enable grafana dashboards for operational visibility
  7. Test alerts in staging before production deployment

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

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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

ExceptionDescription
ErrorReturns an error if:
ErrorFailed to fetch deployments from the cluster
ErrorFailed 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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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

NameTypeDescription
commandCommandsCommand 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

NameTypeDescription
idUuidThe deployment object ID
statusStringOverall health status: healthy, degraded, failing, unknown
summaryHealthSummaryStructured health summary
checked_atDateTime < 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

NameTypeDescription
pods_readyusizeNumber of pods in ready state
pods_totalusizeTotal number of pods
conditionsVec < String >List of detected problematic conditions
resourcesVec < ResourceHealth >Per-resource health details

brokkr-agent::deployment_health::ResourceHealth

pub

Derives: Debug, Clone, Serialize, Deserialize

Health status of an individual resource

Fields

NameTypeDescription
kindStringKind of the resource (e.g., “Pod”, “Deployment”)
nameStringName of the resource
namespaceStringNamespace of the resource
readyboolWhether the resource is ready
messageOption < String >Human-readable status message

brokkr-agent::deployment_health::HealthChecker

pub

Checks deployment health for Kubernetes resources

Fields

NameTypeDescription
k8s_clientClient

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

NameTypeDescription
deployment_objectsVec < 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

NameTypeDescription
idUuidThe deployment object ID
statusStringHealth status: healthy, degraded, failing, or unknown
summaryOption < HealthSummary >Structured health summary
checked_atDateTime < 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

NameTypeDescription
idUuidUnique identifier for the diagnostic request.
agent_idUuidThe agent that should handle this request.
deployment_object_idUuidThe deployment object to gather diagnostics for.
statusStringStatus: pending, claimed, completed, failed, expired.
requested_byOption < String >Who requested the diagnostics.
created_atDateTime < Utc >When the request was created.
claimed_atOption < DateTime < Utc > >When the agent claimed the request.
completed_atOption < DateTime < Utc > >When the request was completed.
expires_atDateTime < Utc >When the request expires.

brokkr-agent::diagnostics::SubmitDiagnosticResult

pub

Derives: Debug, Clone, Serialize, Deserialize

Result to submit back to the broker.

Fields

NameTypeDescription
pod_statusesStringJSON-encoded pod statuses.
eventsStringJSON-encoded Kubernetes events.
log_tailsOption < String >JSON-encoded log tails (optional).
collected_atDateTime < Utc >When the diagnostics were collected.

brokkr-agent::diagnostics::PodStatus

pub

Derives: Debug, Clone, Serialize, Deserialize

Pod status information for diagnostics.

Fields

NameTypeDescription
nameStringPod name.
namespaceStringPod namespace.
phaseStringPod phase (Pending, Running, Succeeded, Failed, Unknown).
conditionsVec < PodCondition >Pod conditions.
containersVec < ContainerStatus >Container statuses.

brokkr-agent::diagnostics::PodCondition

pub

Derives: Debug, Clone, Serialize, Deserialize

Pod condition information.

Fields

NameTypeDescription
condition_typeStringCondition type.
statusStringCondition status (True, False, Unknown).
reasonOption < String >Reason for the condition.
messageOption < String >Human-readable message.

brokkr-agent::diagnostics::ContainerStatus

pub

Derives: Debug, Clone, Serialize, Deserialize

Container status information.

Fields

NameTypeDescription
nameStringContainer name.
readyboolWhether the container is ready.
restart_counti32Number of restarts.
stateStringCurrent state of the container.
state_reasonOption < String >Reason for current state.
state_messageOption < String >Message for current state.

brokkr-agent::diagnostics::EventInfo

pub

Derives: Debug, Clone, Serialize, Deserialize

Kubernetes event information.

Fields

NameTypeDescription
event_typeOption < String >Event type (Normal, Warning).
reasonOption < String >Event reason.
messageOption < String >Event message.
involved_objectStringObject involved.
first_timestampOption < DateTime < Utc > >First timestamp.
last_timestampOption < DateTime < Utc > >Last timestamp.
countOption < i32 >Event count.

brokkr-agent::diagnostics::DiagnosticsHandler

pub

Diagnostics handler for collecting Kubernetes diagnostics.

Fields

NameTypeDescription
clientClientKubernetes 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:

NameTypeDescription
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

NameTypeDescription
k8s_clientClient
broker_statusArc < RwLock < BrokerStatus > >
start_timeSystemTime

brokkr-agent::health::BrokerStatus

pub

Derives: Clone

Broker connection status

Fields

NameTypeDescription
connectedbool
last_heartbeatOption < String >

brokkr-agent::health::HealthStatus

private

Derives: Serialize

Health status response structure

Fields

NameTypeDescription
statusString
kubernetesKubernetesStatus
brokerBrokerStatusResponse
uptime_secondsu64
versionString
timestampString

brokkr-agent::health::KubernetesStatus

private

Derives: Serialize

Kubernetes health status

Fields

NameTypeDescription
connectedbool
errorOption < String >

brokkr-agent::health::BrokerStatusResponse

private

Derives: Serialize

Broker health status for response

Fields

NameTypeDescription
connectedbool
last_heartbeatOption < 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

NameTypeDescription
max_elapsed_timeDuration
initial_intervalDuration
max_intervalDuration
multiplierf64

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:

NameTypeDescription
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:

NameTypeDescription
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 instance configured for the specified resource type

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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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, &params, &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:

NameTypeDescription
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:

  1. Applies priority resources (Namespaces, CRDs) first to ensure dependencies exist
  2. Validates remaining objects against the API server
  3. Applies all resources with server-side apply
  4. Prunes any objects that are no longer part of the desired state but belong to the same stack
  5. Rolls back namespace creation if any part of the reconciliation fails

Parameters:

NameTypeDescription
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(&regular_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, &params, &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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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

NameTypeDescription
idUuidDelivery ID.
subscription_idUuidSubscription ID.
event_typeStringEvent type being delivered.
payloadStringJSON-encoded event payload.
urlStringDecrypted webhook URL.
auth_headerOption < String >Decrypted Authorization header (if configured).
timeout_secondsi32HTTP timeout in seconds.
max_retriesi32Maximum retries for this subscription.
attemptsi32Current attempt number.

brokkr-agent::webhooks::DeliveryResultRequest

pub

Derives: Debug, Clone, Serialize

Request body for reporting delivery result to broker.

Fields

NameTypeDescription
successboolWhether delivery succeeded.
status_codeOption < i32 >HTTP status code (if available).
errorOption < String >Error message (if failed).
duration_msOption < i64 >Delivery duration in milliseconds.

brokkr-agent::webhooks::DeliveryResult

pub

Derives: Debug

Result of a webhook delivery attempt.

Fields

NameTypeDescription
successboolWhether delivery succeeded.
status_codeOption < i32 >HTTP status code (if available).
errorOption < String >Error message (if failed).
duration_msi64Delivery 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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

  1. Fetches pending webhooks from the broker
  2. Delivers each webhook via HTTP
  3. Reports results back to the broker

Parameters:

NameTypeDescription
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:

  1. Fetches pending work orders from the broker
  2. Claims the first available work order
  3. Executes the work based on work type
  4. Reports completion to the broker

Parameters:

NameTypeDescription
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

NameTypeDescription
agent_idUuid

brokkr-agent::work_orders::broker::CompleteRequest

private

Derives: Debug, Serialize

Request body for completing a work order.

Fields

NameTypeDescription
successbool
messageOption < String >
retryableboolWhether 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

NameTypeDescription
statusString

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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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

NameTypeDescription
conditionsVec < Condition >
outputOption < BuildRunOutput >
failure_detailsOption < FailureDetails >

brokkr-agent::work_orders::build::Condition

pub(crate)

Derives: Debug, Deserialize

Fields

NameTypeDescription
condition_typeString
statusString
reasonOption < String >
messageOption < String >

brokkr-agent::work_orders::build::BuildRunOutput

pub(crate)

Derives: Debug, Deserialize

Fields

NameTypeDescription
digestOption < String >
sizeOption < i64 >

brokkr-agent::work_orders::build::FailureDetails

pub(crate)

Derives: Debug, Deserialize

Fields

NameTypeDescription
reasonOption < String >
messageOption < String >

brokkr-agent::work_orders::build::ParsedBuildInfo

pub(crate)

Derives: Debug, Clone, PartialEq

Result of parsing build YAML content

Fields

NameTypeDescription
build_nameString
build_namespaceString
build_docsVec < 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:

  1. Parses the YAML content to find Build resources
  2. Applies Build resources to the cluster
  3. Creates a BuildRun
  4. Watches the BuildRun until completion
  5. Returns the image digest on success or error details on failure

Parameters:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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

NameTypeDescription
reloaded_atDateTime < Utc >Timestamp when the configuration was reloaded.
changesVec < ConfigChangeInfo >List of configuration changes detected during reload.
successboolIndicates whether the reload was successful.
messageOption < String >Optional message providing additional context.

brokkr-broker::api::v1::admin::ConfigChangeInfo

pub

Derives: Debug, Serialize, ToSchema

Information about a single configuration change.

Fields

NameTypeDescription
keyStringThe configuration key that changed.
old_valueStringThe previous value (as a string representation).
new_valueStringThe new value (as a string representation).

brokkr-broker::api::v1::admin::AuditLogQueryParams

pub

Derives: Debug, Deserialize, IntoParams

Query parameters for listing audit logs.

Fields

NameTypeDescription
actor_typeOption < String >Filter by actor type (admin, agent, generator, system).
actor_idOption < Uuid >Filter by actor ID.
actionOption < String >Filter by action (exact match or prefix with *).
resource_typeOption < String >Filter by resource type.
resource_idOption < Uuid >Filter by resource ID.
fromOption < DateTime < Utc > >Filter by start time (inclusive, ISO 8601).
toOption < DateTime < Utc > >Filter by end time (exclusive, ISO 8601).
limitOption < i64 >Maximum number of results (default 100, max 1000).
offsetOption < 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

NameTypeDescription
logsVec < AuditLog >The audit log entries.
totali64Total count of matching entries (for pagination).
countusizeNumber of entries returned.
limiti64Limit used for this query.
offseti64Offset 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:

NameTypeDescription
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:

NameTypeDescription
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

NameTypeDescription
nameOption < String >
cluster_nameOption < String >

brokkr-broker::api::v1::agents::TargetStateParams

private

Derives: Deserialize, Default

Defines query parameters for the target state endpoint

Fields

NameTypeDescription
modeOption < 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

NameTypeDescription
agent_idUuidThe agent that should handle this request.
requested_byOption < String >Who is requesting the diagnostics (optional).
retention_minutesOption < 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

NameTypeDescription
requestDiagnosticRequestThe diagnostic request.
resultOption < 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

NameTypeDescription
pod_statusesStringJSON-encoded pod statuses.
eventsStringJSON-encoded Kubernetes events.
log_tailsOption < String >JSON-encoded log tails (optional).
collected_atDateTime < 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

NameTypeDescription
generatorGeneratorThe created generator
pakStringThe 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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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

NameTypeDescription
deployment_objectsVec < 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

NameTypeDescription
idUuidThe deployment object ID.
statusStringHealth status: healthy, degraded, failing, or unknown.
summaryOption < HealthSummary >Structured health summary.
checked_atDateTime < 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

NameTypeDescription
deployment_object_idUuidThe deployment object ID.
health_recordsVec < DeploymentHealth >List of health records from different agents.
overall_statusStringOverall 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

NameTypeDescription
stack_idUuidThe stack ID.
overall_statusStringOverall status for the stack.
deployment_objectsVec < 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

NameTypeDescription
idUuidThe deployment object ID.
statusStringOverall status for this deployment object.
healthy_agentsusizeNumber of agents reporting healthy.
degraded_agentsusizeNumber of agents reporting degraded.
failing_agentsusizeNumber 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

NameTypeDescription
adminboolIndicates if the authenticated entity is an admin.
agentOption < Uuid >The UUID of the authenticated agent, if applicable.
generatorOption < 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

NameTypeDescription
adminboolIndicates if the authenticated entity is an admin.
agentOption < String >The string representation of the agent’s UUID, if applicable.
generatorOption < 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:

NameTypeDescription
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:

NameTypeDescription
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

NameTypeDescription
template_idUuidID of the template to instantiate.
parametersserde_json :: ValueParameters 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

NameTypeDescription
nameStringName of the template.
descriptionOption < String >Optional description.
template_contentStringTera template content.
parameters_schemaStringJSON 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

NameTypeDescription
descriptionOption < String >Optional new description.
template_contentStringTera template content.
parameters_schemaStringJSON Schema for parameter validation.

brokkr-broker::api::v1::templates::AddAnnotationRequest

pub

Derives: Debug, Deserialize, Serialize, ToSchema

Request body for adding an annotation.

Fields

NameTypeDescription
keyStringAnnotation key.
valueStringAnnotation 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

NameTypeDescription
nameStringHuman-readable name for the subscription.
urlStringWebhook endpoint URL (will be encrypted at rest).
auth_headerOption < String >Optional Authorization header value (will be encrypted at rest).
event_typesVec < String >Event types to subscribe to (supports wildcards like “deployment.*”).
filtersOption < WebhookFilters >Optional filters to narrow which events are delivered.
max_retriesOption < i32 >Maximum number of delivery retries (default: 5).
timeout_secondsOption < i32 >HTTP timeout in seconds (default: 30).
validateboolWhether to validate the URL by sending a test request.
target_labelsOption < 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

NameTypeDescription
nameOption < String >New name.
urlOption < String >New URL (will be encrypted at rest).
auth_headerOption < Option < String > >New Authorization header (will be encrypted at rest).
Use null to remove, omit to keep unchanged.
event_typesOption < Vec < String > >New event types.
filtersOption < Option < WebhookFilters > >New filters.
enabledOption < bool >Enable/disable the subscription.
max_retriesOption < i32 >New max retries.
timeout_secondsOption < i32 >New timeout.
target_labelsOption < 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

NameTypeDescription
idUuidUnique identifier.
nameStringHuman-readable name.
has_urlboolWhether a URL is configured (actual value is encrypted).
has_auth_headerboolWhether an auth header is configured (actual value is encrypted).
event_typesVec < String >Subscribed event types.
filtersOption < WebhookFilters >Configured filters.
target_labelsOption < Vec < String > >Labels for delivery targeting (NULL = broker delivers).
enabledboolWhether the subscription is active.
max_retriesi32Maximum delivery retries.
timeout_secondsi32HTTP timeout in seconds.
created_atchrono :: DateTime < chrono :: Utc >When created.
updated_atchrono :: DateTime < chrono :: Utc >When last updated.
created_byOption < String >Who created this subscription.

brokkr-broker::api::v1::webhooks::ListDeliveriesQuery

pub

Derives: Debug, Clone, Deserialize, ToSchema

Query parameters for listing deliveries.

Fields

NameTypeDescription
statusOption < String >Filter by status (pending, acquired, success, failed, dead).
limitOption < i64 >Maximum number of results (default: 50).
offsetOption < 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

NameTypeDescription
idUuidDelivery ID.
subscription_idUuidSubscription ID.
event_typeStringEvent type being delivered.
payloadStringJSON-encoded event payload.
urlStringDecrypted webhook URL.
auth_headerOption < String >Decrypted Authorization header (if configured).
timeout_secondsi32HTTP timeout in seconds.
max_retriesi32Maximum retries for this subscription.
attemptsi32Current attempt number.

brokkr-broker::api::v1::webhooks::DeliveryResultRequest

pub

Derives: Debug, Clone, Deserialize, ToSchema

Request body for reporting delivery result.

Fields

NameTypeDescription
successboolWhether delivery succeeded.
status_codeOption < i32 >HTTP status code (if available).
errorOption < String >Error message (if failed).
duration_msOption < 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

NameTypeDescription
work_typeStringType of work (e.g., “build”, “test”, “backup”).
yaml_contentStringMulti-document YAML content.
max_retriesOption < i32 >Maximum number of retry attempts (default: 3).
backoff_secondsOption < i32 >Base backoff seconds for exponential retry (default: 60).
claim_timeout_secondsOption < i32 >Claim timeout in seconds (default: 3600).
targetingOption < WorkOrderTargeting >Optional targeting configuration. At least one targeting method must be specified.
target_agent_idsOption < 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

NameTypeDescription
agent_idsOption < Vec < Uuid > >Direct agent IDs that can claim this work order (hard targets).
labelsOption < Vec < String > >Labels that agents must have (OR logic - agent needs any one of these labels).
annotationsOption < 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

NameTypeDescription
agent_idUuidID 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

NameTypeDescription
successboolWhether the work completed successfully.
messageOption < String >Result message (image digest on success, error details on failure).
retryableboolWhether 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

NameTypeDescription
statusOption < String >Filter by status (PENDING, CLAIMED, RETRY_PENDING).
work_typeOption < 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

NameTypeDescription
work_typeOption < String >Filter by work type.

brokkr-broker::api::v1::work_orders::ListLogQuery

pub

Derives: Debug, Deserialize

Query parameters for listing work order log.

Fields

NameTypeDescription
work_typeOption < String >Filter by work type.
successOption < bool >Filter by success status.
agent_idOption < Uuid >Filter by agent ID.
limitOption < 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

NameTypeDescription
commandCommands

brokkr-broker::cli::CreateCommands

pub

Derives: Args

Fields

NameTypeDescription
commandCreateSubcommands

brokkr-broker::cli::RotateCommands

pub

Derives: Args

Fields

NameTypeDescription
commandRotateSubcommands

Enums

brokkr-broker::cli::Commands pub

Variants

  • Serve - Start the Brokkr Broker server
  • Create - Create new entities
  • Rotate - Rotate keys

brokkr-broker::cli::CreateSubcommands pub

Variants

  • Agent - Create a new agent
  • Generator - Create a new generator

brokkr-broker::cli::RotateSubcommands pub

Variants

  • Agent - Rotate an agent key
  • Generator - Rotate a generator key
  • Admin - 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

NameTypeDescription
counti64

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

NameTypeDescription
poolConnectionPoolA connection pool for PostgreSQL database connections with schema support.
auth_cacheOption < 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:

NameTypeDescription
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:

NameTypeDescription
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 pool
  • Query - Database query error
  • NotFound - Resource not found

brokkr-broker::dal::FilterType pub

Variants

  • And
  • Or

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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
labelsVec < String >
annotationsVec < (String , String) >
agent_targetsVec < Uuid >
filter_typeFilterType

brokkr-broker::dal::agents::AgentsDAL<’a>

pub

Data Access Layer for Agent operations.

Fields

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference to the main DAL instance.

brokkr-broker::dal::webhook_deliveries::DeliveryStats

pub

Derives: Debug, Default, Clone

Statistics about webhook deliveries.

Fields

NameTypeDescription
pendingi64Number of pending deliveries.
acquiredi64Number of acquired deliveries (in progress).
successi64Number of successful deliveries.
failedi64Number of failed deliveries (retrying).
deadi64Number 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
dal& 'a DALReference 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

NameTypeDescription
poolPool < ConnectionManager < PgConnection > >The actual connection pool.
schemaOption < 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:

ExceptionDescription
PanicThis method will panic if:
PanicUnable to get a connection from the pool
PanicThe schema name is invalid
PanicFailed 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:

NameTypeDescription
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:

NameTypeDescription
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:

ExceptionDescription
PanicThis function will panic if:
PanicThe base URL is invalid
PanicThe 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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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

NameTypeDescription
idUuid
created_atchrono :: DateTime < Utc >
updated_atchrono :: DateTime < Utc >
pak_hashString

brokkr-broker::utils::NewAdminKey

pub

Derives: Insertable

Represents a new admin key to be inserted into the database.

Fields

NameTypeDescription
pak_hashString

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

NameTypeDescription
channel_sizeusizeChannel buffer size.
batch_sizeusizeMaximum batch size for writes.
flush_interval_msu64Flush interval in milliseconds.

brokkr-broker::utils::audit::AuditLogger

pub

Derives: Clone

The async audit logger for buffering and batching audit entries.

Fields

NameTypeDescription
sendermpsc :: 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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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

NameTypeDescription
interval_secondsu64How often to run the cleanup (in seconds).
max_age_hoursi64Maximum age for completed/expired diagnostics before deletion (in hours).

brokkr-broker::utils::background_tasks::WorkOrderMaintenanceConfig

pub

Configuration for work order maintenance task.

Fields

NameTypeDescription
interval_secondsu64How often to run the maintenance (in seconds).

brokkr-broker::utils::background_tasks::WebhookDeliveryConfig

pub

Configuration for webhook delivery worker.

Fields

NameTypeDescription
interval_secondsu64How often to poll for pending deliveries (in seconds).
batch_sizei64Maximum number of deliveries to process per interval.

brokkr-broker::utils::background_tasks::WebhookCleanupConfig

pub

Configuration for webhook cleanup task.

Fields

NameTypeDescription
interval_secondsu64How often to run the cleanup (in seconds).
retention_daysi64Number of days to retain completed/dead deliveries.

brokkr-broker::utils::background_tasks::AuditLogCleanupConfig

pub

Configuration for audit log cleanup task.

Fields

NameTypeDescription
interval_secondsu64How often to run the cleanup (in seconds).
retention_daysi64Number 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:

  1. Expires pending diagnostic requests that have passed their expiry time
  2. Deletes old completed/expired/failed diagnostic requests and their results

Parameters:

NameTypeDescription
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:

  1. Moves RETRY_PENDING work orders back to PENDING when their backoff has elapsed
  2. Reclaims stale CLAIMED work orders that have timed out

Parameters:

NameTypeDescription
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:

  1. Releases expired acquired deliveries back to pending
  2. Moves failed deliveries with elapsed backoff back to pending
  3. Claims pending deliveries for broker (target_labels is NULL)
  4. Attempts to deliver each via HTTP POST
  5. Marks deliveries as success or failure (with retry scheduling)

Parameters:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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

NameTypeDescription
config_file_pathStringPath to the configuration file to watch.
debounce_durationDurationDebounce duration to prevent rapid successive reloads.
enabledboolWhether 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:

NameTypeDescription
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

NameTypeDescription
key[u8 ; 32]The raw 32-byte key.
cipherAes256GcmPre-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 failed
  • DecryptionFailed - Decryption operation failed (wrong key or corrupted data)
  • InvalidData - Invalid data format
  • UnsupportedVersion - 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:

NameTypeDescription
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:

ExceptionDescription
PanicPanics 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:

NameTypeDescription
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:

NameTypeDescription
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:

  1. Finds all enabled subscriptions matching the event type
  2. Creates a webhook_delivery record for each matching subscription
  3. Copies target_labels from subscription to delivery for routing

Parameters:

NameTypeDescription
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

NameTypeDescription
matchesboolWhether the template matches the stack.
missing_labelsVec < String >Labels required by the template that are missing from the stack.
missing_annotationsVec < (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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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

NameTypeDescription
messageString
detailsOption < String >

brokkr-broker::utils::templating::ParameterValidationError

pub

Derives: Debug, Clone

Validation error details for parameter validation.

Fields

NameTypeDescription
pathString
messageString

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:

NameTypeDescription
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:

NameTypeDescription
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, &params).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:

NameTypeDescription
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:

NameTypeDescription
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, &params).is_ok());

// Missing required field
let params = json!({});
assert!(validate_parameters(schema, &params).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:

NameTypeDescription
database_url-A string slice that holds the URL of the database to connect to.

Returns:

  • PgConnection - A connection to the PostgreSQL database.

Raises:

ExceptionDescription
PanicThis 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

NameTypeDescription
idUuidUnique identifier for the annotation.
agent_idUuidID of the agent this annotation belongs to.
keyStringKey of the annotation (max 64 characters, no whitespace).
valueStringValue 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

NameTypeDescription
agent_idUuidID of the agent this annotation belongs to.
keyStringKey of the annotation (max 64 characters, no whitespace).
valueStringValue 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the event.
created_atDateTime < Utc >Timestamp when the event was created.
updated_atDateTime < Utc >Timestamp when the event was last updated.
deleted_atOption < DateTime < Utc > >Timestamp for soft deletion, if applicable.
agent_idUuidID of the agent associated with this event.
deployment_object_idUuidID of the deployment object associated with this event.
event_typeStringType of the event.
statusStringStatus of the event (e.g., “SUCCESS”, “FAILURE”, “IN_PROGRESS”, “PENDING”).
messageOption < 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

NameTypeDescription
agent_idUuidID of the agent associated with this event.
deployment_object_idUuidID of the deployment object associated with this event.
event_typeStringType of the event.
statusStringStatus of the event (e.g., “SUCCESS”, “FAILURE”).
messageOption < 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the agent label.
agent_idUuidID of the agent this label is associated with.
labelStringThe 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

NameTypeDescription
agent_idUuidID of the agent this label is associated with.
labelStringThe 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the agent target.
agent_idUuidID of the agent associated with this target.
stack_idUuidID 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

NameTypeDescription
agent_idUuidID of the agent to associate with a stack.
stack_idUuidID 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the agent.
created_atDateTime < Utc >Timestamp when the agent was created.
updated_atDateTime < Utc >Timestamp when the agent was last updated.
deleted_atOption < DateTime < Utc > >Timestamp for soft deletion, if applicable.
nameStringName of the agent.
cluster_nameStringName of the cluster the agent belongs to.
last_heartbeatOption < DateTime < Utc > >Timestamp of the last heartbeat received from the agent.
statusStringCurrent status of the agent.
pak_hashStringHash 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

NameTypeDescription
nameStringName of the agent.
cluster_nameStringName 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the log entry.
timestampDateTime < Utc >When the event occurred.
actor_typeStringType of actor: admin, agent, generator, system.
actor_idOption < Uuid >ID of the actor (NULL for system or unauthenticated).
actionStringThe action performed (e.g., “agent.created”, “auth.failed”).
resource_typeStringType of resource affected.
resource_idOption < Uuid >ID of the affected resource (NULL if not applicable).
detailsOption < serde_json :: Value >Additional structured details.
ip_addressOption < String >Client IP address.
user_agentOption < String >Client user agent string.
created_atDateTime < 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

NameTypeDescription
actor_typeStringType of actor.
actor_idOption < Uuid >ID of the actor.
actionStringThe action performed.
resource_typeStringType of resource affected.
resource_idOption < Uuid >ID of the affected resource.
detailsOption < serde_json :: Value >Additional structured details.
ip_addressOption < String >Client IP address.
user_agentOption < 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:

NameTypeDescription
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

NameTypeDescription
actor_typeOption < String >Filter by actor type.
actor_idOption < Uuid >Filter by actor ID.
actionOption < String >Filter by action (exact match or prefix with *).
resource_typeOption < String >Filter by resource type.
resource_idOption < Uuid >Filter by resource ID.
fromOption < DateTime < Utc > >Filter by start time (inclusive).
toOption < 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

NameTypeDescription
idUuidUnique identifier for the health record.
agent_idUuidID of the agent that reported this health status.
deployment_object_idUuidID of the deployment object this health status applies to.
statusStringHealth status: healthy, degraded, failing, or unknown.
summaryOption < String >JSON-encoded summary with pod counts, conditions, and resource details.
checked_atDateTime < Utc >Timestamp when the agent last checked health.
created_atDateTime < Utc >Timestamp when the record was created.
updated_atDateTime < 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

NameTypeDescription
agent_idUuidID of the agent reporting this health status.
deployment_object_idUuidID of the deployment object this health status applies to.
statusStringHealth status: healthy, degraded, failing, or unknown.
summaryOption < String >JSON-encoded summary with pod counts, conditions, and resource details.
checked_atDateTime < 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:

NameTypeDescription
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

NameTypeDescription
statusStringUpdated health status.
summaryOption < String >Updated JSON-encoded summary.
checked_atDateTime < Utc >Updated check timestamp.

brokkr-models::models::deployment_health::HealthSummary

pub

Derives: Debug, Clone, Serialize, Deserialize, ToSchema

Structured health summary for serialization/deserialization.

Fields

NameTypeDescription
pods_readyi32Number of pods in ready state.
pods_totali32Total number of pods.
conditionsVec < String >List of detected problematic conditions (e.g., ImagePullBackOff).
resourcesOption < 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

NameTypeDescription
kindStringResource kind (e.g., Deployment, StatefulSet).
nameStringResource name.
namespaceStringResource namespace.
readyboolWhether the resource is ready.
messageOption < 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

NameTypeDescription
idUuidUnique identifier for the deployment object.
created_atDateTime < Utc >Timestamp when the deployment object was created.
updated_atDateTime < Utc >Timestamp when the deployment object was last updated.
deleted_atOption < DateTime < Utc > >Timestamp for soft deletion, if applicable.
sequence_idi64Auto-incrementing sequence number for ordering.
stack_idUuidID of the stack this deployment object belongs to.
yaml_contentStringYAML content of the deployment.
yaml_checksumStringSHA-256 checksum of the YAML content.
submitted_atDateTime < Utc >Timestamp when the deployment was submitted.
is_deletion_markerboolIndicates 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

NameTypeDescription
stack_idUuidID of the stack this deployment object belongs to.
yaml_contentStringYAML content of the deployment.
yaml_checksumStringSHA-256 checksum of the YAML content.
is_deletion_markerboolIndicates 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the diagnostic request.
agent_idUuidThe agent that should handle this request.
deployment_object_idUuidThe deployment object to gather diagnostics for.
statusStringStatus: pending, claimed, completed, failed, expired.
requested_byOption < String >Who requested the diagnostics (e.g., operator username).
created_atDateTime < Utc >When the request was created.
claimed_atOption < DateTime < Utc > >When the agent claimed the request.
completed_atOption < DateTime < Utc > >When the request was completed.
expires_atDateTime < 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

NameTypeDescription
agent_idUuidThe agent that should handle this request.
deployment_object_idUuidThe deployment object to gather diagnostics for.
statusStringStatus (defaults to “pending”).
requested_byOption < String >Who requested the diagnostics.
expires_atDateTime < 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:

NameTypeDescription
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

NameTypeDescription
statusOption < String >New status.
claimed_atOption < DateTime < Utc > >When claimed.
completed_atOption < 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

NameTypeDescription
idUuidUnique identifier for the diagnostic result.
request_idUuidThe diagnostic request this result belongs to.
pod_statusesStringJSON-encoded pod statuses.
eventsStringJSON-encoded Kubernetes events.
log_tailsOption < String >JSON-encoded log tails (optional).
collected_atDateTime < Utc >When the diagnostics were collected by the agent.
created_atDateTime < 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

NameTypeDescription
request_idUuidThe diagnostic request this result belongs to.
pod_statusesStringJSON-encoded pod statuses.
eventsStringJSON-encoded Kubernetes events.
log_tailsOption < String >JSON-encoded log tails (optional).
collected_atDateTime < 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the generator.
created_atDateTime < Utc >Timestamp of when the generator was created.
updated_atDateTime < Utc >Timestamp of when the generator was last updated.
deleted_atOption < DateTime < Utc > >Timestamp of when the generator was deleted, if applicable.
nameStringName of the generator.
descriptionOption < String >Optional description of the generator.
pak_hashOption < String >Hash of the Pre-Authentication Key (PAK) for the generator.
last_active_atOption < DateTime < Utc > >Timestamp of when the generator was last active.
is_activeboolIndicates 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

NameTypeDescription
nameStringName of the new generator.
descriptionOption < 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for this provenance record.
deployment_object_idUuidID of the deployment object that was created.
template_idUuidID of the template used to create the deployment object.
template_versioni32Version of the template at the time of rendering (snapshot).
template_parametersStringJSON string of parameters used for rendering.
created_atDateTime < 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

NameTypeDescription
deployment_object_idUuidID of the deployment object that was created.
template_idUuidID of the template used to create the deployment object.
template_versioni32Version of the template at the time of rendering (snapshot).
template_parametersStringJSON 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the annotation.
stack_idUuidID of the stack this annotation belongs to.
keyStringKey of the annotation (max 64 characters, no whitespace).
valueStringValue 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

NameTypeDescription
stack_idUuidID of the stack this annotation belongs to.
keyStringKey of the annotation (max 64 characters, no whitespace).
valueStringValue 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the stack label.
stack_idUuidID of the stack this label is associated with.
labelStringThe 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

NameTypeDescription
stack_idUuidID of the stack this label is associated with.
labelStringThe 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the template.
created_atDateTime < Utc >Timestamp when the template was created.
updated_atDateTime < Utc >Timestamp when the template was last updated.
deleted_atOption < DateTime < Utc > >Timestamp for soft deletion, if applicable.
generator_idOption < Uuid >Generator ID - NULL for system templates (admin-only).
nameStringName of the template.
descriptionOption < String >Optional description of the template.
versioni32Version number (auto-incremented per name+generator_id).
template_contentStringTera template content.
parameters_schemaStringJSON Schema for parameter validation.
checksumStringSHA-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

NameTypeDescription
generator_idOption < Uuid >Generator ID - NULL for system templates (admin-only).
nameStringName of the template.
descriptionOption < String >Optional description of the template.
versioni32Version number.
template_contentStringTera template content.
parameters_schemaStringJSON Schema for parameter validation.
checksumStringSHA-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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the stack.
created_atDateTime < Utc >Timestamp when the stack was created.
updated_atDateTime < Utc >Timestamp when the stack was last updated.
deleted_atOption < DateTime < Utc > >Timestamp for soft deletion, if applicable.
nameStringName of the stack.
descriptionOption < String >Optional description of the stack.
generator_idUuidOptional 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

NameTypeDescription
nameStringName of the stack.
descriptionOption < String >Optional description of the stack.
generator_idUuidOptional 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the template annotation.
template_idUuidID of the template this annotation is associated with.
keyStringThe annotation key (max 64 characters, no whitespace).
valueStringThe annotation value (max 64 characters, no whitespace).
created_atDateTime < 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

NameTypeDescription
template_idUuidID of the template this annotation is associated with.
keyStringThe annotation key (max 64 characters, no whitespace).
valueStringThe 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the template label.
template_idUuidID of the template this label is associated with.
labelStringThe label text (max 64 characters, no whitespace).
created_atDateTime < 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

NameTypeDescription
template_idUuidID of the template this label is associated with.
labelStringThe 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the template target.
template_idUuidID of the template associated with this target.
stack_idUuidID of the stack associated with this target.
created_atDateTime < 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

NameTypeDescription
template_idUuidID of the template to associate with a stack.
stack_idUuidID 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for this event (idempotency key).
event_typeStringEvent type (e.g., “deployment.applied”).
timestampDateTime < Utc >When the event occurred.
dataserde_json :: ValueEvent-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

NameTypeDescription
agent_idOption < Uuid >Filter by specific agent ID.
stack_idOption < Uuid >Filter by specific stack ID.
labelsOption < 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

NameTypeDescription
idUuidUnique identifier for the subscription.
nameStringHuman-readable name for the subscription.
url_encryptedVec < u8 >Encrypted webhook URL.
auth_header_encryptedOption < Vec < u8 > >Encrypted Authorization header value.
event_typesVec < Option < String > >Event types to subscribe to (supports wildcards like “deployment.*”).
filtersOption < String >JSON-encoded filters.
target_labelsOption < Vec < Option < String > > >Labels for delivery targeting (NULL = broker delivers).
enabledboolWhether the subscription is active.
max_retriesi32Maximum delivery retry attempts.
timeout_secondsi32HTTP request timeout in seconds.
created_atDateTime < Utc >When the subscription was created.
updated_atDateTime < Utc >When the subscription was last updated.
created_byOption < 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

NameTypeDescription
nameStringHuman-readable name.
url_encryptedVec < u8 >Encrypted webhook URL.
auth_header_encryptedOption < Vec < u8 > >Encrypted Authorization header value.
event_typesVec < Option < String > >Event types to subscribe to.
filtersOption < String >JSON-encoded filters.
target_labelsOption < Vec < Option < String > > >Labels for delivery targeting (NULL = broker delivers).
enabledboolWhether the subscription is active (defaults to true).
max_retriesi32Maximum retry attempts (defaults to 5).
timeout_secondsi32HTTP timeout in seconds (defaults to 30).
created_byOption < 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:

NameTypeDescription
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

NameTypeDescription
nameOption < String >New name.
url_encryptedOption < Vec < u8 > >New encrypted URL.
auth_header_encryptedOption < Option < Vec < u8 > > >New encrypted auth header.
event_typesOption < Vec < Option < String > > >New event types.
filtersOption < Option < String > >New filters.
target_labelsOption < Option < Vec < Option < String > > > >New target labels for delivery.
enabledOption < bool >Enable/disable.
max_retriesOption < i32 >New max retries.
timeout_secondsOption < 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

NameTypeDescription
idUuidUnique identifier for the delivery.
subscription_idUuidThe subscription this delivery belongs to.
event_typeStringThe event type being delivered.
event_idUuidThe event ID (idempotency key).
payloadStringJSON-encoded event payload.
target_labelsOption < Vec < Option < String > > >Labels for delivery targeting (copied from subscription).
statusStringDelivery status: pending, acquired, success, failed, dead.
acquired_byOption < Uuid >Agent ID that acquired this delivery (NULL = broker).
acquired_untilOption < DateTime < Utc > >TTL for the acquisition - release if exceeded.
attemptsi32Number of delivery attempts.
last_attempt_atOption < DateTime < Utc > >When the last delivery attempt was made.
next_retry_atOption < DateTime < Utc > >When to retry after failure.
last_errorOption < String >Error message from last failed attempt.
created_atDateTime < Utc >When the delivery was created.
completed_atOption < 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

NameTypeDescription
subscription_idUuidThe subscription to deliver to.
event_typeStringThe event type.
event_idUuidThe event ID.
payloadStringJSON-encoded payload.
target_labelsOption < Vec < Option < String > > >Labels for delivery targeting (copied from subscription).
statusStringInitial 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:

NameTypeDescription
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

NameTypeDescription
statusOption < String >New status.
acquired_byOption < Option < Uuid > >Agent that acquired this delivery.
acquired_untilOption < Option < DateTime < Utc > > >TTL for the acquisition.
attemptsOption < i32 >Increment attempts.
last_attempt_atOption < DateTime < Utc > >When last attempted.
next_retry_atOption < Option < DateTime < Utc > > >When to retry.
last_errorOption < Option < String > >Error message.
completed_atOption < 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

NameTypeDescription
idUuidUnique identifier for the annotation.
work_order_idUuidID of the work order this annotation belongs to.
keyStringKey of the annotation (max 64 characters, no whitespace).
valueStringValue of the annotation (max 64 characters, no whitespace).
created_atchrono :: 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

NameTypeDescription
work_order_idUuidID of the work order this annotation belongs to.
keyStringKey of the annotation (max 64 characters, no whitespace).
valueStringValue 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the work order label.
work_order_idUuidID of the work order this label is associated with.
labelStringThe label text (max 64 characters, no whitespace).
created_atchrono :: 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

NameTypeDescription
work_order_idUuidID of the work order this label is associated with.
labelStringThe 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:

NameTypeDescription
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

NameTypeDescription
idUuidUnique identifier for the work order.
created_atDateTime < Utc >Timestamp when the work order was created.
updated_atDateTime < Utc >Timestamp when the work order was last updated.
work_typeStringType of work (e.g., “build”, “test”, “backup”).
yaml_contentStringMulti-document YAML content (e.g., Build + WorkOrder definitions).
statusStringQueue status: PENDING, CLAIMED, or RETRY_PENDING.
claimed_byOption < Uuid >ID of the agent that claimed this work order (if any).
claimed_atOption < DateTime < Utc > >Timestamp when the work order was claimed.
claim_timeout_secondsi32Seconds before a claimed work order is considered stale.
max_retriesi32Maximum number of retry attempts.
retry_counti32Current retry count.
backoff_secondsi32Base backoff seconds for exponential retry calculation.
next_retry_afterOption < DateTime < Utc > >Timestamp when RETRY_PENDING work order becomes PENDING again.
last_errorOption < String >Most recent error message from failed execution attempt.
last_error_atOption < 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

NameTypeDescription
work_typeStringType of work (e.g., “build”, “test”, “backup”).
yaml_contentStringMulti-document YAML content.
max_retriesi32Maximum number of retry attempts.
backoff_secondsi32Base backoff seconds for exponential retry calculation.
claim_timeout_secondsi32Seconds 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:

NameTypeDescription
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

NameTypeDescription
idUuidOriginal work order ID.
work_typeStringType of work.
created_atDateTime < Utc >Timestamp when the work order was created.
claimed_atOption < DateTime < Utc > >Timestamp when the work order was claimed.
completed_atDateTime < Utc >Timestamp when the work order completed.
claimed_byOption < Uuid >ID of the agent that executed this work order.
successboolWhether the work completed successfully.
retries_attemptedi32Number of retry attempts before completion.
result_messageOption < String >Result message (image digest on success, error details on failure).
yaml_contentStringOriginal 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

NameTypeDescription
idUuidOriginal work order ID.
work_typeStringType of work.
created_atDateTime < Utc >Timestamp when the work order was created.
claimed_atOption < DateTime < Utc > >Timestamp when the work order was claimed.
claimed_byOption < Uuid >ID of the agent that executed this work order.
successboolWhether the work completed successfully.
retries_attemptedi32Number of retry attempts before completion.
result_messageOption < String >Result message.
yaml_contentStringOriginal 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

NameTypeDescription
idUuidUnique identifier for the target entry.
work_order_idUuidID of the work order.
agent_idUuidID of the eligible agent.
created_atDateTime < 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

NameTypeDescription
work_order_idUuidID of the work order.
agent_idUuidID 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

NameTypeDescription
databaseDatabaseDatabase configuration
logLogLogging configuration
pakPAKPAK configuration
agentAgentAgent configuration
brokerBrokerBroker configuration
corsCorsCORS configuration
telemetryTelemetryTelemetry configuration

Methods

new pub
#![allow(unused)]
fn main() {
fn new (file : Option < String >) -> Result < Self , ConfigError >
}

Creates a new Settings instance

Parameters:

NameTypeDescription
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

NameTypeDescription
allowed_originsVec < 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_methodsVec < String >Allowed HTTP methods
Can be set as comma-separated string via env var: “GET,POST,PUT”
allowed_headersVec < String >Allowed HTTP headers
Can be set as comma-separated string via env var: “Authorization,Content-Type”
max_age_secondsu64Max age for preflight cache in seconds

brokkr-utils::config::Broker

pub

Derives: Debug, Deserialize, Clone

Fields

NameTypeDescription
pak_hashOption < String >PAK Hash
diagnostic_cleanup_interval_secondsOption < u64 >Interval for diagnostic cleanup task in seconds (default: 900 = 15 minutes)
diagnostic_max_age_hoursOption < i64 >Maximum age for completed/expired diagnostics before deletion in hours (default: 1)
webhook_encryption_keyOption < 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_secondsOption < u64 >Webhook delivery worker interval in seconds (default: 5)
webhook_delivery_batch_sizeOption < i64 >Webhook delivery batch size (default: 50)
webhook_cleanup_retention_daysOption < i64 >Webhook delivery cleanup retention in days (default: 7)
audit_log_retention_daysOption < i64 >Audit log retention in days (default: 90)
auth_cache_ttl_secondsOption < 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

NameTypeDescription
broker_urlStringBroker URL
polling_intervalu64Polling interval in seconds
kubeconfig_pathOption < String >Kubeconfig path
max_retriesu32Max number of retries
pakStringPAK
agent_nameStringAgent name
cluster_nameStringCluster name
max_event_message_retriesusizeMax number of retries for event messages
event_message_retry_delayu64Delay between event message retries in seconds
health_portOption < u16 >Health check HTTP server port
deployment_health_enabledOption < bool >Whether deployment health checking is enabled
deployment_health_intervalOption < u64 >Interval for deployment health checks in seconds

brokkr-utils::config::Database

pub

Derives: Debug, Deserialize, Clone

Represents the database configuration

Fields

NameTypeDescription
urlStringDatabase connection URL
schemaOption < String >Optional schema name for multi-tenant isolation

brokkr-utils::config::Log

pub

Derives: Debug, Deserialize, Clone

Represents the logging configuration

Fields

NameTypeDescription
levelStringLog level (e.g., “info”, “debug”, “warn”, “error”)
formatStringLog 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

NameTypeDescription
enabledboolWhether telemetry is enabled (base default)
otlp_endpointStringOTLP endpoint for trace export (gRPC)
service_nameStringService name for traces
sampling_ratef64Sampling rate (0.0 to 1.0)
brokerTelemetryOverrideBroker-specific overrides
agentTelemetryOverrideAgent-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

NameTypeDescription
enabledOption < bool >Override enabled flag
otlp_endpointOption < String >Override OTLP endpoint
service_nameOption < String >Override service name
sampling_rateOption < f64 >Override sampling rate

brokkr-utils::config::ResolvedTelemetry

pub

Derives: Debug, Clone

Resolved telemetry configuration after merging base with overrides

Fields

NameTypeDescription
enabledbool
otlp_endpointString
service_nameString
sampling_ratef64

brokkr-utils::config::PAK

pub

Derives: Debug, Deserialize, Clone

Represents the PAK configuration

Fields

NameTypeDescription
prefixOption < String >PAK prefix
digestOption < String >Digest algorithm for PAK
rngOption < String >RNG type for PAK
short_token_lengthOption < usize >Short token length for PAK
short_token_length_strOption < String >Short token length as a string
short_token_prefixOption < String >Prefix for short tokens
long_token_lengthOption < usize >Long token length for PAK
long_token_length_strOption < 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

NameTypeDescription
log_levelStringLog level (e.g., “info”, “debug”, “warn”, “error”)
diagnostic_cleanup_interval_secondsu64Interval for diagnostic cleanup task in seconds
diagnostic_max_age_hoursi64Maximum age for completed/expired diagnostics before deletion in hours
webhook_delivery_interval_secondsu64Webhook delivery worker interval in seconds
webhook_delivery_batch_sizei64Webhook delivery batch size
webhook_cleanup_retention_daysi64Webhook delivery cleanup retention in days
cors_allowed_originsVec < String >Allowed origins for CORS requests
cors_max_age_secondsu64Max 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

NameTypeDescription
keyStringThe configuration key that changed
old_valueStringThe old value (as string for display)
new_valueStringThe 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

NameTypeDescription
static_configSettingsStatic configuration that requires restart to change
dynamicArc < RwLock < DynamicConfig > >Dynamic configuration that can be hot-reloaded
config_fileOption < 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:

NameTypeDescription
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:

NameTypeDescription
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 This allows environment variables to be set as “value1,value2,value3” while also supporting proper arrays in config files

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:

NameTypeDescription
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:

NameTypeDescription
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:

NameTypeDescription
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 exporter
  • TracerError - Failed to initialize tracer
  • SubscriberError - 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:

NameTypeDescription
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();
}
}