From a341a4ee245ae35ef06e4db929fc97dc530759df Mon Sep 17 00:00:00 2001 From: Mark Johnston Date: Tue, 26 Aug 2025 14:23:18 +0000 Subject: [PATCH] compat: Add a stub container/stats handler for FreeBSD The container/stats endpoint was unimplemented. Copy over the structure from the Linux endpoint handler, albeit with most of the stats unpopulated. This is similar to how "podman stats" returns very little info on FreeBSD. On the other hand, some orchestration tools will query the stats endpoint to check liveness of a container, so it's useful to implement it even though some fields are unimplemented for now. Signed-off-by: Mark Johnston --- .../compat/containers_stats_freebsd.go | 123 +++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/pkg/api/handlers/compat/containers_stats_freebsd.go b/pkg/api/handlers/compat/containers_stats_freebsd.go index ad48739fb9..02863af005 100644 --- a/pkg/api/handlers/compat/containers_stats_freebsd.go +++ b/pkg/api/handlers/compat/containers_stats_freebsd.go @@ -3,15 +3,136 @@ package compat import ( + "encoding/json" "fmt" "net/http" "time" + "github.com/containers/podman/v5/libpod" + "github.com/containers/podman/v5/libpod/define" "github.com/containers/podman/v5/pkg/api/handlers/utils" + api "github.com/containers/podman/v5/pkg/api/types" + "github.com/docker/docker/api/types/container" + "github.com/sirupsen/logrus" ) const DefaultStatsPeriod = 5 * time.Second func StatsContainer(w http.ResponseWriter, r *http.Request) { - utils.Error(w, http.StatusBadRequest, fmt.Errorf("compat.StatsContainer not supported on FreeBSD")) + runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) + decoder := utils.GetDecoder(r) + + query := struct { + Stream bool `schema:"stream"` + OneShot bool `schema:"one-shot"` // added schema for one shot + }{ + Stream: true, + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) + return + } + if query.Stream && query.OneShot { // mismatch. one-shot can only be passed with stream=false + utils.Error(w, http.StatusBadRequest, define.ErrInvalidArg) + return + } + + name := utils.GetName(r) + ctnr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + stats, err := ctnr.GetContainerStats(nil) + if err != nil { + utils.InternalServerError(w, fmt.Errorf("failed to obtain Container %s stats: %w", name, err)) + return + } + + coder := json.NewEncoder(w) + // Write header and content type. + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + + // Set up JSON encoder for streaming. + coder.SetEscapeHTML(true) + var preRead time.Time + var preCPUStats CPUStats + if query.Stream { + preRead = time.Now() + preCPUStats = CPUStats{ + CPUUsage: container.CPUUsage{ + TotalUsage: stats.CPUNano, + }, + CPU: stats.CPU, + OnlineCPUs: 0, + ThrottlingData: container.ThrottlingData{}, + } + } + +streamLabel: // A label to flatten the scope + select { + case <-r.Context().Done(): + logrus.Debugf("Client connection (container stats) cancelled") + + default: + stats, err = ctnr.GetContainerStats(stats) + if err != nil { + logrus.Errorf("Unable to get container stats: %v", err) + return + } + s := StatsJSON{ + Stats: Stats{ + Read: time.Now(), + PreRead: preRead, + CPUStats: CPUStats{ + CPUUsage: container.CPUUsage{ + TotalUsage: stats.CPUNano, + }, + CPU: stats.CPU, + OnlineCPUs: 0, + ThrottlingData: container.ThrottlingData{}, + }, + PreCPUStats: preCPUStats, + MemoryStats: container.MemoryStats{}, + }, + Name: stats.Name, + ID: stats.ContainerID, + } + + var jsonOut interface{} + if utils.IsLibpodRequest(r) { + jsonOut = s + } else { + jsonOut = DockerStatsJSON(s) + } + + if err := coder.Encode(jsonOut); err != nil { + logrus.Errorf("Unable to encode stats: %v", err) + return + } + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + + if !query.Stream || query.OneShot { + return + } + + preRead = s.Read + bits, err := json.Marshal(s.CPUStats) + if err != nil { + logrus.Errorf("Unable to marshal cpu stats: %q", err) + } + if err := json.Unmarshal(bits, &preCPUStats); err != nil { + logrus.Errorf("Unable to unmarshal previous stats: %q", err) + } + + time.Sleep(DefaultStatsPeriod) + goto streamLabel + } }