Skip to content

Commit 9451c3e

Browse files
committed
refactor: use admin API for node discovery in kubeadm-deployer example
Replace wondersdk (user-level API) with direct admin API calls in the kubeadm-deployer Go binary. The deployer now takes --admin-token and --wonder-net-id flags instead of --api-key, and calls GET /admin/api/v1/wonder-nets/{id}/nodes to discover mesh nodes. This simplifies the demo flow by removing the API key creation step from run-demo.sh, since the admin token is already available.
1 parent 13e19b3 commit 9451c3e

4 files changed

Lines changed: 106 additions & 72 deletions

File tree

examples/kubeadm-deployer/README.md

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ The kubeadm-deployer demonstrates the deployer integration pattern using the **A
99
1. **Admin** creates a wonder net via Admin API using `ADMIN_API_AUTH_TOKEN`
1010
2. **Admin** creates join tokens for worker nodes
1111
3. **Workers** join the mesh network using join tokens
12-
4. **Admin** creates an API key and deployer credentials for the deployer
13-
5. **Deployer** joins the mesh and discovers online worker nodes
12+
4. **Admin** creates deployer mesh credentials via Admin API
13+
5. **Deployer** joins the mesh and discovers online worker nodes via Admin API
1414
6. **Deployer** SSHs to each node over the mesh to install containerd and kubeadm
1515
7. **Deployer** runs `kubeadm init` on the first node (control plane)
1616
8. **Deployer** installs Flannel CNI
@@ -53,9 +53,8 @@ NO_CLEAN=1 ./run-demo.sh
5353
3. **Creates wonder net**: Via Admin API using `ADMIN_API_AUTH_TOKEN`
5454
4. **Creates join token**: Via Admin API for worker authentication
5555
5. **Workers join mesh**: Each worker runs `wonder worker join`
56-
6. **Creates API key**: Via Admin API for deployer authentication
57-
7. **Deployer joins mesh**: Using userspace Tailscale with SOCKS5 proxy (credentials via Admin API)
58-
8. **Runs kubeadm-deployer**: Bootstraps the Kubernetes cluster
56+
6. **Deployer joins mesh**: Using userspace Tailscale with SOCKS5 proxy (credentials via Admin API)
57+
7. **Runs kubeadm-deployer**: Discovers nodes via Admin API, bootstraps the Kubernetes cluster
5958

6059
## Manual Execution
6160

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

90-
# 6. Create API key via Admin API
91-
API_KEY=$(docker exec kubeadm-deployer curl -s -X POST \
92-
-H "Authorization: Bearer $ADMIN_TOKEN" \
93-
-H "Content-Type: application/json" \
94-
-d '{"name": "kubeadm-deployer", "expires_in": "24h"}' \
95-
"http://nginx/coordinator/admin/api/v1/wonder-nets/$WONDER_NET_ID/api-keys" | jq -r '.key')
96-
97-
# 7. Run deployer
89+
# 6. Run deployer (uses Admin API directly for node discovery)
9890
docker exec kubeadm-deployer kubeadm-deployer \
9991
--coordinator-url="http://nginx/coordinator" \
100-
--api-key="$API_KEY" \
92+
--admin-token="$ADMIN_TOKEN" \
93+
--wonder-net-id="$WONDER_NET_ID" \
10194
--verbose
10295
```
10396

@@ -140,8 +133,9 @@ Usage:
140133
kubeadm-deployer [flags]
141134
142135
Flags:
143-
--api-key string API key for authentication (required)
136+
--admin-token string Admin API auth token (required)
144137
--coordinator-url string Wonder Mesh Net coordinator URL (required)
138+
--wonder-net-id string Wonder net ID to deploy into (required)
145139
-h, --help help for kubeadm-deployer
146140
-v, --verbose Enable verbose logging
147141
```
@@ -152,26 +146,16 @@ Default values are hardcoded for demo simplicity:
152146
- SSH user/password: root/worker
153147
- SOCKS5 proxy: localhost:1080
154148

155-
## SDK Usage Example
156-
157-
The deployer demonstrates how to use `wondersdk`:
149+
## Admin API Usage
158150

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

162-
// Create client
163-
client := wondersdk.NewClient(coordinatorURL, apiKey)
164-
165-
// Discover online nodes
166-
nodes, err := client.GetOnlineNodes(ctx, "")
167-
if err != nil {
168-
log.Fatal(err)
169-
}
170-
171-
for _, node := range nodes {
172-
fmt.Printf("Node: %s, IPs: %v\n", node.Name, node.Addresses)
173-
}
174153
```
154+
GET /coordinator/admin/api/v1/wonder-nets/{id}/nodes
155+
Authorization: Bearer <admin-token>
156+
```
157+
158+
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.
175159

176160
## Troubleshooting
177161

examples/kubeadm-deployer/deployer/deployer.go

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ package deployer
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
7+
"io"
68
"log/slog"
79
"net"
10+
"net/http"
811
"os"
912
"regexp"
1013
"strings"
1114
"time"
12-
13-
"github.com/strrl/wonder-mesh-net/pkg/wondersdk"
1415
)
1516

1617
const (
@@ -21,16 +22,26 @@ const (
2122
// Config holds the deployer configuration
2223
type Config struct {
2324
CoordinatorURL string
24-
APIKey string
25+
AdminToken string
26+
WonderNetID string
2527
SSHUser string
2628
SSHPassword string
2729
SOCKS5Addr string
2830
}
2931

32+
// Node represents a node in the mesh
33+
type Node struct {
34+
ID uint64 `json:"id"`
35+
Name string `json:"name"`
36+
Addresses []string `json:"ip_addresses"`
37+
Online bool `json:"online"`
38+
LastSeen string `json:"last_seen,omitempty"`
39+
}
40+
3041
// Deployer orchestrates Kubernetes cluster bootstrap
3142
type Deployer struct {
3243
config Config
33-
sdkClient *wondersdk.Client
44+
httpClient *http.Client
3445
sshExecutor *SSHExecutor
3546

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

59-
sdkClient := wondersdk.NewClient(config.CoordinatorURL, config.APIKey)
60-
6170
sshConfig := SSHConfig{
6271
User: config.SSHUser,
6372
Password: config.SSHPassword,
@@ -72,7 +81,7 @@ func NewDeployer(config Config) (*Deployer, error) {
7281

7382
return &Deployer{
7483
config: config,
75-
sdkClient: sdkClient,
84+
httpClient: &http.Client{Timeout: 30 * time.Second},
7685
sshExecutor: executor,
7786
}, nil
7887
}
@@ -81,7 +90,7 @@ func NewDeployer(config Config) (*Deployer, error) {
8190
func (d *Deployer) Run(ctx context.Context) error {
8291
slog.Info("starting Kubernetes cluster deployment")
8392

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

188-
func (d *Deployer) discoverNodes(ctx context.Context) ([]wondersdk.Node, error) {
189-
slog.Info("discovering nodes from coordinator")
197+
// healthCheck verifies the coordinator is reachable.
198+
func (d *Deployer) healthCheck(ctx context.Context) error {
199+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, d.config.CoordinatorURL+"/health", nil)
200+
if err != nil {
201+
return fmt.Errorf("create request: %w", err)
202+
}
203+
204+
resp, err := d.httpClient.Do(req)
205+
if err != nil {
206+
return fmt.Errorf("send request: %w", err)
207+
}
208+
defer func() { _ = resp.Body.Close() }()
209+
210+
if resp.StatusCode != http.StatusOK {
211+
return fmt.Errorf("health check: status %d", resp.StatusCode)
212+
}
213+
return nil
214+
}
215+
216+
// listNodes calls the admin API to list nodes for the configured wonder net.
217+
func (d *Deployer) listNodes(ctx context.Context) ([]Node, error) {
218+
url := fmt.Sprintf("%s/admin/api/v1/wonder-nets/%s/nodes", d.config.CoordinatorURL, d.config.WonderNetID)
219+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
220+
if err != nil {
221+
return nil, fmt.Errorf("create request: %w", err)
222+
}
223+
req.Header.Set("Authorization", "Bearer "+d.config.AdminToken)
224+
225+
resp, err := d.httpClient.Do(req)
226+
if err != nil {
227+
return nil, fmt.Errorf("send request: %w", err)
228+
}
229+
defer func() { _ = resp.Body.Close() }()
230+
231+
if resp.StatusCode != http.StatusOK {
232+
body, _ := io.ReadAll(resp.Body)
233+
return nil, fmt.Errorf("list nodes: status %d, body: %s", resp.StatusCode, string(body))
234+
}
190235

191-
allNodes, err := d.sdkClient.GetOnlineNodes(ctx, "")
236+
var result struct {
237+
Nodes []Node `json:"nodes"`
238+
}
239+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
240+
return nil, fmt.Errorf("decode response: %w", err)
241+
}
242+
return result.Nodes, nil
243+
}
244+
245+
func (d *Deployer) discoverNodes(ctx context.Context) ([]Node, error) {
246+
slog.Info("discovering nodes from coordinator via admin API")
247+
248+
allNodes, err := d.listNodes(ctx)
192249
if err != nil {
193-
return nil, fmt.Errorf("get online nodes: %w", err)
250+
return nil, fmt.Errorf("list nodes: %w", err)
194251
}
195252

196253
hostname, _ := os.Hostname()
197254

198-
nodes := make([]wondersdk.Node, 0, len(allNodes))
255+
var online []Node
199256
for _, node := range allNodes {
257+
if !node.Online {
258+
continue
259+
}
200260
if node.Name == hostname {
201261
slog.Debug("skipping self", "name", node.Name)
202262
continue
203263
}
204-
nodes = append(nodes, node)
264+
online = append(online, node)
205265
}
206266

207-
slog.Info("discovered nodes", "count", len(nodes), "excluded_self", hostname)
208-
for _, node := range nodes {
267+
slog.Info("discovered nodes", "total", len(allNodes), "online", len(online), "excluded_self", hostname)
268+
for _, node := range online {
209269
slog.Debug("node", "name", node.Name, "addresses", node.Addresses, "online", node.Online)
210270
}
211271

212-
return nodes, nil
272+
return online, nil
213273
}
214274

215275
// selectIPv4 returns the first IPv4 address from the list.
@@ -239,7 +299,7 @@ func selectIPv4(addresses []string) string {
239299
//
240300
// Returns an error if fewer than 1 node is provided or if the control plane node
241301
// has no valid IPv4 address. Worker nodes without IPv4 addresses are silently skipped.
242-
func (d *Deployer) selectNodes(nodes []wondersdk.Node) error {
302+
func (d *Deployer) selectNodes(nodes []Node) error {
243303
if len(nodes) < 1 {
244304
return fmt.Errorf("at least 1 node required, found %d", len(nodes))
245305
}

examples/kubeadm-deployer/main.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import (
1414

1515
var (
1616
coordinatorURL string
17-
apiKey string
17+
adminToken string
18+
wonderNetID string
1819
verbose bool
1920
)
2021

@@ -35,18 +36,20 @@ The deployer will:
3536
5. Join remaining nodes as workers
3637
3738
Prerequisites:
38-
- Wonder Mesh Net coordinator running with workers joined
39-
- API key created for the deployer
39+
- Wonder Mesh Net coordinator running with admin API enabled and workers joined
40+
- Admin API auth token and wonder net ID
4041
- Tailscale SOCKS5 proxy running (userspace networking)`,
4142
RunE: runDeploy,
4243
}
4344

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

4850
rootCmd.MarkFlagRequired("coordinator-url")
49-
rootCmd.MarkFlagRequired("api-key")
51+
rootCmd.MarkFlagRequired("admin-token")
52+
rootCmd.MarkFlagRequired("wonder-net-id")
5053

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

7679
d, err := deployer.NewDeployer(deployer.Config{
7780
CoordinatorURL: coordinatorURL,
78-
APIKey: apiKey,
81+
AdminToken: adminToken,
82+
WonderNetID: wonderNetID,
7983
})
8084
if err != nil {
8185
return fmt.Errorf("create deployer: %w", err)

examples/kubeadm-deployer/run-demo.sh

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -138,21 +138,6 @@ log_info "Testing mesh connectivity..."
138138
docker exec k8s-node-1 ping -c 2 "$NODE2_IP" >/dev/null && log_info " k8s-node-1 -> k8s-node-2: OK"
139139
docker exec k8s-node-1 ping -c 2 "$NODE3_IP" >/dev/null && log_info " k8s-node-1 -> k8s-node-3: OK"
140140

141-
log_info "Creating API key for deployer via Admin API..."
142-
API_KEY_RESPONSE=$(docker exec kubeadm-deployer curl -s -X POST \
143-
-H "Authorization: Bearer $ADMIN_TOKEN" \
144-
-H "Content-Type: application/json" \
145-
-d '{"name": "kubeadm-deployer", "expires_in": "24h"}' \
146-
"http://nginx/coordinator/admin/api/v1/wonder-nets/$WONDER_NET_ID/api-keys")
147-
148-
API_KEY=$(echo "$API_KEY_RESPONSE" | jq -r '.key // empty')
149-
if [ -z "$API_KEY" ]; then
150-
log_error "Failed to create API key"
151-
echo "$API_KEY_RESPONSE"
152-
exit 1
153-
fi
154-
log_info "API key created"
155-
156141
log_info "Deployer joining mesh via Admin API..."
157142
DEPLOYER_JOIN_RESPONSE=$(docker exec kubeadm-deployer curl -s -X POST \
158143
-H "Authorization: Bearer $ADMIN_TOKEN" \
@@ -209,7 +194,8 @@ echo "==========================================="
209194
log_info "Starting kubeadm-deployer..."
210195
docker exec kubeadm-deployer kubeadm-deployer \
211196
--coordinator-url="http://nginx/coordinator" \
212-
--api-key="$API_KEY" \
197+
--admin-token="$ADMIN_TOKEN" \
198+
--wonder-net-id="$WONDER_NET_ID" \
213199
--verbose
214200

215201
DEPLOY_EXIT=$?

0 commit comments

Comments
 (0)