From 094ce078c9cf0585cfeeb10c3ec28e8ca7b8de2c Mon Sep 17 00:00:00 2001 From: Richa Banker Date: Tue, 18 Jun 2024 20:30:43 -0700 Subject: [PATCH] POC: statusz for apiserver --- .../src/k8s.io/apiserver/pkg/server/config.go | 10 ++ .../k8s.io/component-base/statusz/registry.go | 125 ++++++++++++++++++ .../k8s.io/component-base/statusz/statusz.go | 82 ++++++++++++ .../component-base/statusz/statusz_test.go | 97 ++++++++++++++ 4 files changed, 314 insertions(+) create mode 100644 staging/src/k8s.io/component-base/statusz/registry.go create mode 100644 staging/src/k8s.io/component-base/statusz/statusz.go create mode 100644 staging/src/k8s.io/component-base/statusz/statusz_test.go diff --git a/staging/src/k8s.io/apiserver/pkg/server/config.go b/staging/src/k8s.io/apiserver/pkg/server/config.go index 0502448d6ebfc..1c0d3be9d79a7 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/config.go +++ b/staging/src/k8s.io/apiserver/pkg/server/config.go @@ -75,6 +75,7 @@ import ( "k8s.io/component-base/logs" "k8s.io/component-base/metrics/features" "k8s.io/component-base/metrics/prometheus/slis" + "k8s.io/component-base/statusz" "k8s.io/component-base/tracing" "k8s.io/klog/v2" openapicommon "k8s.io/kube-openapi/pkg/common" @@ -1078,6 +1079,8 @@ func installAPI(s *GenericAPIServer, c *Config) { } if c.EnableMetrics { + statuzOpts := statuszOptions() + statusz.Statusz{}.Install(s.Handler.NonGoRestfulMux, statuzOpts) if c.EnableProfiling { routes.MetricsWithReset{}.Install(s.Handler.NonGoRestfulMux) slis.SLIMetricsWithReset{}.Install(s.Handler.NonGoRestfulMux) @@ -1170,3 +1173,10 @@ func SetHostnameFuncForTests(name string) { return } } + +func statuszOptions() statusz.Options { + return statusz.Options{ + ComponentName: "apiserver", + StartTime: time.Now(), + } +} diff --git a/staging/src/k8s.io/component-base/statusz/registry.go b/staging/src/k8s.io/component-base/statusz/registry.go new file mode 100644 index 0000000000000..f7314e7714862 --- /dev/null +++ b/staging/src/k8s.io/component-base/statusz/registry.go @@ -0,0 +1,125 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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 statusz + +import ( + "context" + "fmt" + "html/template" + "io" + "sync" + "time" +) + +type statuszRegistry struct { + lock sync.Mutex + options Options + sections []section +} + +type section struct { + Title string + Func func(context.Context, io.Writer) error +} + +func Register(opts Options) *statuszRegistry { + registry := &statuszRegistry{ + options: opts, + } + registry.addSection("default", defaultSection) + return registry +} + +func (reg *statuszRegistry) addSection(title string, f func(ctx context.Context, wr io.Writer, opts Options) error) error { + reg.lock.Lock() + defer reg.lock.Unlock() + reg.sections = append(reg.sections, section{ + Title: title, + Func: func(ctx context.Context, wr io.Writer) error { + err := f(ctx, wr, reg.options) + if err != nil { + failErr := template.Must(template.New("").Parse("invalid HTML: {{.}}")).Execute(wr, err) + if failErr != nil { + return fmt.Errorf("go/server: couldn't execute the error template for %q: %v (couldn't get HTML fragment: %v)", title, failErr, err) + } + return err + } + return nil + }, + }) + return nil +} + +func defaultSection(ctx context.Context, wr io.Writer, opts Options) error { + var data struct { + ServerName string + StartTime string + Uptime string + } + + data.ServerName = opts.ComponentName + data.StartTime = opts.StartTime.Format(time.RFC1123) + uptime := int64(time.Since(opts.StartTime).Seconds()) + data.Uptime = fmt.Sprintf("%d hr %02d min %02d sec", + uptime/3600, (uptime/60)%60, uptime%60) + + if err := defaultTmp.Execute(wr, data); err != nil { + return fmt.Errorf("couldn't execute template: %v", err) + } + + return nil +} + +var defaultTmp = template.Must(template.New("").Parse(` + + + + Status for {{.ServerName}} + + + + +

Status for {{.ServerName}}

+ +
+
+ Started: {{.StartTime}}
+ Up {{.Uptime}}
+ + +`)) diff --git a/staging/src/k8s.io/component-base/statusz/statusz.go b/staging/src/k8s.io/component-base/statusz/statusz.go new file mode 100644 index 0000000000000..299c6d6bd8bb2 --- /dev/null +++ b/staging/src/k8s.io/component-base/statusz/statusz.go @@ -0,0 +1,82 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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 statusz + +import ( + "bytes" + "context" + "fmt" + "net/http" + "time" + + "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/klog/v2" +) + +type Statusz struct { + registry *statuszRegistry +} + +type Options struct { + ComponentName string + StartTime time.Time +} + +type mux interface { + Handle(path string, handler http.Handler) +} + +func (f Statusz) Install(m mux, opts Options) { + f.registry = Register(opts) + f.registry.installHandler(m) +} + +func (reg *statuszRegistry) installHandler(m mux) { + reg.lock.Lock() + defer reg.lock.Unlock() + m.Handle("/statusz", + metrics.InstrumentHandlerFunc("GET", + /* group = */ "", + /* version = */ "", + /* resource = */ "", + /* subresource = */ "/statusz", + /* scope = */ "", + /* component = */ "", + /* deprecated */ false, + /* removedRelease */ "", + handleSections(reg.sections))) +} + +// handleSections returns an http.HandlerFunc that serves the provided sections. +func handleSections(sections []section) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var individualCheckOutput bytes.Buffer + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + for _, section := range sections { + err := section.Func(context.Background(), w) + if err != nil { + fmt.Fprintf(&individualCheckOutput, "[-]%s failed: reason withheld\n", section.Title) + klog.V(2).Infof("%s section failed: %v", section.Title, err) + http.Error(w, fmt.Sprintf("%s%s section failed", individualCheckOutput.String(), section.Title), http.StatusInternalServerError) + return + } + fmt.Fprint(w) + } + individualCheckOutput.WriteTo(w) + } +} diff --git a/staging/src/k8s.io/component-base/statusz/statusz_test.go b/staging/src/k8s.io/component-base/statusz/statusz_test.go new file mode 100644 index 0000000000000..f1b1125562ed1 --- /dev/null +++ b/staging/src/k8s.io/component-base/statusz/statusz_test.go @@ -0,0 +1,97 @@ +/* +Copyright 2024 The Kubernetes Authors. +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 statusz + +import ( + "bytes" + "fmt" + "html/template" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestStatusz(t *testing.T) { + tests := []struct { + name string + opts Options + expectedTemplate *template.Template + expectedStatus int + }{ + { + name: "default", + opts: Options{ + ComponentName: "test-component", + StartTime: time.Now(), + }, + expectedTemplate: defaultTmp, + expectedStatus: http.StatusOK, + }, + } + + for i, test := range tests { + mux := http.NewServeMux() + Statusz{}.Install(mux, test.opts) + + path := "/statusz" + req, err := http.NewRequest("GET", fmt.Sprintf("http://example.com%s", path), nil) + if err != nil { + t.Fatalf("case[%d] Unexpected error: %v", i, err) + } + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + if w.Code != test.expectedStatus { + t.Errorf("case[%d] Expected: %v, got: %v", i, test.expectedStatus, w.Code) + } + + c := w.Header().Get("Content-Type") + if c != "text/plain; charset=utf-8" { + t.Errorf("case[%d] Expected: %v, got: %v", i, "text/plain", c) + } + + data := prepareData(test.opts) + want := new(bytes.Buffer) + err = test.expectedTemplate.Execute(want, data) + if err != nil { + t.Fatalf("unexpected error while executing expected template: %v", err) + } + + if w.Body.String() != want.String() { + t.Errorf("case[%d] Expected:\n%v\ngot:\n%v\n", i, test.expectedTemplate, w.Body.String()) + } + } + +} + +func prepareData(opts Options) struct { + ServerName string + StartTime string + Uptime string +} { + var data struct { + ServerName string + StartTime string + Uptime string + } + + data.ServerName = opts.ComponentName + data.StartTime = opts.StartTime.Format(time.RFC1123) + uptime := int64(time.Since(opts.StartTime).Seconds()) + data.Uptime = fmt.Sprintf("%d hr %02d min %02d sec", + uptime/3600, (uptime/60)%60, uptime%60) + + return data +}