Skip to content

TinyNode CRD

TinyNode is the core Custom Resource that represents a component instance in a flow. Understanding TinyNode is essential for module development.

Full Specification

yaml
apiVersion: operator.tinysystems.io/v1alpha1
kind: TinyNode
metadata:
  name: my-router-abc123
  namespace: tinysystems
  labels:
    tiny.systems/flow-id: "flow-xyz"
    tiny.systems/project-id: "project-123"
    tiny.systems/workspace-id: "workspace-456"
spec:
  # Which module provides this component
  module: github.com/tiny-systems/common-module

  # Component name within the module
  component: router

  # Module version
  version: "1.0.0"

  # Outgoing connections
  edges:
    - id: "edge-1"
      port: "out_success"           # Source port on this node
      to: "next-node-name"          # Target node name
      toPort: "input"               # Target port name
      configuration:                 # Data mapping (expressions)
        context: "{{$.result}}"
        userId: "{{$.user.id}}"

status:
  # Observed generation for idempotency
  observedGeneration: 1

  # Module information
  moduleName: common-module

  # Component information
  component: router

  # Port definitions with schemas
  ports:
    - name: input
      label: "Input"
      position: 3              # Left
      source: false
      schema: |
        {"type": "object", "properties": {...}}
      configuration: |
        {"context": {}}
    - name: out_success
      label: "Success"
      position: 1              # Right
      source: true
      schema: |
        {"type": "object", "properties": {...}}

  # Shared metadata across pods
  metadata:
    http-server-port: "8080"
    custom-key: "custom-value"

  # Error state
  error: ""

  # Last update timestamp
  lastUpdateTime: "2024-01-15T10:30:00Z"

Spec Fields

module

The Go module path that provides this component:

yaml
spec:
  module: github.com/tiny-systems/common-module

Used to route messages and find the correct operator.

component

The component name within the module:

yaml
spec:
  component: router

Must match the Name returned by GetInfo().

version

Semantic version of the module:

yaml
spec:
  version: "1.0.0"

edges

Array of outgoing connections:

yaml
spec:
  edges:
    - id: "edge-abc123"           # Unique edge identifier
      port: "output"              # Source port on this node
      to: "target-node-name"      # Target TinyNode name
      toPort: "input"             # Target port name
      configuration:               # Expression-based data mapping
        result: "{{$.data}}"
        timestamp: "{{now()}}"

Edge Configuration

The configuration object uses expressions to transform data:

yaml
configuration:
  # Direct mapping
  userId: "{{$.user.id}}"

  # String interpolation
  message: "Hello {{$.user.name}}!"

  # Arithmetic
  total: "{{$.price * $.quantity}}"

  # Conditional (using comparison)
  isValid: "{{$.status == 'active'}}"

  # Literal values (no expressions)
  staticValue: "constant"

Status Fields

observedGeneration

Tracks which spec version was last processed:

go
if node.Status.ObservedGeneration >= node.Generation {
    // Already processed this version
    return ctrl.Result{}, nil
}

// Process and update
node.Status.ObservedGeneration = node.Generation

ports

Array of port definitions generated from the component:

yaml
status:
  ports:
    - name: input
      label: "Input"
      position: 3          # module.Left
      source: false        # Input port
      schema: |            # JSON Schema (stringified)
        {
          "type": "object",
          "properties": {
            "context": {"type": "object"}
          }
        }
      configuration: |     # Current configuration (stringified JSON)
        {"context": {}}

metadata

Key-value store for sharing state across pods:

yaml
status:
  metadata:
    http-server-port: "8080"
    custom-state: "value"

Used for CR-Based State Propagation.

error

Error message if the node is in an error state:

yaml
status:
  error: "component initialization failed: missing required setting"

Labels

Standard labels applied to TinyNodes:

LabelPurposeExample
tiny.systems/flow-idFlow identifierflow-abc123
tiny.systems/project-idProject identifierproject-xyz
tiny.systems/workspace-idWorkspace identifierworkspace-456
tiny.systems/moduleModule namecommon-module

Accessing TinyNode in Components

Via Reconcile Port

go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
    if port == v1alpha1.ReconcilePort {
        node, ok := msg.(v1alpha1.TinyNode)
        if !ok {
            return nil
        }

        // Access node information
        nodeName := node.Name
        edges := node.Spec.Edges
        metadata := node.Status.Metadata

        // Read shared state
        if port, ok := metadata["http-server-port"]; ok {
            c.configuredPort = port
        }
    }
    return nil
}

Updating Node Status

Only the leader pod should update status:

go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
    // Update metadata (leader only)
    if utils.IsLeader(ctx) {
        output(ctx, v1alpha1.ReconcilePort, func(node *v1alpha1.TinyNode) {
            if node.Status.Metadata == nil {
                node.Status.Metadata = make(map[string]string)
            }
            node.Status.Metadata["my-key"] = "my-value"
        })
    }
    return nil
}

TinyNode Lifecycle

1. CREATION
   ┌────────────────────────────────────────────────────────────┐
   │ Platform creates TinyNode CR when flow is deployed         │
   │ Labels link to flow, project, workspace                    │
   └────────────────────────────────────────────────────────────┘


2. RECONCILIATION
   ┌────────────────────────────────────────────────────────────┐
   │ TinyNodeReconciler detects new node                        │
   │ Creates Runner instance                                    │
   │ Sends to _settings port                                    │
   │ Updates status with port schemas                           │
   └────────────────────────────────────────────────────────────┘


3. EXECUTION
   ┌────────────────────────────────────────────────────────────┐
   │ TinySignals trigger node execution                         │
   │ Component.Handle() processes messages                      │
   │ Output routed via edges to next nodes                      │
   └────────────────────────────────────────────────────────────┘


4. PERIODIC RECONCILIATION
   ┌────────────────────────────────────────────────────────────┐
   │ Every 5 minutes: _reconcile port triggered                 │
   │ Component can refresh state                                │
   │ Leader updates status if needed                            │
   └────────────────────────────────────────────────────────────┘


5. DELETION
   ┌────────────────────────────────────────────────────────────┐
   │ Platform deletes TinyNode CR when flow is undeployed       │
   │ Runner instance destroyed                                  │
   │ Resources cleaned up                                       │
   └────────────────────────────────────────────────────────────┘

Example: Reading Edge Configuration

go
func (r *Runner) processEdges(ctx context.Context, port string, data any) error {
    for _, edge := range r.node.Spec.Edges {
        if edge.Port != port {
            continue
        }

        // Evaluate expressions in edge configuration
        transformedData := evaluator.Evaluate(edge.Configuration, data)

        // Send to next node
        r.scheduler.Handle(ctx, runner.Msg{
            To:   fmt.Sprintf("%s.%s", edge.To, edge.ToPort),
            From: fmt.Sprintf("%s.%s", r.node.Name, port),
            Data: transformedData,
        })
    }
    return nil
}

Next Steps

Build flow-based applications on Kubernetes