add statusz implementation and enablement in apiserver

This commit is contained in:
Richa Banker 2024-06-18 20:30:43 -07:00
parent 847be85000
commit 8bf6eecedf
17 changed files with 792 additions and 13 deletions

View File

@ -36,6 +36,8 @@ import (
utilfeature "k8s.io/apiserver/pkg/util/feature"
clientgoinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/statusz"
"k8s.io/component-helpers/apimachinery/lease"
"k8s.io/klog/v2"
"k8s.io/utils/clock"
@ -151,6 +153,10 @@ func (c completedConfig) New(name string, delegationTarget genericapiserver.Dele
return nil, fmt.Errorf("failed to get listener address: %w", err)
}
if utilfeature.DefaultFeatureGate.Enabled(zpagesfeatures.ComponentStatusz) {
statusz.Install(s.GenericAPIServer.Handler.NonGoRestfulMux, name, statusz.NewRegistry())
}
if utilfeature.DefaultFeatureGate.Enabled(apiserverfeatures.CoordinatedLeaderElection) {
leaseInformer := s.VersionedInformers.Coordination().V1().Leases()
lcInformer := s.VersionedInformers.Coordination().V1alpha1().LeaseCandidates()

View File

@ -21,6 +21,7 @@ import (
utilfeature "k8s.io/apiserver/pkg/util/feature"
clientfeatures "k8s.io/client-go/features"
"k8s.io/component-base/featuregate"
zpagesfeatures "k8s.io/component-base/zpages/features"
)
const (
@ -840,6 +841,7 @@ const (
func init() {
runtime.Must(utilfeature.DefaultMutableFeatureGate.Add(defaultKubernetesFeatureGates))
runtime.Must(utilfeature.DefaultMutableFeatureGate.AddVersioned(defaultVersionedKubernetesFeatureGates))
runtime.Must(zpagesfeatures.AddFeatureGates(utilfeature.DefaultMutableFeatureGate))
// Register all client-go features with kube's feature gate instance and make all client-go
// feature checks use kube's instance. The effect is that for kube binaries, client-go

View File

@ -21,6 +21,7 @@ import (
"k8s.io/apimachinery/pkg/util/version"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/component-base/featuregate"
zpagesfeatures "k8s.io/component-base/zpages/features"
kcmfeatures "k8s.io/controller-manager/pkg/features"
)
@ -800,4 +801,8 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
WindowsHostNetwork: {
{Version: version.MustParse("1.26"), Default: true, PreRelease: featuregate.Alpha},
},
zpagesfeatures.ComponentStatusz: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
}

View File

@ -25,6 +25,7 @@ import (
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"
utilfeature "k8s.io/apiserver/pkg/util/feature"
zpagesfeatures "k8s.io/component-base/zpages/features"
rbacv1helpers "k8s.io/kubernetes/pkg/apis/rbac/v1"
"k8s.io/kubernetes/pkg/features"
@ -194,6 +195,18 @@ func NodeRules() []rbacv1.PolicyRule {
// ClusterRoles returns the cluster roles to bootstrap an API server with
func ClusterRoles() []rbacv1.ClusterRole {
monitoringRules := []rbacv1.PolicyRule{
rbacv1helpers.NewRule("get").URLs(
"/metrics", "/metrics/slis",
"/livez", "/readyz", "/healthz",
"/livez/*", "/readyz/*", "/healthz/*",
).RuleOrDie(),
}
if utilfeature.DefaultFeatureGate.Enabled(zpagesfeatures.ComponentStatusz) {
monitoringRules = append(monitoringRules, rbacv1helpers.NewRule("get").URLs("/statusz").RuleOrDie())
}
roles := []rbacv1.ClusterRole{
{
// a "root" role which can do absolutely anything
@ -223,13 +236,7 @@ func ClusterRoles() []rbacv1.ClusterRole {
// The splatted health check endpoints allow read access to individual health check
// endpoints which may contain more sensitive cluster information information
ObjectMeta: metav1.ObjectMeta{Name: "system:monitoring"},
Rules: []rbacv1.PolicyRule{
rbacv1helpers.NewRule("get").URLs(
"/metrics", "/metrics/slis",
"/livez", "/readyz", "/healthz",
"/livez/*", "/readyz/*", "/healthz/*",
).RuleOrDie(),
},
Rules: monitoringRules,
},
}

View File

@ -979,7 +979,7 @@ func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*G
s.listedPathProvider = routes.ListedPathProviders{s.listedPathProvider, delegationTarget}
installAPI(s, c.Config)
installAPI(name, s, c.Config)
// use the UnprotectedHandler from the delegation target to ensure that we don't attempt to double authenticator, authorize,
// or some other part of the filter chain in delegation cases.
@ -1076,7 +1076,7 @@ func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
return handler
}
func installAPI(s *GenericAPIServer, c *Config) {
func installAPI(name string, s *GenericAPIServer, c *Config) {
if c.EnableIndex {
routes.Index{}.Install(s.listedPathProvider, s.Handler.NonGoRestfulMux)
}

View File

@ -12,6 +12,7 @@ require (
github.com/go-logr/zapr v1.3.0
github.com/google/go-cmp v0.6.0
github.com/moby/term v0.5.0
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/client_model v0.6.1
github.com/prometheus/common v0.55.0
@ -59,7 +60,6 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect

View File

@ -35,7 +35,7 @@ var processStartTime = NewGaugeVec(
// a prometheus registry. This metric needs to be included to ensure counter
// data fidelity.
func RegisterProcessStartTime(registrationFunc func(Registerable) error) error {
start, err := getProcessStart()
start, err := GetProcessStart()
if err != nil {
klog.Errorf("Could not get process start time, %v", err)
start = float64(time.Now().Unix())

View File

@ -25,7 +25,7 @@ import (
"github.com/prometheus/procfs"
)
func getProcessStart() (float64, error) {
func GetProcessStart() (float64, error) {
pid := os.Getpid()
p, err := procfs.NewProc(pid)
if err != nil {

View File

@ -23,7 +23,7 @@ import (
"golang.org/x/sys/windows"
)
func getProcessStart() (float64, error) {
func GetProcessStart() (float64, error) {
processHandle := windows.CurrentProcess()
var creationTime, exitTime, kernelTime, userTime windows.Filetime

View File

@ -0,0 +1,22 @@
/*
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 features contains a separate feature set specifically designed for
// managing zpages related features. These feature gates control the
// availability and behavior of various zpages within Kubernetes components.
// New zpages added to Kubernetes components should utilize this feature set
// to ensure proper management of their availability.
package features

View File

@ -0,0 +1,45 @@
/*
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 features
import (
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-base/featuregate"
)
const (
// owner: @richabanker
// kep: https://kep.k8s.io/4827
// alpha: v1.32
//
// Enables /statusz endpoint for a component making it accessible to
// users with the system:monitoring cluster role.
ComponentStatusz featuregate.Feature = "ComponentStatusz"
)
func featureGates() map[featuregate.Feature]featuregate.VersionedSpecs {
return map[featuregate.Feature]featuregate.VersionedSpecs{
ComponentStatusz: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
}
}
// AddFeatureGates adds all feature gates used by this package.
func AddFeatureGates(mutableFeatureGate featuregate.MutableVersionedFeatureGate) error {
return mutableFeatureGate.AddVersioned(featureGates())
}

View File

@ -0,0 +1,68 @@
/*
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 (
"time"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-base/featuregate"
"k8s.io/klog/v2"
compbasemetrics "k8s.io/component-base/metrics"
utilversion "k8s.io/component-base/version"
)
type statuszRegistry interface {
processStartTime() time.Time
goVersion() string
binaryVersion() *version.Version
emulationVersion() *version.Version
}
type registry struct{}
func (registry) processStartTime() time.Time {
start, err := compbasemetrics.GetProcessStart()
if err != nil {
klog.Errorf("Could not get process start time, %v", err)
}
return time.Unix(int64(start), 0)
}
func (registry) goVersion() string {
return utilversion.Get().GoVersion
}
func (registry) binaryVersion() *version.Version {
effectiveVer := featuregate.DefaultComponentGlobalsRegistry.EffectiveVersionFor(featuregate.DefaultKubeComponent)
if effectiveVer != nil {
return effectiveVer.BinaryVersion()
}
return utilversion.DefaultKubeEffectiveVersion().BinaryVersion()
}
func (registry) emulationVersion() *version.Version {
effectiveVer := featuregate.DefaultComponentGlobalsRegistry.EffectiveVersionFor(featuregate.DefaultKubeComponent)
if effectiveVer != nil {
return effectiveVer.EmulationVersion()
}
return nil
}

View File

@ -0,0 +1,105 @@
/*
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 (
"testing"
"github.com/stretchr/testify/assert"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-base/featuregate"
utilversion "k8s.io/component-base/version"
)
func TestBinaryVersion(t *testing.T) {
componentGlobalsRegistry := featuregate.DefaultComponentGlobalsRegistry
tests := []struct {
name string
setFakeEffectiveVersion bool
fakeVersion string
wantBinaryVersion *version.Version
}{
{
name: "binaryVersion with effective version",
wantBinaryVersion: version.MustParseSemantic("v1.2.3"),
setFakeEffectiveVersion: true,
fakeVersion: "1.2.3",
},
{
name: "binaryVersion without effective version",
wantBinaryVersion: utilversion.DefaultKubeEffectiveVersion().BinaryVersion(),
},
}
for _, tt := range tests {
componentGlobalsRegistry.Reset()
t.Run(tt.name, func(t *testing.T) {
if tt.setFakeEffectiveVersion {
verKube := utilversion.NewEffectiveVersion(tt.fakeVersion)
fg := featuregate.NewVersionedFeatureGate(version.MustParse(tt.fakeVersion))
utilruntime.Must(componentGlobalsRegistry.Register(featuregate.DefaultKubeComponent, verKube, fg))
}
registry := &registry{}
got := registry.binaryVersion()
assert.Equal(t, tt.wantBinaryVersion, got)
})
}
}
func TestEmulationVersion(t *testing.T) {
componentGlobalsRegistry := featuregate.DefaultComponentGlobalsRegistry
tests := []struct {
name string
setFakeEffectiveVersion bool
fakeEmulVer string
wantEmul *version.Version
}{
{
name: "emulationVersion with effective version",
fakeEmulVer: "2.3.4",
setFakeEffectiveVersion: true,
wantEmul: version.MustParseSemantic("2.3.4"),
},
{
name: "emulationVersion without effective version",
wantEmul: nil,
},
}
for _, tt := range tests {
componentGlobalsRegistry.Reset()
t.Run(tt.name, func(t *testing.T) {
if tt.setFakeEffectiveVersion {
verKube := utilversion.NewEffectiveVersion("0.0.0")
verKube.SetEmulationVersion(version.MustParse(tt.fakeEmulVer))
fg := featuregate.NewVersionedFeatureGate(version.MustParse(tt.fakeEmulVer))
utilruntime.Must(componentGlobalsRegistry.Register(featuregate.DefaultKubeComponent, verKube, fg))
}
registry := &registry{}
got := registry.emulationVersion()
if tt.wantEmul != nil && got != nil {
assert.Equal(t, tt.wantEmul.Major(), got.Major())
assert.Equal(t, tt.wantEmul.Minor(), got.Minor())
} else {
assert.Equal(t, tt.wantEmul, got)
}
})
}
}

View File

@ -0,0 +1,164 @@
/*
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"
"math/rand"
"net/http"
"strings"
"time"
"github.com/munnerz/goautoneg"
"k8s.io/klog/v2"
)
var (
delimiters = []string{":", ": ", "=", " "}
errUnsupportedMediaType = fmt.Errorf("media type not acceptable, must be: text/plain")
)
const (
headerFmt = `
%s statusz
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.
`
dataTemplate = `
Started{{.Delim}} {{.StartTime}}
Up{{.Delim}} {{.Uptime}}
Go version{{.Delim}} {{.GoVersion}}
Binary version{{.Delim}} {{.BinaryVersion}}
{{if .EmulationVersion}}Emulation version{{.Delim}} {{.EmulationVersion}}{{end}}
`
)
type contentFields struct {
Delim string
StartTime string
Uptime string
GoVersion string
BinaryVersion string
EmulationVersion string
}
type mux interface {
Handle(path string, handler http.Handler)
}
func NewRegistry() statuszRegistry {
return registry{}
}
func Install(m mux, componentName string, reg statuszRegistry) {
dataTmpl, err := initializeTemplates()
if err != nil {
klog.Errorf("error while parsing gotemplates: %v", err)
return
}
m.Handle("/statusz", handleStatusz(componentName, dataTmpl, reg))
}
func initializeTemplates() (*template.Template, error) {
d := template.New("data")
dataTmpl, err := d.Parse(dataTemplate)
if err != nil {
return nil, err
}
return dataTmpl, nil
}
func handleStatusz(componentName string, dataTmpl *template.Template, reg statuszRegistry) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !acceptableMediaType(r) {
http.Error(w, errUnsupportedMediaType.Error(), http.StatusNotAcceptable)
return
}
fmt.Fprintf(w, headerFmt, componentName)
data, err := populateStatuszData(dataTmpl, reg)
if err != nil {
klog.Errorf("error while populating statusz data: %v", err)
http.Error(w, "error while populating statusz data", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, data)
}
}
// TODO(richabanker) : Move this to a common place to be reused for all zpages.
func acceptableMediaType(r *http.Request) bool {
accepts := goautoneg.ParseAccept(r.Header.Get("Accept"))
for _, accept := range accepts {
if !mediaTypeMatches(accept) {
continue
}
if len(accept.Params) == 0 {
return true
}
if len(accept.Params) == 1 {
if charset, ok := accept.Params["charset"]; ok && strings.EqualFold(charset, "utf-8") {
return true
}
}
}
return false
}
func mediaTypeMatches(a goautoneg.Accept) bool {
return (a.Type == "text" || a.Type == "*") &&
(a.SubType == "plain" || a.SubType == "*")
}
func populateStatuszData(tmpl *template.Template, reg statuszRegistry) (string, error) {
if tmpl == nil {
return "", fmt.Errorf("received nil template")
}
randomIndex := rand.Intn(len(delimiters))
data := contentFields{
Delim: delimiters[randomIndex],
StartTime: reg.processStartTime().Format(time.UnixDate),
Uptime: uptime(reg.processStartTime()),
GoVersion: reg.goVersion(),
BinaryVersion: reg.binaryVersion().String(),
}
if reg.emulationVersion() != nil {
data.EmulationVersion = reg.emulationVersion().String()
}
var tpl bytes.Buffer
err := tmpl.Execute(&tpl, data)
if err != nil {
return "", fmt.Errorf("error executing statusz template: %w", err)
}
return tpl.String(), nil
}
func uptime(t time.Time) string {
upSince := int64(time.Since(t).Seconds())
return fmt.Sprintf("%d hr %02d min %02d sec",
upSince/3600, (upSince/60)%60, upSince%60)
}

View File

@ -0,0 +1,237 @@
/*
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 (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/util/version"
)
const wantTmpl = `
%s statusz
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.
Started: %v
Up: %s
Go version: %s
Binary version: %v
Emulation version: %v
`
const wantTmplWithoutEmulation = `
%s statusz
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.
Started: %v
Up: %s
Go version: %s
Binary version: %v
`
func TestStatusz(t *testing.T) {
delimiters = []string{":"}
fakeStartTime := time.Now()
fakeUptime := uptime(fakeStartTime)
fakeGoVersion := "1.21"
fakeBvStr := "1.31"
fakeEvStr := "1.30"
fakeBinaryVersion := parseVersion(t, fakeBvStr)
fakeEmulationVersion := parseVersion(t, fakeEvStr)
tests := []struct {
name string
componentName string
reqHeader string
registry fakeRegistry
wantStatusCode int
wantBody string
}{
{
name: "invalid header",
reqHeader: "some header",
wantStatusCode: http.StatusNotAcceptable,
},
{
name: "valid request",
componentName: "test-server",
reqHeader: "text/plain; charset=utf-8",
registry: fakeRegistry{
startTime: fakeStartTime,
goVer: fakeGoVersion,
binaryVer: fakeBinaryVersion,
emulationVer: fakeEmulationVersion,
},
wantStatusCode: http.StatusOK,
wantBody: fmt.Sprintf(
wantTmpl,
"test-server",
fakeStartTime.Format(time.UnixDate),
fakeUptime,
fakeGoVersion,
fakeBinaryVersion,
fakeEmulationVersion,
),
},
{
name: "missing emulation version",
componentName: "test-server",
reqHeader: "text/plain; charset=utf-8",
registry: fakeRegistry{
startTime: fakeStartTime,
goVer: fakeGoVersion,
binaryVer: fakeBinaryVersion,
emulationVer: nil,
},
wantStatusCode: http.StatusOK,
wantBody: fmt.Sprintf(
wantTmplWithoutEmulation,
"test-server",
fakeStartTime.Format(time.UnixDate),
fakeUptime,
fakeGoVersion,
fakeBinaryVersion,
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mux := http.NewServeMux()
Install(mux, tt.componentName, tt.registry)
path := "/statusz"
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", path), nil)
if err != nil {
t.Fatalf("unexpected error while creating request: %v", err)
}
req.Header.Set("Accept", "text/plain; charset=utf-8")
if tt.reqHeader != "" {
req.Header.Set("Accept", tt.reqHeader)
}
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != tt.wantStatusCode {
t.Fatalf("want status code: %v, got: %v", tt.wantStatusCode, w.Code)
}
if tt.wantStatusCode == http.StatusOK {
c := w.Header().Get("Content-Type")
if c != "text/plain; charset=utf-8" {
t.Fatalf("want header: %v, got: %v", "text/plain", c)
}
if diff := cmp.Diff(tt.wantBody, string(w.Body.String())); diff != "" {
t.Errorf("Unexpected diff on response (-want,+got):\n%s", diff)
}
}
})
}
}
func TestAcceptableMediaTypes(t *testing.T) {
tests := []struct {
name string
reqHeader string
want bool
}{
{
name: "valid text/plain header",
reqHeader: "text/plain",
want: true,
},
{
name: "valid text/* header",
reqHeader: "text/*",
want: true,
},
{
name: "valid */plain header",
reqHeader: "*/plain",
want: true,
},
{
name: "valid accept args",
reqHeader: "text/plain; charset=utf-8",
want: true,
},
{
name: "invalid text/foo header",
reqHeader: "text/foo",
want: false,
},
{
name: "invalid text/plain params",
reqHeader: "text/plain; foo=bar",
want: false,
},
}
for _, tt := range tests {
req, err := http.NewRequest(http.MethodGet, "http://example.com/statusz", nil)
if err != nil {
t.Fatalf("Unexpected error while creating request: %v", err)
}
req.Header.Set("Accept", tt.reqHeader)
got := acceptableMediaType(req)
if got != tt.want {
t.Errorf("Unexpected response from acceptableMediaType(), want %v, got = %v", tt.want, got)
}
}
}
func parseVersion(t *testing.T, v string) *version.Version {
parsed, err := version.ParseMajorMinor(v)
if err != nil {
t.Fatalf("error parsing binary version: %s", v)
}
return parsed
}
type fakeRegistry struct {
startTime time.Time
goVer string
binaryVer *version.Version
emulationVer *version.Version
}
func (f fakeRegistry) processStartTime() time.Time {
return f.startTime
}
func (f fakeRegistry) goVersion() string {
return f.goVer
}
func (f fakeRegistry) binaryVersion() *version.Version {
return f.binaryVer
}
func (f fakeRegistry) emulationVersion() *version.Version {
return f.emulationVer
}

View File

@ -208,6 +208,12 @@
lockToDefault: true
preRelease: GA
version: "1.32"
- name: ComponentStatusz
versionedSpecs:
- default: false
lockToDefault: false
preRelease: Alpha
version: "1.32"
- name: ConcurrentWatchObjectDecode
versionedSpecs:
- default: false

View File

@ -24,6 +24,7 @@ import (
"net/http/httputil"
gopath "path"
"reflect"
"regexp"
"strings"
"testing"
@ -43,10 +44,13 @@ import (
unionauthz "k8s.io/apiserver/pkg/authorization/union"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
utilfeature "k8s.io/apiserver/pkg/util/feature"
clientset "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
watchtools "k8s.io/client-go/tools/watch"
"k8s.io/client-go/transport"
featuregatetesting "k8s.io/component-base/featuregate/testing"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/klog/v2"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
rbachelper "k8s.io/kubernetes/pkg/apis/rbac/v1"
@ -1037,3 +1041,111 @@ func TestRBACContextContamination(t *testing.T) {
}
}
func TestMonitoringURLs(t *testing.T) {
type request struct {
path string
wantBodyRegex string
}
tests := []struct {
name string
requests []request
}{
{
name: "monitoring endpoints",
requests: []request{
{
path: "/metrics",
wantBodyRegex: `# HELP \w+`,
},
{
path: "/metrics/slis",
wantBodyRegex: `kubernetes_healthcheck\{\w+`,
},
{
path: "/livez",
wantBodyRegex: `^ok$`,
},
{
path: "/readyz",
wantBodyRegex: `^ok$`,
},
{
path: "/healthz",
wantBodyRegex: `^ok$`,
},
{
path: "/statusz",
wantBodyRegex: `kube-apiserver statusz`,
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentStatusz, true)
tCtx := ktesting.Init(t)
// Create a user with the system:monitoring role
monitoringUser := "monitoring-user"
authenticator := group.NewAuthenticatedGroupAdder(bearertoken.New(tokenfile.New(map[string]*user.DefaultInfo{
monitoringUser: {Name: monitoringUser, Groups: []string{"system:monitoring"}},
})))
_, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{
ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
opts.Authorization.Modes = []string{"RBAC"}
},
ModifyServerConfig: func(config *controlplane.Config) {
config.ControlPlane.Generic.Authentication.Authenticator = authenticator
},
})
defer tearDownFn()
transport, err := restclient.TransportFor(kubeConfig)
if err != nil {
t.Fatal(err)
}
for _, r := range tc.requests {
req, err := http.NewRequest(http.MethodGet, kubeConfig.Host+r.path, nil)
if r.path == "/statusz" {
req.Header.Set("Accept", "text/plain")
}
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
resp, err := clientForToken(monitoringUser, transport).Do(req)
if err != nil {
t.Errorf("failed to make request to %s: %v", r, err)
continue
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
t.Fatalf("request to %s: expected %q got %q", r, statusCode(http.StatusOK), statusCode(resp.StatusCode))
}
parsedBytes, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("failed to read response body: %v", err)
}
parsedStr := string(parsedBytes)
matched, err := regexp.MatchString(r.wantBodyRegex, parsedStr)
if err != nil {
t.Fatalf("invalid regex: %v", err)
}
if !matched {
t.Errorf("request to %s: response body does not match expected pattern", r.path)
}
}
})
}
}