From 9451c3ecc06d0c0d79b3eb38792d35f59afcb016 Mon Sep 17 00:00:00 2001 From: Zhiqiang ZHOU Date: Sun, 22 Feb 2026 22:03:15 -0800 Subject: [PATCH] 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. --- examples/kubeadm-deployer/README.md | 48 ++++------ .../kubeadm-deployer/deployer/deployer.go | 96 +++++++++++++++---- examples/kubeadm-deployer/main.go | 16 ++-- examples/kubeadm-deployer/run-demo.sh | 18 +--- 4 files changed, 106 insertions(+), 72 deletions(-) diff --git a/examples/kubeadm-deployer/README.md b/examples/kubeadm-deployer/README.md index af02cb6..d6346d8 100644 --- a/examples/kubeadm-deployer/README.md +++ b/examples/kubeadm-deployer/README.md @@ -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 @@ -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 @@ -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 ``` @@ -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 ``` @@ -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 +``` + +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 diff --git a/examples/kubeadm-deployer/deployer/deployer.go b/examples/kubeadm-deployer/deployer/deployer.go index 89da9b1..f4632f9 100644 --- a/examples/kubeadm-deployer/deployer/deployer.go +++ b/examples/kubeadm-deployer/deployer/deployer.go @@ -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 ( @@ -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 @@ -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, @@ -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 } @@ -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") @@ -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. @@ -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)) } diff --git a/examples/kubeadm-deployer/main.go b/examples/kubeadm-deployer/main.go index 904ff36..3f28309 100644 --- a/examples/kubeadm-deployer/main.go +++ b/examples/kubeadm-deployer/main.go @@ -14,7 +14,8 @@ import ( var ( coordinatorURL string - apiKey string + adminToken string + wonderNetID string verbose bool ) @@ -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) @@ -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) diff --git a/examples/kubeadm-deployer/run-demo.sh b/examples/kubeadm-deployer/run-demo.sh index c6bce85..24f6781 100755 --- a/examples/kubeadm-deployer/run-demo.sh +++ b/examples/kubeadm-deployer/run-demo.sh @@ -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" \ @@ -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=$?