Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 16 additions & 32 deletions examples/kubeadm-deployer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ The kubeadm-deployer demonstrates the deployer integration pattern using the **A
1. **Admin** creates a wonder net via Admin API using `ADMIN_API_AUTH_TOKEN`
2. **Admin** creates join tokens for worker nodes
3. **Workers** join the mesh network using join tokens
4. **Admin** creates an API key and deployer credentials for the deployer
5. **Deployer** joins the mesh and discovers online worker nodes
4. **Admin** creates deployer mesh credentials via Admin API
5. **Deployer** joins the mesh and discovers online worker nodes via Admin API
6. **Deployer** SSHs to each node over the mesh to install containerd and kubeadm
7. **Deployer** runs `kubeadm init` on the first node (control plane)
8. **Deployer** installs Flannel CNI
Expand Down Expand Up @@ -53,9 +53,8 @@ NO_CLEAN=1 ./run-demo.sh
3. **Creates wonder net**: Via Admin API using `ADMIN_API_AUTH_TOKEN`
4. **Creates join token**: Via Admin API for worker authentication
5. **Workers join mesh**: Each worker runs `wonder worker join`
6. **Creates API key**: Via Admin API for deployer authentication
7. **Deployer joins mesh**: Using userspace Tailscale with SOCKS5 proxy (credentials via Admin API)
8. **Runs kubeadm-deployer**: Bootstraps the Kubernetes cluster
6. **Deployer joins mesh**: Using userspace Tailscale with SOCKS5 proxy (credentials via Admin API)
7. **Runs kubeadm-deployer**: Discovers nodes via Admin API, bootstraps the Kubernetes cluster

## Manual Execution

Expand Down Expand Up @@ -87,17 +86,11 @@ JOIN_TOKEN=$(docker exec kubeadm-deployer curl -s -X POST \
# 5. Join workers (see run-demo.sh for full flow)
# ...

# 6. Create API key via Admin API
API_KEY=$(docker exec kubeadm-deployer curl -s -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "kubeadm-deployer", "expires_in": "24h"}' \
"http://nginx/coordinator/admin/api/v1/wonder-nets/$WONDER_NET_ID/api-keys" | jq -r '.key')

# 7. Run deployer
# 6. Run deployer (uses Admin API directly for node discovery)
docker exec kubeadm-deployer kubeadm-deployer \
--coordinator-url="http://nginx/coordinator" \
--api-key="$API_KEY" \
--admin-token="$ADMIN_TOKEN" \
--wonder-net-id="$WONDER_NET_ID" \
--verbose
```

Expand Down Expand Up @@ -140,8 +133,9 @@ Usage:
kubeadm-deployer [flags]

Flags:
--api-key string API key for authentication (required)
--admin-token string Admin API auth token (required)
--coordinator-url string Wonder Mesh Net coordinator URL (required)
--wonder-net-id string Wonder net ID to deploy into (required)
-h, --help help for kubeadm-deployer
-v, --verbose Enable verbose logging
```
Expand All @@ -152,26 +146,16 @@ Default values are hardcoded for demo simplicity:
- SSH user/password: root/worker
- SOCKS5 proxy: localhost:1080

## SDK Usage Example

The deployer demonstrates how to use `wondersdk`:
## Admin API Usage

```go
import "github.com/strrl/wonder-mesh-net/pkg/wondersdk"
The deployer uses the Admin API to discover nodes in a wonder net:

// Create client
client := wondersdk.NewClient(coordinatorURL, apiKey)

// Discover online nodes
nodes, err := client.GetOnlineNodes(ctx, "")
if err != nil {
log.Fatal(err)
}

for _, node := range nodes {
fmt.Printf("Node: %s, IPs: %v\n", node.Name, node.Addresses)
}
```
GET /coordinator/admin/api/v1/wonder-nets/{id}/nodes
Authorization: Bearer <admin-token>
```

This returns all nodes (online and offline) in the specified wonder net. The deployer filters to online nodes and uses their Tailscale IPs for SSH connectivity via SOCKS5 proxy.

## Troubleshooting

Expand Down
96 changes: 78 additions & 18 deletions examples/kubeadm-deployer/deployer/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package deployer

import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"os"
"regexp"
"strings"
"time"

"github.com/strrl/wonder-mesh-net/pkg/wondersdk"
)

const (
Expand All @@ -21,16 +22,26 @@ const (
// Config holds the deployer configuration
type Config struct {
CoordinatorURL string
APIKey string
AdminToken string
WonderNetID string
SSHUser string
SSHPassword string
SOCKS5Addr string
}

// Node represents a node in the mesh
type Node struct {
ID uint64 `json:"id"`
Name string `json:"name"`
Addresses []string `json:"ip_addresses"`
Online bool `json:"online"`
LastSeen string `json:"last_seen,omitempty"`
}

// Deployer orchestrates Kubernetes cluster bootstrap
type Deployer struct {
config Config
sdkClient *wondersdk.Client
httpClient *http.Client
sshExecutor *SSHExecutor

// Tailscale IPs - used for SSH connectivity via SOCKS5 proxy
Expand All @@ -56,8 +67,6 @@ func NewDeployer(config Config) (*Deployer, error) {
config.SOCKS5Addr = "localhost:1080"
}

sdkClient := wondersdk.NewClient(config.CoordinatorURL, config.APIKey)

sshConfig := SSHConfig{
User: config.SSHUser,
Password: config.SSHPassword,
Expand All @@ -72,7 +81,7 @@ func NewDeployer(config Config) (*Deployer, error) {

return &Deployer{
config: config,
sdkClient: sdkClient,
httpClient: &http.Client{Timeout: 30 * time.Second},
sshExecutor: executor,
}, nil
}
Expand All @@ -81,7 +90,7 @@ func NewDeployer(config Config) (*Deployer, error) {
func (d *Deployer) Run(ctx context.Context) error {
slog.Info("starting Kubernetes cluster deployment")

if err := d.sdkClient.Health(ctx); err != nil {
if err := d.healthCheck(ctx); err != nil {
return fmt.Errorf("coordinator health check: %w", err)
}
slog.Info("coordinator is healthy")
Expand Down Expand Up @@ -185,31 +194,82 @@ func (d *Deployer) Reset(ctx context.Context) error {
return nil
}

func (d *Deployer) discoverNodes(ctx context.Context) ([]wondersdk.Node, error) {
slog.Info("discovering nodes from coordinator")
// healthCheck verifies the coordinator is reachable.
func (d *Deployer) healthCheck(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, d.config.CoordinatorURL+"/health", nil)
if err != nil {
return fmt.Errorf("create request: %w", err)
}

resp, err := d.httpClient.Do(req)
if err != nil {
return fmt.Errorf("send request: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("health check: status %d", resp.StatusCode)
}
return nil
}

// listNodes calls the admin API to list nodes for the configured wonder net.
func (d *Deployer) listNodes(ctx context.Context) ([]Node, error) {
url := fmt.Sprintf("%s/admin/api/v1/wonder-nets/%s/nodes", d.config.CoordinatorURL, d.config.WonderNetID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+d.config.AdminToken)

resp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("send request: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("list nodes: status %d, body: %s", resp.StatusCode, string(body))
}

allNodes, err := d.sdkClient.GetOnlineNodes(ctx, "")
var result struct {
Nodes []Node `json:"nodes"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return result.Nodes, nil
}

func (d *Deployer) discoverNodes(ctx context.Context) ([]Node, error) {
slog.Info("discovering nodes from coordinator via admin API")

allNodes, err := d.listNodes(ctx)
if err != nil {
return nil, fmt.Errorf("get online nodes: %w", err)
return nil, fmt.Errorf("list nodes: %w", err)
}

hostname, _ := os.Hostname()

nodes := make([]wondersdk.Node, 0, len(allNodes))
var online []Node
for _, node := range allNodes {
if !node.Online {
continue
}
if node.Name == hostname {
slog.Debug("skipping self", "name", node.Name)
continue
}
nodes = append(nodes, node)
online = append(online, node)
}

slog.Info("discovered nodes", "count", len(nodes), "excluded_self", hostname)
for _, node := range nodes {
slog.Info("discovered nodes", "total", len(allNodes), "online", len(online), "excluded_self", hostname)
for _, node := range online {
slog.Debug("node", "name", node.Name, "addresses", node.Addresses, "online", node.Online)
}

return nodes, nil
return online, nil
}

// selectIPv4 returns the first IPv4 address from the list.
Expand Down Expand Up @@ -239,7 +299,7 @@ func selectIPv4(addresses []string) string {
//
// Returns an error if fewer than 1 node is provided or if the control plane node
// has no valid IPv4 address. Worker nodes without IPv4 addresses are silently skipped.
func (d *Deployer) selectNodes(nodes []wondersdk.Node) error {
func (d *Deployer) selectNodes(nodes []Node) error {
if len(nodes) < 1 {
return fmt.Errorf("at least 1 node required, found %d", len(nodes))
}
Expand Down
16 changes: 10 additions & 6 deletions examples/kubeadm-deployer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import (

var (
coordinatorURL string
apiKey string
adminToken string
wonderNetID string
verbose bool
)

Expand All @@ -35,18 +36,20 @@ The deployer will:
5. Join remaining nodes as workers

Prerequisites:
- Wonder Mesh Net coordinator running with workers joined
- API key created for the deployer
- Wonder Mesh Net coordinator running with admin API enabled and workers joined
- Admin API auth token and wonder net ID
- Tailscale SOCKS5 proxy running (userspace networking)`,
RunE: runDeploy,
}

rootCmd.Flags().StringVar(&coordinatorURL, "coordinator-url", "", "Wonder Mesh Net coordinator URL (required)")
rootCmd.Flags().StringVar(&apiKey, "api-key", "", "API key for authentication (required)")
rootCmd.Flags().StringVar(&adminToken, "admin-token", "", "Admin API auth token (required)")
rootCmd.Flags().StringVar(&wonderNetID, "wonder-net-id", "", "Wonder net ID to deploy into (required)")
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")

rootCmd.MarkFlagRequired("coordinator-url")
rootCmd.MarkFlagRequired("api-key")
rootCmd.MarkFlagRequired("admin-token")
rootCmd.MarkFlagRequired("wonder-net-id")

if err := rootCmd.Execute(); err != nil {
os.Exit(1)
Expand Down Expand Up @@ -75,7 +78,8 @@ func runDeploy(cmd *cobra.Command, args []string) error {

d, err := deployer.NewDeployer(deployer.Config{
CoordinatorURL: coordinatorURL,
APIKey: apiKey,
AdminToken: adminToken,
WonderNetID: wonderNetID,
})
if err != nil {
return fmt.Errorf("create deployer: %w", err)
Expand Down
18 changes: 2 additions & 16 deletions examples/kubeadm-deployer/run-demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -138,21 +138,6 @@ log_info "Testing mesh connectivity..."
docker exec k8s-node-1 ping -c 2 "$NODE2_IP" >/dev/null && log_info " k8s-node-1 -> k8s-node-2: OK"
docker exec k8s-node-1 ping -c 2 "$NODE3_IP" >/dev/null && log_info " k8s-node-1 -> k8s-node-3: OK"

log_info "Creating API key for deployer via Admin API..."
API_KEY_RESPONSE=$(docker exec kubeadm-deployer curl -s -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "kubeadm-deployer", "expires_in": "24h"}' \
"http://nginx/coordinator/admin/api/v1/wonder-nets/$WONDER_NET_ID/api-keys")

API_KEY=$(echo "$API_KEY_RESPONSE" | jq -r '.key // empty')
if [ -z "$API_KEY" ]; then
log_error "Failed to create API key"
echo "$API_KEY_RESPONSE"
exit 1
fi
log_info "API key created"

log_info "Deployer joining mesh via Admin API..."
DEPLOYER_JOIN_RESPONSE=$(docker exec kubeadm-deployer curl -s -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
Expand Down Expand Up @@ -209,7 +194,8 @@ echo "==========================================="
log_info "Starting kubeadm-deployer..."
docker exec kubeadm-deployer kubeadm-deployer \
--coordinator-url="http://nginx/coordinator" \
--api-key="$API_KEY" \
--admin-token="$ADMIN_TOKEN" \
--wonder-net-id="$WONDER_NET_ID" \
--verbose

DEPLOY_EXIT=$?
Expand Down
Loading