Skip to content

Hello World Component

Let's build a complete component from scratch. This tutorial creates a "Greeter" component that receives a name and outputs a greeting.

What We'll Build

┌─────────────────────────────────────────┐
│              Greeter                     │
│                                          │
│  ┌─────────┐              ┌──────────┐  │
│  │  input  │              │  output  │  │
│  │  (name) │──────────────│(greeting)│  │
│  └─────────┘              └──────────┘  │
│                                          │
│  Settings: prefix ("Hello", "Hi", etc)   │
└─────────────────────────────────────────┘

Step 1: Create Project Structure

bash
mkdir greeter-module && cd greeter-module
go mod init github.com/myorg/greeter-module

# Create directories
mkdir -p cmd components/greeter

Step 2: Define Message Types

Create components/greeter/types.go:

go
package greeter

// Input message - what the component receives
type Input struct {
    Name string `json:"name" required:"true" title:"Name" description:"Name to greet"`
}

// Output message - what the component produces
type Output struct {
    Greeting string `json:"greeting" title:"Greeting"`
    Original string `json:"original" title:"Original Name"`
}

// Settings - component configuration
type Settings struct {
    Prefix string `json:"prefix" required:"true" title:"Greeting Prefix" default:"Hello"`
}

Understanding Struct Tags

TagPurpose
json:"name"JSON field name
required:"true"Field must be provided
title:"Name"Label shown in UI
description:"..."Help text in UI
default:"Hello"Default value

Step 3: Implement the Component

Create components/greeter/component.go:

go
package greeter

import (
    "context"
    "fmt"

    "github.com/tiny-systems/module/api/v1alpha1"
    "github.com/tiny-systems/module/module"
    "github.com/tiny-systems/module/registry"
)

const (
    ComponentName = "greeter"

    // Port names
    InputPort    = "input"
    OutputPort   = "output"
)

// Component implements module.Component
type Component struct {
    settings Settings
}

// GetInfo returns component metadata
func (c *Component) GetInfo() module.ComponentInfo {
    return module.ComponentInfo{
        Name:        ComponentName,
        Description: "Greets a person by name",
        Info:        "Receives a name and outputs a personalized greeting",
        Tags:        []string{"example", "greeting"},
    }
}

// Ports defines the component's input and output ports
func (c *Component) Ports() []module.Port {
    return []module.Port{
        // Settings port (system port)
        {
            Name:          v1alpha1.SettingsPort,
            Label:         "Settings",
            Configuration: c.settings,
            Source:        false,
        },
        // Input port
        {
            Name:          InputPort,
            Label:         "Input",
            Position:      module.Left,
            Configuration: Input{},
            Source:        false,
        },
        // Output port
        {
            Name:          OutputPort,
            Label:         "Output",
            Position:      module.Right,
            Configuration: Output{},
            Source:        true,
        },
    }
}

// Handle processes incoming messages
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
    switch port {

    // Handle settings updates
    case v1alpha1.SettingsPort:
        settings, ok := msg.(Settings)
        if !ok {
            return fmt.Errorf("invalid settings type: %T", msg)
        }
        c.settings = settings
        return nil

    // Handle input messages
    case InputPort:
        input, ok := msg.(Input)
        if !ok {
            return fmt.Errorf("invalid input type: %T", msg)
        }

        // Create greeting
        greeting := fmt.Sprintf("%s, %s!", c.settings.Prefix, input.Name)

        // Send to output port
        return output(ctx, OutputPort, Output{
            Greeting: greeting,
            Original: input.Name,
        })
    }

    return fmt.Errorf("unknown port: %s", port)
}

// Instance creates a new component instance (factory method)
func (c *Component) Instance() module.Component {
    return &Component{
        settings: Settings{
            Prefix: "Hello", // Default value
        },
    }
}

// Register component on package init
func init() {
    registry.Register(&Component{})
}

Step 4: Create the Main Entry Point

Create cmd/main.go:

go
package main

import (
    "github.com/tiny-systems/module/cli"

    // Import components to register them
    _ "github.com/myorg/greeter-module/components/greeter"
)

func main() {
    cli.Run()
}

Step 5: Install Dependencies

bash
go mod tidy

Step 6: Build and Test

Build the Module

bash
go build -o greeter-module ./cmd

View Registered Components

bash
./greeter-module tools info

Run Locally

bash
./greeter-module run \
    --name=greeter-module \
    --version=1.0.0 \
    --namespace=default

Step 7: Test with Kubernetes

Create a test TinyNode:

yaml
# test-node.yaml
apiVersion: operator.tinysystems.io/v1alpha1
kind: TinyNode
metadata:
  name: test-greeter
  namespace: default
spec:
  module: greeter-module
  component: greeter
  version: "1.0.0"

Apply and trigger:

bash
# Apply the node
kubectl apply -f test-node.yaml

# Create a signal to trigger it
cat <<EOF | kubectl apply -f -
apiVersion: operator.tinysystems.io/v1alpha1
kind: TinySignal
metadata:
  name: test-signal
  namespace: default
spec:
  node: test-greeter
  port: input
  data:
    name: "World"
EOF

Check logs:

bash
# View module logs
kubectl logs -l app=greeter-module -f

Complete Project Structure

greeter-module/
├── cmd/
│   └── main.go              # Entry point
├── components/
│   └── greeter/
│       ├── component.go     # Component implementation
│       └── types.go         # Message types
├── go.mod
├── go.sum
├── Dockerfile               # (optional) For containerization
└── README.md

Adding a Dockerfile

dockerfile
# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o greeter-module ./cmd

FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/greeter-module .
ENTRYPOINT ["./greeter-module"]

Build and push:

bash
docker build -t myregistry/greeter-module:1.0.0 .
docker push myregistry/greeter-module:1.0.0

Key Takeaways

  1. Struct tags control UI rendering and validation
  2. Handle() receives ALL messages - use port name to route
  3. Settings port provides component configuration
  4. output() callback sends data to connected nodes
  5. Instance() is a factory - return a new instance each time
  6. init() registers the component with the registry

Next Steps

Build flow-based applications on Kubernetes