diff --git a/cmd/inventory/main.go b/cmd/inventory/main.go index dd8e6df..0d2ec8a 100644 --- a/cmd/inventory/main.go +++ b/cmd/inventory/main.go @@ -92,6 +92,7 @@ func main() { }) mux.HandleFunc("GET /admin/chaos", c.HandleGetConfig) mux.HandleFunc("PUT /admin/chaos", c.HandleSetConfig) + mux.HandleFunc("POST /admin/chaos/clear-disk", c.HandleClearDisk) mux.Handle("/", svc.Mux) handler := c.Middleware(middleware.Wrap(mux, cfg.ServiceName)) diff --git a/internal/chaos/chaos.go b/internal/chaos/chaos.go index 9a924ba..3643e96 100644 --- a/internal/chaos/chaos.go +++ b/internal/chaos/chaos.go @@ -125,3 +125,14 @@ func (c *Chaos) HandleSetConfig(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(c.GetConfig()) } + +func (c *Chaos) HandleClearDisk(w http.ResponseWriter, r *http.Request) { + if err := c.DiskFiller.Clear(); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "cleared"}) +} diff --git a/internal/chaos/disk_filler.go b/internal/chaos/disk_filler.go index e165538..1c85654 100644 --- a/internal/chaos/disk_filler.go +++ b/internal/chaos/disk_filler.go @@ -54,6 +54,17 @@ func (d *DiskFiller) GetConfig() (enabled bool, path string, rateMB int) { return d.enabled, d.path, d.rateMB } +func (d *DiskFiller) Clear() error { + d.mu.Lock() + defer d.mu.Unlock() + if d.path == "" { + return nil + } + fillDir := filepath.Join(d.path, "chaos-fill") + slog.Info("chaos: clearing disk fill data", "path", fillDir) + return os.RemoveAll(fillDir) +} + func (d *DiskFiller) startLocked() { if d.parentCtx == nil || !d.enabled || d.rateMB <= 0 || d.path == "" { return diff --git a/services/dashboard/handler.go b/services/dashboard/handler.go index 8e008e6..63ae83a 100644 --- a/services/dashboard/handler.go +++ b/services/dashboard/handler.go @@ -80,6 +80,32 @@ func (h *Handler) SetServiceChaos(w http.ResponseWriter, r *http.Request) { io.Copy(w, resp.Body) } +func (h *Handler) ClearServiceDisk(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + svc := h.findService(name) + if svc == nil { + http.Error(w, `{"error":"service not found"}`, http.StatusNotFound) + return + } + + req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, svc.URL+"/admin/chaos/clear-disk", nil) + if err != nil { + http.Error(w, `{"error":"failed to create request"}`, http.StatusInternalServerError) + return + } + + resp, err := h.httpClient.Do(req) + if err != nil { + http.Error(w, `{"error":"service unreachable: `+err.Error()+`"}`, http.StatusBadGateway) + return + } + defer resp.Body.Close() + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) +} + func (h *Handler) findService(name string) *ServiceInfo { for i := range h.services { if h.services[i].Name == name { diff --git a/services/dashboard/routes.go b/services/dashboard/routes.go index ab1f065..d9eb4a3 100644 --- a/services/dashboard/routes.go +++ b/services/dashboard/routes.go @@ -6,5 +6,6 @@ func RegisterRoutes(mux *http.ServeMux, h *Handler, staticFS http.Handler) { mux.HandleFunc("GET /api/services", h.ListServices) mux.HandleFunc("GET /api/services/{name}/chaos", h.GetServiceChaos) mux.HandleFunc("PUT /api/services/{name}/chaos", h.SetServiceChaos) + mux.HandleFunc("POST /api/services/{name}/chaos/clear-disk", h.ClearServiceDisk) mux.Handle("/", staticFS) } diff --git a/services/dashboard/static/index.html b/services/dashboard/static/index.html index 1720cd8..7d1aa34 100644 --- a/services/dashboard/static/index.html +++ b/services/dashboard/static/index.html @@ -209,11 +209,11 @@

Chaos Dashboard

rate_per_sec: parseInt(card.querySelector('#log-rate-' + name).value) || 0, pattern: card.querySelector('#log-pattern-' + name).value }, - disk_fill: { + disk_fill: name === 'inventory' ? { enabled: card.querySelector('#disk-enabled-' + name).checked, path: card.querySelector('#disk-path-' + name).value || '/data', rate_mb: parseInt(card.querySelector('#disk-rate-' + name).value) || 1 - } + } : { enabled: false, path: '/data', rate_mb: 1 } }; } @@ -304,7 +304,7 @@

Log Volume

-
+ ${name === 'inventory' ? `

Disk Fill

@@ -321,7 +321,10 @@

Disk Fill

-
+
+ +
+
` : ''}
@@ -355,10 +358,26 @@

Disk Fill

list.insertAdjacentHTML('beforeend', latencyRouteRow('', '')); } + async function clearDisk(name) { + try { + const resp = await fetch(`/api/services/${name}/chaos/clear-disk`, { method: 'POST' }); + if (!resp.ok) { + const body = await resp.json(); + throw new Error(body.error || `HTTP ${resp.status}`); + } + showToast(`${name}: disk data cleared`, 'success'); + } catch (e) { + showToast(`${name}: ${e.message}`, 'error'); + } + } + async function applyPreset(presetName) { - const preset = PRESETS[presetName](); - const promises = services.map(svc => - fetch(`/api/services/${svc.name}/chaos`, { + const promises = services.map(svc => { + const preset = PRESETS[presetName](); + if (svc.name !== 'inventory') { + preset.disk_fill = { enabled: false, path: '/data', rate_mb: 1 }; + } + return fetch(`/api/services/${svc.name}/chaos`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(preset) @@ -367,8 +386,8 @@

Disk Fill

configs[svc.name] = await resp.json(); renderService(svc.name); } - }).catch(() => {}) - ); + }).catch(() => {}); + }); await Promise.all(promises); showToast(`Preset "${presetName}" applied to all services`, 'success'); }