System Ports
System ports are special ports that the SDK uses to deliver framework-level messages to components. They follow a naming convention with underscore prefix.
System Port Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ SYSTEM PORTS │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────┐
│ │
_settings ─────────▶ Component │
_control ──────────▶ │
_reconcile ────────▶ │──────▶ output
_client ───────────▶ │
│ │
└─────────────────┘
Port | Constant | Purpose
──────────────┼─────────────────────────────┼────────────────────────────
_settings | v1alpha1.SettingsPort | Configuration data
_control | v1alpha1.ControlPort | UI button clicks
_reconcile | v1alpha1.ReconcilePort | TinyNode CR updates
_client | v1alpha1.ClientPort | HTTP client requests_settings Port
Delivers configuration to components.
Constant
go
v1alpha1.SettingsPort = "_settings"Message Type
Your custom Settings struct.
Usage
go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
if port == v1alpha1.SettingsPort {
c.settings = msg.(Settings)
return nil
}
return nil
}Delivery Timing
- Delivered before any other messages
- Delivered on settings change from UI
- Delivered on pod startup
Port Definition
go
{
Name: v1alpha1.SettingsPort,
Label: "Settings",
Source: true,
Position: module.PositionTop,
Schema: schema.FromGo(Settings{}),
}_control Port
Delivers UI button click events.
Constant
go
v1alpha1.ControlPort = "_control"Message Type
Your custom Control struct with button fields.
Usage
go
type Control struct {
Start bool `json:"start,omitempty" title:"Start" format:"button"`
Stop bool `json:"stop,omitempty" title:"Stop" format:"button"`
}
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
if port == v1alpha1.ControlPort {
if !utils.IsLeader(ctx) {
return nil // Only leader handles control
}
control := msg.(Control)
if control.Start {
return c.start(ctx, output)
}
if control.Stop {
return c.stop()
}
}
return nil
}Delivery Timing
- Delivered when user clicks a button in the UI
- Creates a TinySignal CR in Kubernetes
- Leader pod processes the signal
Port Definition
go
{
Name: v1alpha1.ControlPort,
Label: "Control",
Source: true,
Position: module.PositionTop,
Schema: schema.FromGo(c.getControl()),
}_reconcile Port
Delivers TinyNode CR state and triggers status updates.
Constant
go
v1alpha1.ReconcilePort = "_reconcile"Message Types
Input: v1alpha1.TinyNode - The current TinyNode CR
Output: func(*v1alpha1.TinyNode) - Callback to modify node status
Usage - Reading State
go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
if port == v1alpha1.ReconcilePort {
node, ok := msg.(v1alpha1.TinyNode)
if !ok {
return nil
}
c.nodeName = node.Name
// Read shared state from metadata
if val, ok := node.Status.Metadata["my-key"]; ok {
c.cachedValue = val
}
}
return nil
}Usage - Writing State
go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
if port == v1alpha1.ReconcilePort && utils.IsLeader(ctx) {
// Write to TinyNode status
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
}Usage - Triggering UI Update
go
// Send nil to trigger port schema refresh
output(context.Background(), v1alpha1.ReconcilePort, nil)Delivery Timing
- On component startup
- When TinyNode CR changes
- Periodically (every 5 minutes by default)
- When you send to _reconcile to update status
Port Definition
Usually implicit - no need to define in Ports().
_client Port
For HTTP-serving components to receive requests.
Constant
go
v1alpha1.ClientPort = "_client"Message Type
Incoming HTTP request data (component-specific).
Usage
go
type ClientRequest struct {
Method string `json:"method"`
Path string `json:"path"`
Headers map[string]string `json:"headers"`
Body any `json:"body"`
}
type ClientResponse struct {
StatusCode int `json:"statusCode"`
ContentType string `json:"contentType"`
Body string `json:"body"`
}
func (h *HTTPServer) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
if port == v1alpha1.ClientPort {
req := msg.(ClientRequest)
// Forward to flow
result := output(ctx, "request", req)
// Get response from flow
if resp, ok := result.(ClientResponse); ok {
return resp
}
}
return nil
}Delivery Timing
- When HTTP request arrives at component's exposed port
- Only for components that expose HTTP endpoints
Port Delivery Order
┌─────────────────────────────────────────────────────────────────────────────┐
│ PORT DELIVERY ORDER │
└─────────────────────────────────────────────────────────────────────────────┘
Time ────────────────────────────────────────────────────────────────────────▶
1. _settings
│ Configuration delivered first
│
▼
2. _reconcile
│ Initial TinyNode state
│
▼
3. Regular ports (input, etc.)
│ Normal message flow begins
│
▼
4. _control (as needed)
│ User clicks buttons
│
▼
5. _reconcile (periodic)
Status updates, requeueComplete Example
Component using all system ports:
go
package mycomponent
import (
"context"
"sync"
"github.com/tiny-systems/module/api/v1alpha1"
"github.com/tiny-systems/module/module"
"github.com/tiny-systems/module/pkg/schema"
"github.com/tiny-systems/module/pkg/utils"
)
type Settings struct {
Interval int `json:"interval" title:"Interval (ms)" default:"1000"`
}
type Control struct {
Start bool `json:"start,omitempty" title:"Start" format:"button"`
Stop bool `json:"stop,omitempty" title:"Stop" format:"button"`
}
type Input struct {
Data string `json:"data"`
}
type Output struct {
Result string `json:"result"`
}
type MyComponent struct {
settings Settings
nodeName string
isRunning bool
cancelFunc context.CancelFunc
mu sync.Mutex
}
func (c *MyComponent) GetInfo() module.Info {
return module.Info{
Name: "my-component",
Description: "Demonstrates all system ports",
Icon: "IconBox",
}
}
func (c *MyComponent) Ports() []module.Port {
return []module.Port{
// System ports
{
Name: v1alpha1.SettingsPort,
Label: "Settings",
Source: true,
Position: module.PositionTop,
Schema: schema.FromGo(Settings{}),
},
{
Name: v1alpha1.ControlPort,
Label: "Control",
Source: true,
Position: module.PositionTop,
Schema: schema.FromGo(c.getControl()),
},
// Regular ports
{
Name: "input",
Label: "Input",
Source: true,
Position: module.PositionLeft,
Schema: schema.FromGo(Input{}),
},
{
Name: "output",
Label: "Output",
Source: false,
Position: module.PositionRight,
Schema: schema.FromGo(Output{}),
},
}
}
func (c *MyComponent) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
switch port {
// System ports
case v1alpha1.SettingsPort:
c.settings = msg.(Settings)
return nil
case v1alpha1.ControlPort:
return c.handleControl(ctx, output, msg)
case v1alpha1.ReconcilePort:
return c.handleReconcile(ctx, output, msg)
// Regular ports
case "input":
return c.handleInput(ctx, output, msg)
}
return nil
}
func (c *MyComponent) handleControl(ctx context.Context, output module.Handler, msg any) error {
if !utils.IsLeader(ctx) {
return nil
}
control := msg.(Control)
c.mu.Lock()
defer c.mu.Unlock()
if control.Start && !c.isRunning {
ctx, c.cancelFunc = context.WithCancel(ctx)
c.isRunning = true
output(context.Background(), v1alpha1.ReconcilePort, nil)
go c.run(ctx, output)
}
if control.Stop && c.isRunning {
if c.cancelFunc != nil {
c.cancelFunc()
}
c.isRunning = false
output(context.Background(), v1alpha1.ReconcilePort, nil)
}
return nil
}
func (c *MyComponent) handleReconcile(ctx context.Context, output module.Handler, msg any) error {
node, ok := msg.(v1alpha1.TinyNode)
if !ok {
return nil
}
c.nodeName = node.Name
// Read shared state
if val, ok := node.Status.Metadata["last-run"]; ok {
// Use cached value
_ = val
}
// Leader: update shared state
if utils.IsLeader(ctx) {
output(ctx, v1alpha1.ReconcilePort, func(n *v1alpha1.TinyNode) {
if n.Status.Metadata == nil {
n.Status.Metadata = make(map[string]string)
}
n.Status.Metadata["component-status"] = "ready"
})
}
return nil
}
func (c *MyComponent) handleInput(ctx context.Context, output module.Handler, msg any) error {
input := msg.(Input)
result := Output{Result: "Processed: " + input.Data}
return output(ctx, "output", result)
}
func (c *MyComponent) run(ctx context.Context, output module.Handler) {
// Background work
<-ctx.Done()
}
func (c *MyComponent) getControl() Control {
c.mu.Lock()
defer c.mu.Unlock()
return Control{
Start: !c.isRunning,
Stop: c.isRunning,
}
}
func (c *MyComponent) Instance() module.Component {
return &MyComponent{}
}System Port Constants
go
package v1alpha1
const (
SettingsPort = "_settings"
ControlPort = "_control"
ReconcilePort = "_reconcile"
ClientPort = "_client"
)Best Practices
1. Check Port Before Processing
go
switch port {
case v1alpha1.SettingsPort:
// Handle settings
case v1alpha1.ControlPort:
// Handle control
default:
// Handle regular ports
}2. Leader Check for Control
go
if port == v1alpha1.ControlPort {
if !utils.IsLeader(ctx) {
return nil
}
// Process control
}3. Thread Safety
go
c.mu.Lock()
c.settings = msg.(Settings)
c.mu.Unlock()4. Nil Checks for Reconcile
go
if port == v1alpha1.ReconcilePort {
node, ok := msg.(v1alpha1.TinyNode)
if !ok {
return nil // Handle callback case
}
// Process node
}Next Steps
- Component Patterns - Common patterns
- Settings and Configuration - Settings details
- Control Ports - Control details