Fabrica Blog

Simple guides introducing Fabrica with hands-on examples.

View the Project on GitHub OpenCHAMI/fabrica

Resource Model Guide

Understanding Fabrica’s resource structure, UID generation, labels, annotations, and lifecycle.

Table of Contents

Overview

Fabrica follows the Kubernetes resource pattern with a flattened envelope structure. All resources explicitly define standard fields:

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

type Device struct {
    APIVersion string           `json:"apiVersion"`    // "v1" or "example.fabrica.dev/v1"
    Kind       string           `json:"kind"`          // "Device", "User", "Product"
    Metadata   fabrica.Metadata `json:"metadata"`      // Name, UID, labels, annotations, timestamps
    Spec       DeviceSpec       `json:"spec"`          // Desired state (you define)
    Status     DeviceStatus     `json:"status"`        // Observed state (you define)
}

// The Metadata struct provides:
type Metadata struct {
    Name        string            `json:"name"`
    UID         string            `json:"uid"`
    Labels      map[string]string `json:"labels,omitempty"`
    Annotations map[string]string `json:"annotations,omitempty"`
    CreatedAt   time.Time         `json:"createdAt"`
    UpdatedAt   time.Time         `json:"updatedAt"`
}

Migration Note: If you’re familiar with older Fabrica versions that used resource.Resource embedding, the current pattern uses explicit fields with fabrica.Metadata for better clarity and flexibility.

Resource Structure

APIVersion

The version of the API that defines this resource.

APIVersion: "v1"        // Stable version
APIVersion: "v2beta1"   // Beta version
APIVersion: "v3alpha1"  // Alpha version

Usage:

Kind

The type of resource.

Kind: "Device"      // IoT device
Kind: "User"        // User account
Kind: "Product"     // Product catalog item

Convention: Use PascalCase singular nouns.

Metadata

Standard metadata for all resources:

type Metadata struct {
    UID         string            // Unique identifier
    Name        string            // Human-readable name
    Labels      map[string]string // Queryable key-value pairs
    Annotations map[string]string // Non-queryable metadata
    CreatedAt   time.Time         // Creation timestamp
    UpdatedAt   time.Time         // Last update timestamp
}

Spec (Desired State)

Your custom specification - what you want:

type DeviceSpec struct {
    Name     string `json:"name"`
    Location string `json:"location"`
    Model    string `json:"model"`
    Config   map[string]string `json:"config,omitempty"`
}

Principles:

Status (Observed State)

Your custom status - what actually is:

type DeviceStatus struct {
    Active       bool   `json:"active"`
    LastSeen     string `json:"lastSeen,omitempty"`
    IPAddress    string `json:"ipAddress,omitempty"`
    Health       string `json:"health,omitempty"`
    Conditions   []Condition `json:"conditions,omitempty"`
}

Principles:

UID Generation

Fabrica uses structured UIDs instead of UUIDs for better readability and debugging.

Format

<prefix>-<random-hex>

Examples:
  dev-1a2b3c4d    (Device)
  usr-9f8e7d6c    (User)
  prd-5a4b3c2d    (Product)

Register a Prefix

func init() {
    resource.RegisterResourcePrefix("Device", "dev")
    resource.RegisterResourcePrefix("User", "usr")
    resource.RegisterResourcePrefix("Product", "prd")
}

Generate UIDs

Automatic (recommended):

// Framework generates UID automatically on create
device := &Device{
    Spec: DeviceSpec{Name: "Sensor 1"},
}
// UID will be generated: dev-1a2b3c4d

Manual:

uid, err := resource.GenerateUIDForResource("Device")
// Returns: "dev-1a2b3c4d"

// Custom length
uid, err := resource.GenerateUIDWithLength("dev", 12)
// Returns: "dev-1a2b3c4d5e6f"

UID Utilities

// Parse UID
prefix, random, err := resource.ParseUID("dev-1a2b3c4d")
// prefix = "dev", random = "1a2b3c4d"

// Validate UID
valid := resource.IsValidUID("dev-1a2b3c4d") // true

// Get resource type from UID
kind, err := resource.GetResourceTypeFromUID("dev-1a2b3c4d")
// Returns: "Device"

Metadata

Name

Human-readable identifier:

device.Metadata.Name = "temperature-sensor-01"

Best Practices:

UID

System-generated unique identifier:

device.Metadata.UID = "dev-1a2b3c4d"

Best Practices:

Timestamps

Automatically managed:

device.Metadata.CreatedAt = time.Now()  // On create
device.Metadata.UpdatedAt = time.Now()  // On every update

Utilities:

// How old is the resource?
age := device.Age()

// When was it last updated?
lastUpdate := device.LastUpdated()

// Mark as updated
device.Touch()

Labels and Annotations

Labels

Queryable key-value pairs for selection and grouping:

device.SetLabel("environment", "production")
device.SetLabel("location", "datacenter-01")
device.SetLabel("team", "platform")

Use labels for:

Example queries:

// Get label
env, exists := device.GetLabel("environment")

// Check if label exists
if device.HasLabel("critical") {
    // Handle critical device
}

// Match multiple labels
selector := map[string]string{
    "environment": "production",
    "location": "datacenter-01",
}
if device.MatchesLabels(selector) {
    // Device matches criteria
}

// Get all labels
labels := device.GetLabels()
for key, value := range labels {
    fmt.Printf("%s=%s\n", key, value)
}

Annotations

Non-queryable metadata for additional context:

device.SetAnnotation("description", "Primary temperature sensor for cold storage")
device.SetAnnotation("contact.email", "ops@example.com")
device.SetAnnotation("purchased.date", "2024-01-15")
device.SetAnnotation("warranty.expires", "2027-01-15")

Use annotations for:

Example usage:

// Get annotation
desc, exists := device.GetAnnotation("description")

// Get all annotations
annotations := device.GetAnnotations()

// Remove annotation
device.RemoveAnnotation("old-field")

Labels vs. Annotations

Use Case Use Labels Use Annotations
Filtering/querying
Grouping resources
Human documentation
External references
Configuration data

Resource Lifecycle

1. Creation

// Create resource
device := &Device{
    APIVersion: "infra.example.io/v1",
    Kind:       "Device",
    Metadata:   Metadata{},
    Spec: DeviceSpec{
        Name: "Temperature Sensor",
        Location: "Warehouse A",
    },
}

// Metadata is initialized
device.Metadata.Initialize("temp-sensor-01", uid)
device.SetLabel("location", "warehouse-a")

// Save to storage
storage.Save(ctx, device)

What happens:

  1. UID generated: dev-1a2b3c4d
  2. Timestamps set: CreatedAt, UpdatedAt
  3. Persisted to storage

2. Reading

// Get by UID
device, err := storage.Load(ctx, "dev-1a2b3c4d")

// List all
devices, err := storage.LoadAll(ctx)

// Filter by labels (application level)
productionDevices := []Device{}
for _, d := range devices {
    if d.Metadata.Labels["environment"] == "production" {
        productionDevices = append(productionDevices, d)
    }
}

3. Updating

// Load resource
device, err := storage.Load(ctx, "dev-1a2b3c4d")

// Update spec (desired state)
device.Spec.Location = "Warehouse B"

// Update label
device.SetLabel("location", "warehouse-b")

// Update status (observed state)
device.Status.IPAddress = "192.168.1.100"
device.Status.LastSeen = time.Now().Format(time.RFC3339)

// Mark as updated
device.Touch()

// Save
storage.Save(ctx, device)

What happens:

  1. UpdatedAt timestamp refreshed
  2. Changes persisted to storage

4. Deletion

// Delete by UID
err := storage.Delete(ctx, "dev-1a2b3c4d")

What happens:

  1. Resource removed from storage
  2. No soft-delete by default (implement if needed)

Best Practices

Resource Definition

DO:

 Use flattened envelope structure
 Use json tags
 Separate Spec and Status
 Include APIVersion and Kind fields
 Add validation methods

type Device struct {
    APIVersion string       `json:"apiVersion"`
    Kind       string       `json:"kind"`
    Metadata   Metadata     `json:"metadata"`
    Spec       DeviceSpec   `json:"spec"`
    Status     DeviceStatus `json:"status,omitempty"`
}

func (d *Device) Validate() error {
    if d.Spec.Name == "" {
        return fmt.Errorf("name required")
    }
    return nil
}

DON’T:

 Mix Spec and Status fields
 Forget json tags
 Use UUID for UID
 Store computed values in Spec

type BadDevice struct {
    Name string  // Should be in Spec
    Online bool  // Should be in Status
}

Labels

DO:

 Use for queryable attributes
 Use lowercase with hyphens
 Keep values simple
 Use consistent naming

device.SetLabel("environment", "production")
device.SetLabel("team", "platform")
device.SetLabel("location", "us-west-2")

DON’T:

 Store large values
 Use for documentation
 Include sensitive data
 Use inconsistent formats

device.SetLabel("Description", "A very long description...")
device.SetLabel("api_key", "secret123")

Annotations

DO:

 Use for documentation
 Store external references
 Include context
 Use structured keys

device.SetAnnotation("description", "Primary sensor")
device.SetAnnotation("contact.email", "ops@example.com")
device.SetAnnotation("external.id", "EXT-12345")
device.SetAnnotation("docs.url", "https://docs.example.com/devices/temp")

Status Fields

DO:

 Include conditions
 Add health indicators
 Record timestamps
 Show actual state

type DeviceStatus struct {
    Online       bool        `json:"online"`
    LastSeen     string      `json:"lastSeen,omitempty"`
    Health       string      `json:"health,omitempty"`
    Conditions   []Condition `json:"conditions,omitempty"`
}

Condition Pattern:

Conditions follow Kubernetes conventions and automatically publish events when changed:

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

type Condition struct {
    Type               string    `json:"type"`               // "Ready", "Healthy", "Available"
    Status             string    `json:"status"`             // "True", "False", "Unknown"
    LastTransitionTime time.Time `json:"lastTransitionTime"` // Auto-set when status changes
    Reason             string    `json:"reason,omitempty"`   // Machine-readable reason
    Message            string    `json:"message,omitempty"`  // Human-readable description
}

// Manual condition setting
device.Status.Conditions = []Condition{
    {
        Type:               "Ready",
        Status:             "True",
        Reason:             "DeviceOnline",
        Message:            "Device is ready to accept commands",
        LastTransitionTime: time.Now(),
    },
}

// Recommended: Use helper functions (publishes events automatically)
ctx := context.Background()
changed := resource.SetResourceCondition(ctx, device,
    "Ready", "True", "DeviceOnline", "Device is operational")

if changed {
    // CloudEvent published: "io.fabrica.condition.ready"
    log.Println("Ready condition changed")
}

// Common condition patterns
resource.SetResourceCondition(ctx, device, "Healthy", "True", "HealthCheckPassed", "All health checks passing")
resource.SetResourceCondition(ctx, device, "Available", "False", "Maintenance", "Device under maintenance")
resource.SetResourceCondition(ctx, device, "Connected", "Unknown", "NetworkTimeout", "Network connectivity uncertain")

Condition Event Integration:

Summary

Fabrica resources provide:

Next Steps:


Questions? GitHub Discussions