feat: add cluster join and leave handlers

This commit is contained in:
2025-05-17 16:51:25 +02:00
parent 49014c951f
commit de8af61ba7
5 changed files with 115 additions and 5 deletions

View File

@@ -130,6 +130,9 @@ All endpoints except `/health` require authentication via an API token passed in
| POST | `/system/restart` | Restart the system | | POST | `/system/restart` | Restart the system |
| POST | `/system/shutdown` | Shutdown the system | | POST | `/system/shutdown` | Shutdown the system |
| GET | `/cluster/members` | Get the cluster members | | GET | `/cluster/members` | Get the cluster members |
| POST | `/cluster/join` | Join cluster nodes |
| POST | `/cluster/leave` | Leave the cluster |
### Response Codes ### Response Codes

View File

@@ -601,3 +601,70 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Error' $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'

View File

@@ -11,5 +11,5 @@ cluster:
advertise_port: 7947 advertise_port: 7947
bind_addr: 0.0.0.0 bind_addr: 0.0.0.0
bind_port: 7947 bind_port: 7947
join: # join:
- 127.0.0.1:7946 # - 127.0.0.1:7946

View File

@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"strings"
"github.com/0x1d/rcond/pkg/cluster" "github.com/0x1d/rcond/pkg/cluster"
network "github.com/0x1d/rcond/pkg/network" 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"}) 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) { 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) { func HandleClusterMembers(w http.ResponseWriter, r *http.Request, agent *cluster.Agent) {
if agent == nil { if agent == nil {
writeError(w, "cluster agent is not initialized", http.StatusInternalServerError) writeError(w, "cluster agent is not initialized", http.StatusInternalServerError)

View File

@@ -76,7 +76,9 @@ func (s *Server) RegisterRoutes() {
s.router.HandleFunc("/users/{user}/keys/{fingerprint}", s.verifyToken(HandleRemoveAuthorizedKey)).Methods(http.MethodDelete) 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/restart", s.verifyToken(HandleReboot)).Methods(http.MethodPost)
s.router.HandleFunc("/system/shutdown", s.verifyToken(HandleShutdown)).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) { func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {