diff --git a/cmd/nodeproblemdetector/node_problem_detector.go b/cmd/nodeproblemdetector/node_problem_detector.go index 41648b758..43b706fac 100644 --- a/cmd/nodeproblemdetector/node_problem_detector.go +++ b/cmd/nodeproblemdetector/node_problem_detector.go @@ -25,6 +25,7 @@ import ( _ "k8s.io/node-problem-detector/cmd/nodeproblemdetector/problemdaemonplugins" "k8s.io/node-problem-detector/cmd/options" "k8s.io/node-problem-detector/pkg/exporters" + "k8s.io/node-problem-detector/pkg/exporters/httpexporter" "k8s.io/node-problem-detector/pkg/exporters/k8sexporter" "k8s.io/node-problem-detector/pkg/exporters/prometheusexporter" "k8s.io/node-problem-detector/pkg/problemdaemon" @@ -59,6 +60,10 @@ func npdMain(ctx context.Context, npdo *options.NodeProblemDetectorOptions) erro defaultExporters = append(defaultExporters, pe) klog.Info("Prometheus exporter started.") } + if he := httpexporter.NewExporterOrDie(npdo); he != nil { + defaultExporters = append(defaultExporters, he) + klog.Info("HTTP exporter started.") + } plugableExporters := exporters.NewExporters() diff --git a/pkg/exporters/httpexporter/http_exporter.go b/pkg/exporters/httpexporter/http_exporter.go new file mode 100644 index 000000000..7d43b0dee --- /dev/null +++ b/pkg/exporters/httpexporter/http_exporter.go @@ -0,0 +1,98 @@ +/* +Copyright 2026 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package httpexporter provides a standalone HTTP server exposing /healthz, +// /conditions and /debug/pprof without requiring a Kubernetes API server. +package httpexporter + +import ( + "net" + "net/http" + "net/http/pprof" + "strconv" + "sync" + + "k8s.io/klog/v2" + + "k8s.io/node-problem-detector/cmd/options" + "k8s.io/node-problem-detector/pkg/types" + "k8s.io/node-problem-detector/pkg/util" +) + +type httpExporter struct { + mu sync.RWMutex + conditions map[string]types.Condition +} + +// NewExporterOrDie creates the standalone HTTP exporter and starts the server. +// Returns nil if --port is 0 (disabled). Panics on bind errors. +func NewExporterOrDie(npdo *options.NodeProblemDetectorOptions) types.Exporter { + if npdo.ServerPort <= 0 { + return nil + } + + he := &httpExporter{ + conditions: make(map[string]types.Condition), + } + + mux := http.NewServeMux() + + mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte("ok")); err != nil { + klog.Errorf("Failed to write response: %v", err) + } + }) + + mux.HandleFunc("/conditions", func(w http.ResponseWriter, r *http.Request) { + util.ReturnHTTPJson(w, he.getConditions()) + }) + + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + addr := net.JoinHostPort(npdo.ServerAddress, strconv.Itoa(npdo.ServerPort)) + go func() { + if err := http.ListenAndServe(addr, mux); err != nil { + klog.Fatalf("Failed to start HTTP server: %v", err) + } + }() + + klog.Infof("HTTP exporter started on %s", addr) + return he +} + +// ExportProblems updates the in-memory condition store from the incoming status. +func (he *httpExporter) ExportProblems(status *types.Status) { + he.mu.Lock() + defer he.mu.Unlock() + for _, cdt := range status.Conditions { + he.conditions[cdt.Type] = cdt + } +} + +func (he *httpExporter) getConditions() []types.Condition { + he.mu.RLock() + defer he.mu.RUnlock() + conditions := make([]types.Condition, 0, len(he.conditions)) + for _, c := range he.conditions { + conditions = append(conditions, c) + } + return conditions +} diff --git a/pkg/exporters/k8sexporter/k8s_exporter.go b/pkg/exporters/k8sexporter/k8s_exporter.go index d47e04ff0..f0551a28e 100644 --- a/pkg/exporters/k8sexporter/k8s_exporter.go +++ b/pkg/exporters/k8sexporter/k8s_exporter.go @@ -18,10 +18,6 @@ package k8sexporter import ( "context" - "net" - "net/http" - "net/http/pprof" - "strconv" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/klog/v2" @@ -65,7 +61,6 @@ func NewExporterOrDie(ctx context.Context, npdo *options.NodeProblemDetectorOpti updateConditions: npdo.K8sExporterUpdateNodeConditions, } - ke.startHTTPReporting(npdo) ke.conditionManager.Start(ctx) return &ke @@ -84,42 +79,6 @@ func (ke *k8sExporter) ExportProblems(status *types.Status) { } } -func (ke *k8sExporter) startHTTPReporting(npdo *options.NodeProblemDetectorOptions) { - if npdo.ServerPort <= 0 { - return - } - mux := http.NewServeMux() - - // Add healthz http request handler. Always return ok now, add more health check - // logic in the future. - mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte("ok")); err != nil { - klog.Errorf("Failed to write response: %v", err) - } - }) - - // Add the handler to serve condition http request. - mux.HandleFunc("/conditions", func(w http.ResponseWriter, r *http.Request) { - util.ReturnHTTPJson(w, ke.conditionManager.GetConditions()) - }) - - // register pprof - mux.HandleFunc("/debug/pprof/", pprof.Index) - mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - mux.HandleFunc("/debug/pprof/profile", pprof.Profile) - mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - mux.HandleFunc("/debug/pprof/trace", pprof.Trace) - - addr := net.JoinHostPort(npdo.ServerAddress, strconv.Itoa(npdo.ServerPort)) - go func() { - err := http.ListenAndServe(addr, mux) - if err != nil { - klog.Fatalf("Failed to start server: %v", err) - } - }() -} - func waitForAPIServerReadyWithTimeout(ctx context.Context, c problemclient.Client, npdo *options.NodeProblemDetectorOptions) error { return wait.PollUntilContextTimeout(ctx, npdo.APIServerWaitInterval, npdo.APIServerWaitTimeout, true, func(ctx context.Context) (done bool, err error) { // If NPD can get the node object from kube-apiserver, the server is