Understanding Fabrica's resource structure, UID generation, labels, annotations, and lifecycle.
- Overview
- Resource Structure
- UID Generation
- Metadata
- Labels and Annotations
- Resource Lifecycle
- Best Practices
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 versionUsage:
- Enables multi-version support
- Allows gradual API evolution
- Supports backward compatibility
The type of resource.
Kind: "Device" // IoT device
Kind: "User" // User account
Kind: "Product" // Product catalog itemConvention: 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:
- Immutable desired state
- User-provided values
- What should exist
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:
- Mutable observed state
- System-provided values
- What actually exists
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-1a2b3c4dManual:
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:
- Use lowercase with hyphens
- Include context (location, purpose, number)
- Make it meaningful for humans
System-generated unique identifier:
device.Metadata.UID = "dev-1a2b3c4d"Best Practices:
- Never modify after creation
- Use for all internal references
- Use for API paths (
/devices/dev-1a2b3c4d)
Automatically managed:
device.Metadata.CreatedAt = time.Now() // On create
device.Metadata.UpdatedAt = time.Now() // On every updateUtilities:
// 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:
- Filtering (
?label=environment=production) - Grouping (all devices in a location)
- Selection (which resources to process)
- Organization (teams, projects, environments)
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:
- Descriptions and documentation
- Contact information
- External references
- Configuration hints
- Build/deployment metadata
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:
- UID generated:
dev-1a2b3c4d - Timestamps set:
CreatedAt,UpdatedAt - Persisted to storage
// 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:
UpdatedAttimestamp refreshed- Changes persisted to storage
// Delete by UID
err := storage.Delete(ctx, "dev-1a2b3c4d")What happens:
- Resource removed from storage
- No soft-delete by default (implement if needed)
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:
- Conditions automatically publish CloudEvents when status changes
- Event type:
{prefix}.condition.{type}(e.g.,io.fabrica.condition.ready) - Only publishes when status actually changes (not on every update)
- Includes full condition details and resource context
- See Events Guide for complete condition event documentation
Fabrica resources provide:
- 📋 Structured format - Kubernetes-inspired pattern
- 🔑 Unique IDs - Structured UIDs for readability
- 🏷️ Labels - Queryable organization
- 📝 Annotations - Rich metadata
- ⏰ Timestamps - Automatic lifecycle tracking
- 📊 Status - Observed state tracking
Next Steps:
- Build resources in Getting Started
- Generate code in Code Generation
- Store resources in Storage Guide
Questions? GitHub Discussions