From de8af61ba749f5ac1332fc1647203e4677de92bd Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sat, 17 May 2025 16:51:25 +0200 Subject: [PATCH] feat: add cluster join and leave handlers --- README.md | 3 ++ api/rcond.yaml | 67 +++++++++++++++++++++++++++++++++++++++++ config/rcond-agent.yaml | 4 +-- pkg/http/handlers.go | 42 ++++++++++++++++++++++++-- pkg/http/server.go | 4 ++- 5 files changed, 115 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 52babd5..919f375 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,9 @@ All endpoints except `/health` require authentication via an API token passed in | POST | `/system/restart` | Restart the system | | POST | `/system/shutdown` | Shutdown the system | | GET | `/cluster/members` | Get the cluster members | +| POST | `/cluster/join` | Join cluster nodes | +| POST | `/cluster/leave` | Leave the cluster | + ### Response Codes diff --git a/api/rcond.yaml b/api/rcond.yaml index 8e973dc..587d80d 100644 --- a/api/rcond.yaml +++ b/api/rcond.yaml @@ -601,3 +601,70 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /cluster/join: + post: + summary: Join the cluster + description: Join the cluster with the provided addresses + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + join: + type: array + items: + type: string + responses: + '200': + description: Successfully joined the cluster + content: + application/json: + schema: + type: object + properties: + joined: + description: Number of nodes successfully joined + type: integer + example: 1 + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /cluster/leave: + post: + summary: Leave the cluster + description: Leave the cluster + responses: + '200': + description: Successfully left the cluster + content: + application/json: + schema: + type: object + properties: + success: + description: Indicates if the node has left the cluster + type: boolean + example: true + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' diff --git a/config/rcond-agent.yaml b/config/rcond-agent.yaml index 67147a3..4ec9f93 100644 --- a/config/rcond-agent.yaml +++ b/config/rcond-agent.yaml @@ -11,5 +11,5 @@ cluster: advertise_port: 7947 bind_addr: 0.0.0.0 bind_port: 7947 - join: - - 127.0.0.1:7946 \ No newline at end of file +# join: +# - 127.0.0.1:7946 \ No newline at end of file diff --git a/pkg/http/handlers.go b/pkg/http/handlers.go index 0dcb54a..8651207 100644 --- a/pkg/http/handlers.go +++ b/pkg/http/handlers.go @@ -5,6 +5,7 @@ import ( "encoding/json" "log" "net/http" + "strings" "github.com/0x1d/rcond/pkg/cluster" network "github.com/0x1d/rcond/pkg/network" @@ -289,12 +290,49 @@ func HandleShutdown(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } -func ClusterAgentWrapper(agent *cluster.Agent) func(http.ResponseWriter, *http.Request) { +func ClusterAgentHandler(agent *cluster.Agent, handler func(http.ResponseWriter, *http.Request, *cluster.Agent)) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - HandleClusterMembers(w, r, agent) + handler(w, r, agent) } } +func HandleClusterJoin(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) { + var joinRequest struct { + Join []string `json:"join"` + } + err := json.NewDecoder(r.Body).Decode(&joinRequest) + if err != nil { + writeError(w, "Invalid request body", http.StatusBadRequest) + return + } + joinAddrs := strings.Join(joinRequest.Join, ",") + if joinAddrs == "" { + writeError(w, "No join addresses provided", http.StatusBadRequest) + return + } + + addrs := strings.Split(joinAddrs, ",") + n, err := agent.Join(addrs, true) + if err != nil { + writeError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]int{"joined": n}) +} + +func HandleClusterLeave(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) { + err := agent.Leave() + if err != nil { + writeError(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + func HandleClusterMembers(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) { if agent == nil { writeError(w, "cluster agent is not initialized", http.StatusInternalServerError) diff --git a/pkg/http/server.go b/pkg/http/server.go index e0063d8..e99fe0d 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -76,7 +76,9 @@ func (s *Server) RegisterRoutes() { s.router.HandleFunc("/users/{user}/keys/{fingerprint}", s.verifyToken(HandleRemoveAuthorizedKey)).Methods(http.MethodDelete) s.router.HandleFunc("/system/restart", s.verifyToken(HandleReboot)).Methods(http.MethodPost) s.router.HandleFunc("/system/shutdown", s.verifyToken(HandleShutdown)).Methods(http.MethodPost) - s.router.HandleFunc("/cluster/members", s.verifyToken(ClusterAgentWrapper(s.clusterAgent))).Methods(http.MethodGet) + s.router.HandleFunc("/cluster/members", s.verifyToken(ClusterAgentHandler(s.clusterAgent, HandleClusterMembers))).Methods(http.MethodGet) + s.router.HandleFunc("/cluster/join", s.verifyToken(ClusterAgentHandler(s.clusterAgent, HandleClusterJoin))).Methods(http.MethodPost) + s.router.HandleFunc("/cluster/leave", s.verifyToken(ClusterAgentHandler(s.clusterAgent, HandleClusterLeave))).Methods(http.MethodPost) } func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {