RegisterNodeEnrollTokenEndpoint 注册 POST /v1/nodes/{nodeID}/enroll-token, 由管理员调用以生成 pulse-node 安装期使用的一次性 enrollment token。 该路径必须挂在已加 admin 鉴权的 mux 上。 resolveServerURL 用于决定回写到 install/manual 命令中的控制面 URL; 优先级:URL 查询参数 ?server= > resolveServerURL(r) > 由请求 Host 推断。
(mux *http.ServeMux, nodeStore nodes.Store, tokens enrolltokens.Store, resolveServerURL ServerURLResolver)
| 45 | // resolveServerURL 用于决定回写到 install/manual 命令中的控制面 URL; |
| 46 | // 优先级:URL 查询参数 ?server=<url> > resolveServerURL(r) > 由请求 Host 推断。 |
| 47 | func RegisterNodeEnrollTokenEndpoint(mux *http.ServeMux, nodeStore nodes.Store, tokens enrolltokens.Store, resolveServerURL ServerURLResolver) { |
| 48 | mux.HandleFunc("POST /v1/nodes/{nodeID}/enroll-token", func(w http.ResponseWriter, r *http.Request) { |
| 49 | nodeID := strings.TrimSpace(r.PathValue("nodeID")) |
| 50 | if nodeID == "" { |
| 51 | writeJSON(w, http.StatusBadRequest, map[string]any{"error": "node_id is required"}) |
| 52 | return |
| 53 | } |
| 54 | |
| 55 | if _, err := nodeStore.Get(nodeID); err != nil { |
| 56 | if errors.Is(err, nodes.ErrNodeNotFound) { |
| 57 | writeJSON(w, http.StatusNotFound, map[string]any{"error": "node not found"}) |
| 58 | return |
| 59 | } |
| 60 | internalError(w, r, fmt.Errorf("nodes.Get(%q): %w", nodeID, err)) |
| 61 | return |
| 62 | } |
| 63 | |
| 64 | // 请求体可选;为空时使用默认 TTL。 |
| 65 | var req enrollTokenRequest |
| 66 | if r.Body != nil { |
| 67 | body, _ := io.ReadAll(r.Body) |
| 68 | if len(strings.TrimSpace(string(body))) > 0 { |
| 69 | if err := json.Unmarshal(body, &req); err != nil { |
| 70 | writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid json body"}) |
| 71 | return |
| 72 | } |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | ttl := defaultEnrollTokenTTL |
| 77 | if req.TTLSeconds > 0 { |
| 78 | ttl = time.Duration(req.TTLSeconds) * time.Second |
| 79 | } |
| 80 | if ttl > maxEnrollTokenTTL { |
| 81 | ttl = maxEnrollTokenTTL |
| 82 | } |
| 83 | |
| 84 | token, err := newEnrollTokenString() |
| 85 | if err != nil { |
| 86 | internalError(w, r, fmt.Errorf("generate enroll token: %w", err)) |
| 87 | return |
| 88 | } |
| 89 | |
| 90 | now := time.Now().UTC() |
| 91 | expiresAt := now.Add(ttl) |
| 92 | record := enrolltokens.Token{ |
| 93 | Token: token, |
| 94 | NodeID: nodeID, |
| 95 | ExpiresAt: expiresAt, |
| 96 | CreatedAt: now, |
| 97 | } |
| 98 | if err := tokens.Insert(r.Context(), record); err != nil { |
| 99 | internalError(w, r, fmt.Errorf("insert enroll token: %w", err)) |
| 100 | return |
| 101 | } |
| 102 | |
| 103 | serverURL := strings.TrimSpace(r.URL.Query().Get("server")) |
| 104 | if serverURL == "" && resolveServerURL != nil { |