TinyModule CRD
TinyModule is the Custom Resource used for module discovery. It allows modules to find each other for cross-module communication.
Full Specification
yaml
apiVersion: operator.tinysystems.io/v1alpha1
kind: TinyModule
metadata:
name: common-module-v1
namespace: tinysystems
spec:
# Container image (optional, for reference)
image: ghcr.io/tiny-systems/common-module:v1.0.0
status:
# gRPC address for cross-module communication
address: "common-module-v1:50051"
# Module version
version: "1.0.0"
# Available components
components:
- name: router
info:
description: "Routes messages based on conditions"
tags: ["routing", "conditional"]
- name: split
info:
description: "Iterates over array items"
tags: ["array", "iteration"]
- name: ticker
info:
description: "Emits at regular intervals"
tags: ["timer", "periodic"]How Discovery Works
┌─────────────────────────────────────────────────────────────────────────────┐
│ MODULE DISCOVERY FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
1. MODULE STARTUP
┌────────────────────────────────────────────────────────────────────────┐
│ All pods create TinyModule CR with their module name │
│ │
│ kubectl apply -f - <<EOF │
│ apiVersion: operator.tinysystems.io/v1alpha1 │
│ kind: TinyModule │
│ metadata: │
│ name: my-module-v1 │
│ spec: │
│ image: myregistry/my-module:v1.0.0 │
│ EOF │
└────────────────────────────────────────────────────────────────────────┘
│
▼
2. LEADER UPDATES STATUS
┌────────────────────────────────────────────────────────────────────────┐
│ Only the leader pod updates TinyModule.Status: │
│ │
│ status: │
│ address: "my-module-v1:50051" # gRPC endpoint │
│ version: "1.0.0" │
│ components: │
│ - name: mycomponent │
│ info: {...} │
└────────────────────────────────────────────────────────────────────────┘
│
▼
3. OTHER MODULES WATCH
┌────────────────────────────────────────────────────────────────────────┐
│ TinyModuleReconciler watches ALL TinyModule CRs │
│ │
│ For remote modules (not own module): │
│ - Read status.address │
│ - Register in ClientPool │
│ - Now can send gRPC messages to that module │
└────────────────────────────────────────────────────────────────────────┘
│
▼
4. CROSS-MODULE COMMUNICATION
┌────────────────────────────────────────────────────────────────────────┐
│ When a node needs to send to a different module: │
│ │
│ 1. Look up module in ClientPool │
│ 2. Get gRPC connection to status.address │
│ 3. Send message via gRPC │
└────────────────────────────────────────────────────────────────────────┘TinyModule Controller
go
// tinymodule_controller.go
func (r *TinyModuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
instance := &v1alpha1.TinyModule{}
if err := r.Get(ctx, req.NamespacedName, instance); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Is this a REMOTE module? (not our own)
if req.Name != r.Module.GetNameSanitised() {
// Register for cross-module communication
if instance.Status.Addr != "" {
r.ClientPool.Register(req.Name, instance.Status.Addr)
}
return ctrl.Result{}, nil
}
// This is OUR module - only leader updates status
if !r.IsLeader.Load() {
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}
// Update our module's status
instance.Status.Addr = r.Module.Addr
instance.Status.Version = r.Module.Version
instance.Status.Components = r.buildComponentStatus()
if err := r.Status().Update(ctx, instance); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
}Client Pool
The ClientPool manages gRPC connections to other modules:
go
type Pool struct {
clients map[string]*grpc.ClientConn
mu sync.RWMutex
}
func (p *Pool) Register(moduleName, address string) {
p.mu.Lock()
defer p.mu.Unlock()
// Skip if already registered
if _, exists := p.clients[moduleName]; exists {
return
}
// Create gRPC connection
conn, err := grpc.Dial(address,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 10 * time.Second,
Timeout: 3 * time.Second,
PermitWithoutStream: true,
}),
)
if err != nil {
log.Error("failed to connect", "module", moduleName, "error", err)
return
}
p.clients[moduleName] = conn
}
func (p *Pool) Get(moduleName string) (*grpc.ClientConn, bool) {
p.mu.RLock()
defer p.mu.RUnlock()
conn, ok := p.clients[moduleName]
return conn, ok
}Creating TinyModule
Modules automatically create their TinyModule CR on startup:
go
// resource/manager.go
func (m *Manager) CreateModule(ctx context.Context, info ModuleInfo) error {
module := &v1alpha1.TinyModule{
ObjectMeta: metav1.ObjectMeta{
Name: info.NameSanitised,
Namespace: m.namespace,
},
Spec: v1alpha1.TinyModuleSpec{
Image: info.Image,
},
}
return m.client.Create(ctx, module)
}Component Status
The status includes component information from the registry:
go
func (r *TinyModuleReconciler) buildComponentStatus() []v1alpha1.ComponentStatus {
var components []v1alpha1.ComponentStatus
for _, comp := range registry.Get() {
info := comp.GetInfo()
components = append(components, v1alpha1.ComponentStatus{
Name: info.Name,
Info: v1alpha1.ComponentInfo{
Description: info.Description,
Tags: info.Tags,
},
})
}
return components
}Service Discovery Pattern
┌───────────────┐ Watch ┌───────────────────────────────────┐
│ │─────────────▶│ TinyModule CRs │
│ Module A │ │ │
│ │◀─────────────│ - common-module-v1 │
│ │ Updates │ status.address: "...:50051" │
└───────────────┘ │ - http-module-v1 │
│ status.address: "...:50052" │
┌───────────────┐ Watch │ - my-module-v1 │
│ │─────────────▶│ status.address: "...:50053" │
│ Module B │ │ │
│ │◀─────────────└───────────────────────────────────┘
│ │ Updates
└───────────────┘
Each module:
1. Creates its own TinyModule CR
2. Leader updates status.address with gRPC endpoint
3. Watches all TinyModule CRs
4. Registers remote modules in ClientPool
5. Can now communicate with any discovered moduleKubernetes Service
The gRPC address typically points to a Kubernetes Service:
yaml
apiVersion: v1
kind: Service
metadata:
name: common-module-v1
spec:
selector:
app: common-module
ports:
- name: grpc
port: 50051
targetPort: 50051Next Steps
- TinySignal CRD - Triggering execution
- Cross-Module Communication - gRPC details
- Module Discovery - Discovery patterns