Files
worker/internal/webapp/handlers_cluster.go
Gleb Tv e987f24903
Все проверки выполнены успешно
CI / test (push) Successful in 2m32s
Docker / Build and publish worker image (push) Successful in 18m17s
fix(worker): harden control-plane lifecycle
- reconnect safely after token rotation and retry leased results
- reject malformed tasks and remove production cluster debug mutation
- validate environment files and require immutable container images

BREAKING CHANGE: Docker install, deploy, and Compose now require an
immutable repository@sha256 image reference.
2026-07-19 23:11:43 +03:00

80 строки
2.9 KiB
Go

package webapp
import (
"encoding/json"
"errors"
"net/http"
)
// clusterStatusResponse is the JSON the operator-facing
// /web/api/cluster/status endpoint returns. The shape is stable so the
// e2e shell script and any future frontend pages can pin against it.
//
// FSMConfigVersion / FSMOutboxLen / FSMPartition surface the FSM-side
// operator signals from plan section 6.1 (config_version, outbox
// length, partition_state) so a single GET tells the operator what
// config the cluster has adopted, whether the notification outbox is
// draining, and whether the cluster sees itself as partitioned.
type clusterStatusResponse struct {
SelfID string `json:"self_id"`
Role string `json:"role"`
Term uint64 `json:"term"`
LeaderID string `json:"leader_id"`
Voters []string `json:"voters"`
AppliedIndex uint64 `json:"applied_index"`
CommitIndex uint64 `json:"commit_index"`
FSMChecks int `json:"fsm_checks"`
FSMMembers int `json:"fsm_membership"`
FSMConfigVersion uint64 `json:"fsm_config_version"`
FSMOutboxLen int `json:"fsm_outbox_len"`
FSMPartition string `json:"fsm_partition"`
ClusterID string `json:"cluster_id"`
LocalAddr string `json:"local_addr"`
}
// handleClusterStatus serializes the current cluster state for the
// operator. Returns 503 if no cluster is attached; 200 otherwise.
//
// Admin-only: the worker webapp is single-tenant so the session
// middleware (requireSession) is the admin check. Cross-tenant
// protection is not required at this layer.
//
// The `r` parameter is unused but kept so the signature matches
// http.HandlerFunc (the route is registered via requireSession).
func (s *Server) handleClusterStatus(w http.ResponseWriter, _ *http.Request) {
writeNoStore(w)
if s.cluster == nil {
http.Error(w, "cluster not configured", http.StatusServiceUnavailable)
return
}
stats := s.cluster.Stats()
resp := clusterStatusResponse{
SelfID: stats.NodeID,
Role: stats.State,
Term: stats.Term,
LeaderID: stats.Leader,
Voters: stats.Voters,
AppliedIndex: stats.AppliedIndex,
CommitIndex: stats.LastIndex,
FSMChecks: stats.FSMChecks,
FSMMembers: stats.FSMMembers,
FSMConfigVersion: stats.FSMConfigVersion,
FSMOutboxLen: stats.FSMOutboxLen,
FSMPartition: stats.FSMPartition,
ClusterID: s.cluster.ClusterID(),
LocalAddr: s.cluster.LocalAddr(),
}
if resp.Voters == nil {
resp.Voters = []string{}
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(resp); err != nil {
s.deps.Logger.Printf("cluster status encode: %v", err)
}
}
// ErrClusterNotConfigured is returned when a cluster-admin endpoint is
// hit on a server without a cluster attached.
var ErrClusterNotConfigured = errors.New("webapp: cluster not configured")