Simple guides introducing Fabrica with hands-on examples.
Understanding Fabrica’s resource structure, UID generation, labels, annotations, and lifecycle.
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.Resourceembedding, the current pattern uses explicit fields withfabrica.Metadatafor better clarity and flexibility.
The version of the API that defines this resource.
APIVersion: "v1" // Stable version
APIVersion: "v2beta1" // Beta version
APIVersion: "v3alpha1" // Alpha version
Usage:
The type of resource.
Kind: "Device" // IoT device
Kind: "User" // User account
Kind: "Product" // Product catalog item
Convention: Use PascalCase singular nouns.
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
}
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:
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:
Fabrica uses structured UIDs instead of UUIDs for better readability and debugging.
<prefix>-<random-hex>
Examples:
dev-1a2b3c4d (Device)
usr-9f8e7d6c (User)
prd-5a4b3c2d (Product)
func init() {
resource.RegisterResourcePrefix("Device", "dev")
resource.RegisterResourcePrefix("User", "usr")
resource.RegisterResourcePrefix("Product", "prd")
}
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"
// 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"
Human-readable identifier:
device.Metadata.Name = "temperature-sensor-01"
Best Practices:
System-generated unique identifier:
device.Metadata.UID = "dev-1a2b3c4d"
Best Practices:
/devices/dev-1a2b3c4d)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()
Queryable key-value pairs for selection and grouping:
device.SetLabel("environment", "production")
device.SetLabel("location", "datacenter-01")
device.SetLabel("team", "platform")
Use labels for:
?label=environment=production)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)
}
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")
| Use Case | Use Labels | Use Annotations |
|---|---|---|
| Filtering/querying | ✅ | ❌ |
| Grouping resources | ✅ | ❌ |
| Human documentation | ❌ | ✅ |
| External references | ❌ | ✅ |
| Configuration data | ❌ | ✅ |
// 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:
dev-1a2b3c4dCreatedAt, UpdatedAt// 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)
}
}
// 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:
UpdatedAt timestamp refreshed// Delete by UID
err := storage.Delete(ctx, "dev-1a2b3c4d")
What happens:
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
}
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")
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")
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:
{prefix}.condition.{type} (e.g., io.fabrica.condition.ready)Fabrica resources provide:
Next Steps:
Questions? GitHub Discussions