Skip to content

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 module

Kubernetes 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: 50051

Next Steps

Build flow-based applications on Kubernetes