Fabrica Blog

Simple guides introducing Fabrica with hands-on examples.

View the Project on GitHub OpenCHAMI/fabrica

Status Subresource Guide

Status: Implemented ✅ Version: 0.3.1+ Pattern: Kubernetes-inspired

Overview

Fabrica-generated microservices support Kubernetes-style status subresources, providing clear separation between desired state (spec) and observed state (status). This prevents conflicts between user updates and system controllers.

Quick Start

# Generate a project (status subresource is automatic)
fabrica init my-api --module github.com/example/my-api
fabrica add resource Device
fabrica generate

# Build and run
go mod tidy
go run ./cmd/server/

Generated Endpoints:

PUT    /devices/{uid}        → Update spec (user-facing)
PUT    /devices/{uid}/status → Update status (system-facing)
PATCH  /devices/{uid}/status → Patch status

Architecture

The Problem

Without status subresources, a single endpoint updates both spec and status:

// ❌ Old pattern - conflicts possible
PUT /devices/{uid}
{
  "spec": {"location": "dc2"},     // User wants this
  "status": {"phase": "Ready"}     // Controller wants this
}

Issues:

The Solution

Status subresources provide separate endpoints:

// ✅ New pattern - no conflicts
PUT /devices/{uid}          // Users update spec only
{
  "spec": {"location": "dc2"}
}

PUT /devices/{uid}/status   // Controllers update status only
{
  "status": {"phase": "Ready", "health": "Healthy"}
}

Benefits:

API Usage

User Operations (Spec)

Update desired state:

# Update device location (spec)
curl -X PUT http://localhost:8080/devices/dev-123 \
  -H "Content-Type: application/json" \
  -d '{
    "spec": {
      "name": "sensor-01",
      "location": "datacenter-2",
      "model": "TempSensor-Pro"
    }
  }'

Patch specific fields:

# Patch just the location
curl -X PATCH http://localhost:8080/devices/dev-123 \
  -H "Content-Type: application/merge-patch+json" \
  -d '{
    "spec": {
      "location": "datacenter-3"
    }
  }'

Controller Operations (Status)

Update observed state:

# Update device status
curl -X PUT http://localhost:8080/devices/dev-123/status \
  -H "Content-Type: application/json" \
  -d '{
    "status": {
      "phase": "Ready",
      "health": "Healthy",
      "lastSeen": "2025-10-24T16:00:00Z",
      "conditions": [
        {
          "type": "Ready",
          "status": "True",
          "reason": "DeviceOnline",
          "message": "Device is operational"
        }
      ]
    }
  }'

Patch status fields:

# Patch just the health field
curl -X PATCH http://localhost:8080/devices/dev-123/status \
  -H "Content-Type: application/merge-patch+json" \
  -d '{
    "status": {
      "health": "Degraded"
    }
  }'

Client Library Usage

Updating Spec

import (
  "context"
  "github.com/example/my-api/pkg/client"
  "github.com/example/my-api/apis/example.fabrica.dev/v1/device"
)

func updateDeviceLocation(c *client.Client, uid string) error {
    ctx := context.Background()

    // Update spec only
    spec := device.DeviceSpec{
        Name:     "sensor-01",
        Location: "datacenter-2",
        Model:    "TempSensor-Pro",
    }

    req := client.UpdateDeviceRequest{
        Name:       "sensor-01",
        DeviceSpec: spec,
    }

    device, err := c.UpdateDevice(ctx, uid, req)
    if err != nil {
        return err
    }

    fmt.Printf("Updated device spec: %s\n", device.Spec.Location)
    return nil
}

Updating Status

func updateDeviceStatus(c *client.Client, uid string) error {
    ctx := context.Background()

    // Update status only
    status := device.DeviceStatus{
        Phase:   "Ready",
        Health:  "Healthy",
        Message: "Device is operational",
    }

    device, err := c.UpdateDeviceStatus(ctx, uid, status)
    if err != nil {
        return err
    }

    fmt.Printf("Updated device status: %s\n", device.Status.Phase)
    return nil
}

Patching Status

func patchDeviceHealth(c *client.Client, uid string) error {
    ctx := context.Background()

    // Patch just the health field
    patch := []byte(`{"health": "Degraded"}`)

    device, err := c.PatchDeviceStatus(ctx, uid, patch)
    if err != nil {
        return err
    }

    fmt.Printf("Patched device health: %s\n", device.Status.Health)
    return nil
}

Reconciler Pattern

Reconcilers automatically use status-only updates through the BaseReconciler.UpdateStatus() method.

Example Reconciler

package reconcilers

import (
  "context"
  "time"

  "github.com/openchami/fabrica/pkg/reconcile"
  "github.com/example/my-api/apis/example.fabrica.dev/v1/device"
)

type DeviceReconciler struct {
    reconcile.BaseReconciler
}

func (r *DeviceReconciler) reconcileDevice(ctx context.Context, dev *device.Device) error {
    // 1. Observe actual state (e.g., check if device is online)
    isOnline := r.checkDeviceOnline(dev)

    // 2. Update status based on observation
    if isOnline {
        dev.Status.Phase = "Ready"
        dev.Status.Health = "Healthy"
        dev.Status.LastSeen = time.Now().Format(time.RFC3339)
    } else {
        dev.Status.Phase = "Offline"
        dev.Status.Health = "Unhealthy"
    }

    // 3. UpdateStatus automatically:
    //    - Loads fresh copy from storage
    //    - Preserves any concurrent spec changes
    //    - Only updates status fields
    if err := r.UpdateStatus(ctx, dev); err != nil {
        return err
    }

    return nil
}

func (r *DeviceReconciler) checkDeviceOnline(dev *device.Device) bool {
    // Implement actual device health check
    return true
}

How UpdateStatus Works

The BaseReconciler.UpdateStatus() method ensures spec safety:

// From pkg/reconcile/reconciler.go
func (r *BaseReconciler) UpdateStatus(ctx context.Context, resource interface{}) error {
    // 1. Extract resource UID
    uid := resource.GetUID()
    kind := resource.GetKind()

    // 2. Load FRESH copy from storage (gets any concurrent spec updates)
    current, err := r.Client.Get(ctx, kind, uid)
    if err != nil {
        return err
    }

    // 3. Copy status from reconciled resource to fresh resource
    //    (preserves fresh spec, applies reconciled status)
    current.Status = resource.Status
    current.Touch()

    // 4. Save (spec is preserved, status is updated)
    return r.Client.Update(ctx, current)
}

Resource Definition

Define resources with separate Spec and Status using a flattened envelope:

package v1

import "github.com/openchami/fabrica/pkg/resource"

// Device represents a monitored device
type Device struct {
    APIVersion string       `json:"apiVersion"` // "infra.example.io/v1"
    Kind       string       `json:"kind"`       // "Device"
    Metadata   Metadata     `json:"metadata"`
    Spec       DeviceSpec   `json:"spec"`
    Status     DeviceStatus `json:"status,omitempty"`
}

// DeviceSpec defines desired state (user-managed)
type DeviceSpec struct {
    Name        string            `json:"name" validate:"required"`
    Location    string            `json:"location" validate:"required"`
    Model       string            `json:"model,omitempty"`
    Config      map[string]string `json:"config,omitempty"`
}

// DeviceStatus defines observed state (system-managed)
type DeviceStatus struct {
    // Phase represents the lifecycle phase
    Phase   string `json:"phase,omitempty"` // Pending, Ready, Offline, Error

    // Health represents operational health
    Health  string `json:"health,omitempty"` // Healthy, Degraded, Unhealthy

    // Message provides human-readable status
    Message string `json:"message,omitempty"`

    // LastSeen is when the device was last contacted
    LastSeen string `json:"lastSeen,omitempty"`

    // Conditions track specific state transitions
    Conditions []resource.Condition `json:"conditions,omitempty"`
}

Authorization

Separate Permissions

Status subresources support fine-grained authorization:

// In your custom authorization middleware
type DeviceAuthMiddleware struct {
    // Your authorization implementation
}

// Users can update spec
func (m *DeviceAuthMiddleware) CheckUpdate(r *http.Request, uid string) bool {
    user := getUserFromRequest(r)
    return hasPermission(user, "devices:update")
}

// Controllers can update status (separate permission)
func (m *DeviceAuthMiddleware) CheckStatusUpdate(r *http.Request, uid string) bool {
    user := getUserFromRequest(r)
    // Only controllers can update status
    return hasRole(user, "controller")
}

Authorization Policy Example

# Example RBAC rules for status subresources

# Users can update device specs
- role: user
  permissions:
    - devices:update

# Controllers can update device status
- role: controller
  permissions:
    - devices:update_status

# Admins can do both
- role: admin
  permissions:
    - devices:update
    - devices:update_status

# Role assignments
- user: alice
  role: user
- user: device-controller
  role: controller
- user: bob
  role: admin

Events

Status updates publish lifecycle events with distinguishing metadata:

Status Update Event

{
  "specversion": "1.0",
  "type": "io.fabrica.device.updated",
  "source": "fabrica-api/resources/Device/dev-123",
  "id": "evt-abc123",
  "time": "2025-10-24T16:00:00Z",
  "datacontenttype": "application/json",
  "data": {
    "action": "updated",
    "resourceKind": "Device",
    "resourceUID": "dev-123",
    "resourceName": "sensor-01",
    "resource": { ... },
    "metadata": {
      "updatedAt": "2025-10-24T16:00:00Z",
      "updateType": "status"
    }
  }
}

Spec Update Event

{
  "type": "io.fabrica.device.updated",
  "data": {
    "metadata": {
      "updatedAt": "2025-10-24T16:00:00Z"
      // No updateType field (or updateType: "spec")
    }
  }
}

Subscribing to Events

// Subscribe to all device updates
eventBus.Subscribe("io.fabrica.device.updated", func(ctx context.Context, event events.Event) error {
    var data events.ResourceChangeData
    event.DataAs(&data)

    // Check if this is a status update
    if updateType, ok := data.Metadata["updateType"].(string); ok && updateType == "status" {
        fmt.Printf("Status updated for device %s\n", data.ResourceUID)
    } else {
        fmt.Printf("Spec updated for device %s\n", data.ResourceUID)
    }

    return nil
})

Testing

Test Spec/Status Separation

func TestStatusSubresource(t *testing.T) {
    // Create device
    device := createDevice(t, DeviceSpec{
        Name:     "sensor-01",
        Location: "dc1",
    })

    // User updates spec
    device.Spec.Location = "dc2"
    updateDevice(t, device.UID, device.Spec)

    // Controller updates status
    status := DeviceStatus{
        Phase:  "Ready",
        Health: "Healthy",
    }
    updateDeviceStatus(t, device.UID, status)

    // Verify both are updated
    final := getDevice(t, device.UID)
    assert.Equal(t, "dc2", final.Spec.Location)
    assert.Equal(t, "Ready", final.Status.Phase)
}

Best Practices

✅ DO

  1. Use status for observed state only
    status.Phase = "Ready"        // ✅ Observed
    status.Health = "Healthy"     // ✅ Observed
    status.LastSeen = time.Now()  // ✅ Observed
    
  2. Update status in reconcilers
    dev.Status.Phase = "Ready"
    r.UpdateStatus(ctx, dev)  // ✅ Status-only update
    
  3. Use conditions for details
    resource.SetResourceCondition(ctx, dev,
        "Ready", "True", "DeviceOnline", "Device is operational")
    

❌ DON’T

  1. Don’t put computed values in spec
    spec.CalculatedField = "value"  // ❌ Use status
    
  2. Don’t update spec from reconcilers
    dev.Spec.Location = "new"  // ❌ Spec is user-defined
    
  3. Don’t mix spec and status in updates
    // ❌ Old pattern
    PUT /devices/{uid}
    {
      "spec": {...},
      "status": {...}
    }
    

Troubleshooting

Status Update Doesn’t Work

Problem: Status update returns 404 or 405

Solution: Verify route is registered:

# Check generated routes
grep "status" cmd/server/routes_generated.go

Should show:

r.Route("/status", func(r chi.Router) {
    r.Put("/", UpdateDeviceStatus)
    r.Patch("/", PatchDeviceStatus)
})

Spec Gets Overwritten

Problem: Reconciler overwrites spec changes

Solution: Use BaseReconciler.UpdateStatus() which loads fresh resource:

// ❌ Don't do this
r.Client.Update(ctx, dev)  // Overwrites everything

// ✅ Do this instead
r.UpdateStatus(ctx, dev)   // Only updates status

Authorization Fails

Problem: Controllers can’t update status

Solution: Implement StatusPolicy interface:

type DevicePolicy struct {
    // ... existing fields
}

// Add this method
func (p *DevicePolicy) CanUpdateStatus(ctx context.Context, auth interface{}, r *http.Request, uid string) PolicyDecision {
    // Check if user has controller role
    user := getUserFromAuth(auth)
    if isController(user) {
        return PolicyDecision{Allowed: true}
    }
    return PolicyDecision{Allowed: false, Reason: "not a controller"}
}

Migration from Old Pattern

If you have existing code that updates both spec and status:

Before

// Old pattern - updates everything
dev.Spec.Location = "dc2"
dev.Status.Phase = "Ready"
storage.SaveDevice(ctx, dev)

After

// New pattern - separate updates

// User updates (spec)
dev.Spec.Location = "dc2"
storage.SaveDevice(ctx, dev)

// Controller updates (status) - in reconciler
dev.Status.Phase = "Ready"
r.UpdateStatus(ctx, dev)  // Loads fresh, preserves spec

OpenAPI Documentation

Generated OpenAPI specs include status subresource operations:

paths:
  /devices/{uid}/status:
    put:
      summary: Update device status
      description: |
        Updates only the status portion of a device. This endpoint is intended
        for controllers, reconcilers, and monitoring systems.
      parameters:
        - name: uid
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceStatus'
      responses:
        '200':
          description: Status updated successfully
        '403':
          description: Forbidden - requires update_status permission

Further Reading


Implementation Status: ✅ Complete Version: Fabrica 0.3.1+ Questions? GitHub Discussions