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/greeterStep 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
| Tag | Purpose |
|---|---|
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 tidyStep 6: Build and Test
Build the Module
bash
go build -o greeter-module ./cmdView Registered Components
bash
./greeter-module tools infoRun Locally
bash
./greeter-module run \
--name=greeter-module \
--version=1.0.0 \
--namespace=defaultStep 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"
EOFCheck logs:
bash
# View module logs
kubectl logs -l app=greeter-module -fComplete 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.mdAdding 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.0Key Takeaways
- Struct tags control UI rendering and validation
- Handle() receives ALL messages - use port name to route
- Settings port provides component configuration
- output() callback sends data to connected nodes
- Instance() is a factory - return a new instance each time
- init() registers the component with the registry
Next Steps
- Project Structure - Organize larger modules
- Ports and Messages - Deep dive into ports
- Component Patterns - Common patterns