mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-06-08 16:30:57 -04:00
Structured statusz
This commit is contained in:
parent
6056b0dfa4
commit
c1a95eb7e2
26 changed files with 1508 additions and 525 deletions
|
|
@ -40,6 +40,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/mux"
|
||||
"k8s.io/apiserver/pkg/server/statusz"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
cacheddiscovery "k8s.io/client-go/discovery/cached/memory"
|
||||
"k8s.io/client-go/informers"
|
||||
|
|
@ -67,7 +68,6 @@ import (
|
|||
"k8s.io/component-base/version/verflag"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/component-base/zpages/statusz"
|
||||
genericcontrollermanager "k8s.io/controller-manager/app"
|
||||
"k8s.io/controller-manager/controller"
|
||||
"k8s.io/controller-manager/pkg/clientbuilder"
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import (
|
|||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/mux"
|
||||
"k8s.io/apiserver/pkg/server/routes"
|
||||
"k8s.io/apiserver/pkg/server/statusz"
|
||||
"k8s.io/apiserver/pkg/util/compatibility"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/informers"
|
||||
|
|
@ -62,7 +63,6 @@ import (
|
|||
"k8s.io/component-base/version/verflag"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/component-base/zpages/statusz"
|
||||
nodeutil "k8s.io/component-helpers/node/util"
|
||||
"k8s.io/klog/v2"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ import (
|
|||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/server/statusz"
|
||||
"k8s.io/apiserver/pkg/util/compatibility"
|
||||
"k8s.io/component-base/zpages/statusz"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
kubeproxyconfig "k8s.io/kubernetes/pkg/proxy/apis/config"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import (
|
|||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/mux"
|
||||
"k8s.io/apiserver/pkg/server/routes"
|
||||
"k8s.io/apiserver/pkg/server/statusz"
|
||||
"k8s.io/apiserver/pkg/util/compatibility"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/informers"
|
||||
|
|
@ -61,7 +62,6 @@ import (
|
|||
"k8s.io/component-base/version/verflag"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/component-base/zpages/statusz"
|
||||
"k8s.io/klog/v2"
|
||||
schedulerserverconfig "k8s.io/kubernetes/cmd/kube-scheduler/app/config"
|
||||
"k8s.io/kubernetes/cmd/kube-scheduler/app/options"
|
||||
|
|
|
|||
96
pkg/generated/openapi/zz_generated.openapi.go
generated
96
pkg/generated/openapi/zz_generated.openapi.go
generated
|
|
@ -90,6 +90,7 @@ import (
|
|||
intstr "k8s.io/apimachinery/pkg/util/intstr"
|
||||
version "k8s.io/apimachinery/pkg/version"
|
||||
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
|
||||
apiv1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
|
||||
clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
configv1alpha1 "k8s.io/cloud-provider/config/v1alpha1"
|
||||
|
|
@ -1336,6 +1337,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
|||
auditv1.Policy{}.OpenAPIModelName(): schema_pkg_apis_audit_v1_Policy(ref),
|
||||
auditv1.PolicyList{}.OpenAPIModelName(): schema_pkg_apis_audit_v1_PolicyList(ref),
|
||||
auditv1.PolicyRule{}.OpenAPIModelName(): schema_pkg_apis_audit_v1_PolicyRule(ref),
|
||||
apiv1alpha1.Statusz{}.OpenAPIModelName(): schema_server_statusz_api_v1alpha1_Statusz(ref),
|
||||
clientauthenticationv1.Cluster{}.OpenAPIModelName(): schema_pkg_apis_clientauthentication_v1_Cluster(ref),
|
||||
clientauthenticationv1.ExecCredential{}.OpenAPIModelName(): schema_pkg_apis_clientauthentication_v1_ExecCredential(ref),
|
||||
clientauthenticationv1.ExecCredentialSpec{}.OpenAPIModelName(): schema_pkg_apis_clientauthentication_v1_ExecCredentialSpec(ref),
|
||||
|
|
@ -64828,6 +64830,100 @@ func schema_pkg_apis_audit_v1_PolicyRule(ref common.ReferenceCallback) common.Op
|
|||
}
|
||||
}
|
||||
|
||||
func schema_server_statusz_api_v1alpha1_Statusz(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Statusz is a struct used for versioned statusz endpoint.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Standard object's metadata.",
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref(metav1.ObjectMeta{}.OpenAPIModelName()),
|
||||
},
|
||||
},
|
||||
"startTime": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "StartTime is the time the component process was initiated.",
|
||||
Ref: ref(metav1.Time{}.OpenAPIModelName()),
|
||||
},
|
||||
},
|
||||
"uptimeSeconds": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "UptimeSeconds is the duration in seconds for which the component has been running continuously.",
|
||||
Default: 0,
|
||||
Type: []string{"integer"},
|
||||
Format: "int64",
|
||||
},
|
||||
},
|
||||
"goVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "GoVersion is the version of the Go programming language used to build the binary. The format is not guaranteed to be consistent across different Go builds.",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"binaryVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "BinaryVersion is the version of the component's binary. The format is not guaranteed to be semantic versioning and may be an arbitrary string.",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"emulationVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "EmulationVersion is the Kubernetes API version which this component is emulating. if present, formatted as \"<major>.<minor>\"",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"paths": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "set",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Paths contains relative URLs to other essential read-only endpoints for debugging and troubleshooting.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"startTime", "uptimeSeconds", "binaryVersion"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
metav1.ObjectMeta{}.OpenAPIModelName(), metav1.Time{}.OpenAPIModelName()},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_clientauthentication_v1_Cluster(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ import (
|
|||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/statusz"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/component-base/configz"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/component-base/zpages/statusz"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import (
|
|||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/httplog"
|
||||
"k8s.io/apiserver/pkg/server/routes"
|
||||
"k8s.io/apiserver/pkg/server/statusz"
|
||||
"k8s.io/apiserver/pkg/util/compatibility"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/apiserver/pkg/util/flushwriter"
|
||||
|
|
@ -70,7 +71,6 @@ import (
|
|||
"k8s.io/component-base/metrics/prometheus/slis"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/component-base/zpages/statusz"
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
"k8s.io/cri-client/pkg/util"
|
||||
podresourcesapi "k8s.io/kubelet/pkg/apis/podresources/v1"
|
||||
|
|
|
|||
|
|
@ -51,13 +51,13 @@ import (
|
|||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/routes"
|
||||
"k8s.io/apiserver/pkg/server/statusz"
|
||||
"k8s.io/apiserver/pkg/storageversion"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
basecompatibility "k8s.io/component-base/compatibility"
|
||||
"k8s.io/component-base/featuregate"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/statusz"
|
||||
"k8s.io/klog/v2"
|
||||
openapibuilder3 "k8s.io/kube-openapi/pkg/builder3"
|
||||
openapicommon "k8s.io/kube-openapi/pkg/common"
|
||||
|
|
|
|||
7
staging/src/k8s.io/apiserver/pkg/server/statusz/OWNERS
Normal file
7
staging/src/k8s.io/apiserver/pkg/server/statusz/OWNERS
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
approvers:
|
||||
- sig-instrumentation-approvers
|
||||
- sig-api-machinery-approvers
|
||||
reviewers:
|
||||
- sig-instrumentation-reviewers
|
||||
labels:
|
||||
- sig/instrumentation
|
||||
11
staging/src/k8s.io/apiserver/pkg/server/statusz/api/OWNERS
Normal file
11
staging/src/k8s.io/apiserver/pkg/server/statusz/api/OWNERS
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Disable inheritance as this is an api owners file
|
||||
options:
|
||||
no_parent_owners: true
|
||||
approvers:
|
||||
- api-approvers
|
||||
reviewers:
|
||||
- api-reviewers
|
||||
- sig-instrumentation-reviewers
|
||||
labels:
|
||||
- kind/api-change
|
||||
- sig/instrumentation
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
Copyright 2025 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.
|
||||
*/
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:openapi-model-package=io.k8s.apiserver.pkg.server.statusz.api.v1alpha1
|
||||
|
||||
// Package v1alpha1 contains API Schema definitions for the zpages v1alpha1 API group
|
||||
package v1alpha1
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright 2025 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 v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
GroupName = "config.k8s.io"
|
||||
Version = "v1alpha1"
|
||||
)
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version}
|
||||
|
||||
var (
|
||||
// SchemeBuilder initializes a scheme builder
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
// AddToScheme is a global function that adds this group's types to a scheme
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&Statusz{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Copyright 2025 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 v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// Statusz is a struct used for versioned statusz endpoint.
|
||||
type Statusz struct {
|
||||
// TypeMeta is the type metadata for the object.
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// Standard object's metadata.
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
// StartTime is the time the component process was initiated.
|
||||
StartTime metav1.Time `json:"startTime"`
|
||||
// UptimeSeconds is the duration in seconds for which the component has been running continuously.
|
||||
UptimeSeconds int64 `json:"uptimeSeconds"`
|
||||
// GoVersion is the version of the Go programming language used to build the binary.
|
||||
// The format is not guaranteed to be consistent across different Go builds.
|
||||
// +optional
|
||||
GoVersion string `json:"goVersion"`
|
||||
// BinaryVersion is the version of the component's binary.
|
||||
// The format is not guaranteed to be semantic versioning and may be an arbitrary string.
|
||||
BinaryVersion string `json:"binaryVersion"`
|
||||
// EmulationVersion is the Kubernetes API version which this component is emulating.
|
||||
// if present, formatted as "<major>.<minor>"
|
||||
// +optional
|
||||
EmulationVersion string `json:"emulationVersion,omitempty"`
|
||||
// Paths contains relative URLs to other essential read-only endpoints for debugging and troubleshooting.
|
||||
// +optional
|
||||
// +listType=set
|
||||
Paths []string `json:"paths"`
|
||||
}
|
||||
58
staging/src/k8s.io/apiserver/pkg/server/statusz/api/v1alpha1/zz_generated.deepcopy.go
generated
Normal file
58
staging/src/k8s.io/apiserver/pkg/server/statusz/api/v1alpha1/zz_generated.deepcopy.go
generated
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Statusz) DeepCopyInto(out *Statusz) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.StartTime.DeepCopyInto(&out.StartTime)
|
||||
if in.Paths != nil {
|
||||
in, out := &in.Paths, &out.Paths
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Statusz.
|
||||
func (in *Statusz) DeepCopy() *Statusz {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Statusz)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Statusz) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
27
staging/src/k8s.io/apiserver/pkg/server/statusz/api/v1alpha1/zz_generated.model_name.go
generated
Normal file
27
staging/src/k8s.io/apiserver/pkg/server/statusz/api/v1alpha1/zz_generated.model_name.go
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by openapi-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
// OpenAPIModelName returns the OpenAPI model name for this type.
|
||||
func (in Statusz) OpenAPIModelName() string {
|
||||
return "io.k8s.apiserver.pkg.server.statusz.api.v1alpha1.Statusz"
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright 2025 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 negotiate
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// StatuszEndpointRestrictions implements content negotiation restrictions for the z-pages.
|
||||
// It is used to validate and restrict which GroupVersionKinds are allowed for structured responses.
|
||||
type StatuszEndpointRestrictions struct{}
|
||||
|
||||
// AllowsMediaTypeTransform checks if the provided GVK is supported for structured z-page responses.
|
||||
func (StatuszEndpointRestrictions) AllowsMediaTypeTransform(mimeType string, mimeSubType string, gvk *schema.GroupVersionKind) bool {
|
||||
if mimeType == "text" && mimeSubType == "plain" {
|
||||
return gvk == nil
|
||||
}
|
||||
return isStructured(gvk)
|
||||
}
|
||||
|
||||
func (StatuszEndpointRestrictions) AllowsServerVersion(string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (StatuszEndpointRestrictions) AllowsStreamSchema(s string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isStructured(gvk *schema.GroupVersionKind) bool {
|
||||
if gvk != nil {
|
||||
if gvk.Group == "config.k8s.io" && gvk.Version == "v1alpha1" {
|
||||
// TODO: extend this to Flagz once we have a structured Flagz type.
|
||||
if gvk.Kind == "Statusz" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -20,9 +20,9 @@ import (
|
|||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"k8s.io/component-base/compatibility"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/component-base/compatibility"
|
||||
compbasemetrics "k8s.io/component-base/metrics"
|
||||
utilversion "k8s.io/component-base/version"
|
||||
)
|
||||
|
|
@ -33,6 +33,7 @@ type statuszRegistry interface {
|
|||
binaryVersion() *version.Version
|
||||
emulationVersion() *version.Version
|
||||
paths() []string
|
||||
deprecatedVersions() map[string]bool
|
||||
}
|
||||
|
||||
type registry struct {
|
||||
|
|
@ -40,6 +41,8 @@ type registry struct {
|
|||
effectiveVersion compatibility.EffectiveVersion
|
||||
// listedPaths is an alphabetically sorted list of paths to be reported at /.
|
||||
listedPaths []string
|
||||
// deprecatedVersionsMap is a map of deprecated statusz versions.
|
||||
deprecatedVersionsMap map[string]bool
|
||||
}
|
||||
|
||||
// Option is a function to configure registry.
|
||||
|
|
@ -88,3 +91,7 @@ func (r *registry) paths() []string {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registry) deprecatedVersions() map[string]bool {
|
||||
return r.deprecatedVersionsMap
|
||||
}
|
||||
257
staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go
Normal file
257
staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
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"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/component-base/compatibility"
|
||||
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/server/statusz/negotiate"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
v1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
delimiters = []string{":", ": ", "=", " "}
|
||||
nonDebuggingEndpoints = map[string]bool{
|
||||
"/apis": true,
|
||||
"/api": true,
|
||||
"/openid": true,
|
||||
"/openapi": true,
|
||||
"/.well-known": true,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultStatuszPath = "/statusz"
|
||||
Kind = "Statusz"
|
||||
GroupName = "config.k8s.io"
|
||||
Version = "v1alpha1"
|
||||
)
|
||||
|
||||
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.
|
||||
`
|
||||
|
||||
var schemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version}
|
||||
|
||||
type mux interface {
|
||||
Handle(path string, handler http.Handler)
|
||||
}
|
||||
|
||||
type ListedPathsOption []string
|
||||
|
||||
func NewRegistry(effectiveVersion compatibility.EffectiveVersion, opts ...Option) statuszRegistry {
|
||||
r := ®istry{
|
||||
effectiveVersion: effectiveVersion,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func Install(m mux, componentName string, reg statuszRegistry) {
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(v1alpha1.AddToScheme(scheme))
|
||||
codecFactory := serializer.NewCodecFactory(
|
||||
scheme,
|
||||
serializer.WithSerializer(func(_ runtime.ObjectCreater, _ runtime.ObjectTyper) runtime.SerializerInfo {
|
||||
textSerializer := statuszTextSerializer{componentName, reg}
|
||||
return runtime.SerializerInfo{
|
||||
MediaType: "text/plain",
|
||||
MediaTypeType: "text",
|
||||
MediaTypeSubType: "plain",
|
||||
EncodesAsText: true,
|
||||
Serializer: textSerializer,
|
||||
PrettySerializer: textSerializer,
|
||||
}
|
||||
}),
|
||||
)
|
||||
m.Handle(DefaultStatuszPath, handleStatusz(componentName, reg, codecFactory, negotiate.StatuszEndpointRestrictions{}))
|
||||
}
|
||||
|
||||
func handleStatusz(componentName string, reg statuszRegistry, serializer runtime.NegotiatedSerializer, restrictions negotiate.StatuszEndpointRestrictions) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
obj := statusz(componentName, reg)
|
||||
acceptHeader := r.Header.Get("Accept")
|
||||
if strings.TrimSpace(acceptHeader) == "" {
|
||||
writePlainTextResponse(obj, serializer, w)
|
||||
return
|
||||
}
|
||||
|
||||
mediaType, serializerInfo, err := negotiation.NegotiateOutputMediaType(r, serializer, restrictions)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
responsewriters.ErrorNegotiated(
|
||||
err,
|
||||
serializer,
|
||||
schema.GroupVersion{},
|
||||
w,
|
||||
r,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
var targetGV schema.GroupVersion
|
||||
switch serializerInfo.MediaType {
|
||||
case "application/json":
|
||||
if mediaType.Convert == nil {
|
||||
err := fmt.Errorf("content negotiation failed: mediaType.Convert is nil for application/json")
|
||||
utilruntime.HandleError(err)
|
||||
responsewriters.ErrorNegotiated(
|
||||
err,
|
||||
serializer,
|
||||
schema.GroupVersion{},
|
||||
w,
|
||||
r,
|
||||
)
|
||||
return
|
||||
}
|
||||
targetGV = mediaType.Convert.GroupVersion()
|
||||
deprecated := reg.deprecatedVersions()[targetGV.Version]
|
||||
if deprecated {
|
||||
w.Header().Set("Warning", `299 - "This version of the statusz endpoint is deprecated. Please use a newer version."`)
|
||||
}
|
||||
case "text/plain":
|
||||
// Even though text/plain serialization does not use the group/version,
|
||||
// the serialization machinery expects a non-zero schema.GroupVersion to be passed.
|
||||
// Passing the zero value can cause errors or unexpected behavior in the negotiation logic.
|
||||
targetGV = schemeGroupVersion
|
||||
default:
|
||||
err = fmt.Errorf("content negotiation failed: unsupported media type '%s'", serializerInfo.MediaType)
|
||||
utilruntime.HandleError(err)
|
||||
responsewriters.ErrorNegotiated(
|
||||
err,
|
||||
serializer,
|
||||
schema.GroupVersion{},
|
||||
w,
|
||||
r,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
writeResponse(obj, serializer, targetGV, restrictions, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// writePlainTextResponse writes the statusz response as text/plain using the registered serializer.
|
||||
func writePlainTextResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
// Find the text/plain serializer
|
||||
var textSerializer runtime.Serializer
|
||||
for _, info := range serializer.SupportedMediaTypes() {
|
||||
if info.MediaType == "text/plain" {
|
||||
textSerializer = info.Serializer
|
||||
break
|
||||
}
|
||||
}
|
||||
if textSerializer == nil {
|
||||
utilruntime.HandleError(fmt.Errorf("text/plain serializer not available"))
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := textSerializer.Encode(obj, w); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("error encoding statusz as text/plain: %w", err))
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func writeResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, targetGV schema.GroupVersion, restrictions negotiate.StatuszEndpointRestrictions, w http.ResponseWriter, r *http.Request) {
|
||||
responsewriters.WriteObjectNegotiated(
|
||||
serializer,
|
||||
restrictions,
|
||||
targetGV,
|
||||
w,
|
||||
r,
|
||||
http.StatusOK,
|
||||
obj,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func statusz(componentName string, reg statuszRegistry) *v1alpha1.Statusz {
|
||||
startTime := reg.processStartTime()
|
||||
upTimeSeconds := max(0, int64(time.Since(startTime).Seconds()))
|
||||
goVersion := reg.goVersion()
|
||||
binaryVersion := reg.binaryVersion().String()
|
||||
var emulationVersion string
|
||||
if reg.emulationVersion() != nil {
|
||||
emulationVersion = reg.emulationVersion().String()
|
||||
}
|
||||
|
||||
paths := aggregatePaths(reg.paths())
|
||||
data := &v1alpha1.Statusz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: Kind,
|
||||
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: componentName,
|
||||
},
|
||||
StartTime: metav1.Time{Time: startTime},
|
||||
UptimeSeconds: upTimeSeconds,
|
||||
GoVersion: goVersion,
|
||||
BinaryVersion: binaryVersion,
|
||||
EmulationVersion: emulationVersion,
|
||||
Paths: paths,
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func aggregatePaths(listedPaths []string) []string {
|
||||
paths := make(map[string]bool)
|
||||
for _, listedPath := range listedPaths {
|
||||
parts := strings.Split(listedPath, "/")
|
||||
if len(parts) < 2 || parts[1] == "" {
|
||||
continue
|
||||
}
|
||||
folder := "/" + parts[1]
|
||||
if !paths[folder] && !nonDebuggingEndpoints[folder] {
|
||||
paths[folder] = true
|
||||
}
|
||||
}
|
||||
|
||||
var sortedPaths []string
|
||||
for p := range paths {
|
||||
sortedPaths = append(sortedPaths, p)
|
||||
}
|
||||
sort.Strings(sortedPaths)
|
||||
|
||||
return sortedPaths
|
||||
}
|
||||
361
staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go
Normal file
361
staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
/*
|
||||
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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
|
||||
)
|
||||
|
||||
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
|
||||
Paths: /livez /readyz
|
||||
`
|
||||
|
||||
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
|
||||
|
||||
Paths: /livez /readyz
|
||||
`
|
||||
|
||||
func TestHandleStatusz(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)
|
||||
fakeListedPaths := []string{"/livez/poststarthook/peer-discovery-cache-sync", "/livez/post", "/readyz/informer-sync", "/readyz/log", "/readyz/ping"}
|
||||
tests := []struct {
|
||||
name string
|
||||
acceptHeader string
|
||||
componentName string
|
||||
registry fakeRegistry
|
||||
wantStatusCode int
|
||||
wantBody string
|
||||
wantJSONBody *v1alpha1.Statusz
|
||||
wantWarning bool
|
||||
}{
|
||||
{
|
||||
name: "valid request for text/plain",
|
||||
acceptHeader: "text/plain",
|
||||
componentName: "test-server",
|
||||
registry: fakeRegistry{
|
||||
startTime: fakeStartTime,
|
||||
goVer: fakeGoVersion,
|
||||
binaryVer: fakeBinaryVersion,
|
||||
emulationVer: fakeEmulationVersion,
|
||||
listedPaths: fakeListedPaths,
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantBody: fmt.Sprintf(
|
||||
wantTmpl,
|
||||
"test-server",
|
||||
fakeStartTime.Format(time.UnixDate),
|
||||
fakeUptime,
|
||||
fakeGoVersion,
|
||||
fakeBinaryVersion,
|
||||
fakeEmulationVersion,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "valid request for v1alpha1",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz",
|
||||
componentName: "test-server",
|
||||
registry: fakeRegistry{
|
||||
startTime: fakeStartTime,
|
||||
goVer: fakeGoVersion,
|
||||
binaryVer: fakeBinaryVersion,
|
||||
emulationVer: fakeEmulationVersion,
|
||||
listedPaths: fakeListedPaths,
|
||||
deprecated: map[string]bool{},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantJSONBody: &v1alpha1.Statusz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: Kind,
|
||||
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-server",
|
||||
},
|
||||
StartTime: metav1.Time{Time: fakeStartTime},
|
||||
UptimeSeconds: int64(time.Since(fakeStartTime).Seconds()),
|
||||
GoVersion: fakeGoVersion,
|
||||
BinaryVersion: fakeBvStr,
|
||||
EmulationVersion: fakeEvStr,
|
||||
Paths: []string{"/livez", "/readyz"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no accept header",
|
||||
acceptHeader: "",
|
||||
componentName: "test-server",
|
||||
registry: fakeRegistry{
|
||||
startTime: fakeStartTime,
|
||||
goVer: fakeGoVersion,
|
||||
binaryVer: fakeBinaryVersion,
|
||||
emulationVer: fakeEmulationVersion,
|
||||
listedPaths: fakeListedPaths,
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantBody: fmt.Sprintf(
|
||||
wantTmpl,
|
||||
"test-server",
|
||||
fakeStartTime.Format(time.UnixDate),
|
||||
fakeUptime,
|
||||
fakeGoVersion,
|
||||
fakeBinaryVersion,
|
||||
fakeEmulationVersion,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "invalid accept header",
|
||||
acceptHeader: "application/xml",
|
||||
componentName: "test-server",
|
||||
wantStatusCode: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "missing emulation version",
|
||||
acceptHeader: "text/plain",
|
||||
componentName: "test-server",
|
||||
registry: fakeRegistry{
|
||||
startTime: fakeStartTime,
|
||||
goVer: fakeGoVersion,
|
||||
binaryVer: fakeBinaryVersion,
|
||||
emulationVer: nil,
|
||||
listedPaths: fakeListedPaths,
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantBody: fmt.Sprintf(
|
||||
wantTmplWithoutEmulation,
|
||||
"test-server",
|
||||
fakeStartTime.Format(time.UnixDate),
|
||||
fakeUptime,
|
||||
fakeGoVersion,
|
||||
fakeBinaryVersion,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "application/json without params",
|
||||
acceptHeader: "application/json",
|
||||
componentName: "test-server",
|
||||
wantStatusCode: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "application/json with missing as",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
|
||||
componentName: "test-server",
|
||||
wantStatusCode: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "wildcard accept header",
|
||||
acceptHeader: "*/*",
|
||||
componentName: "test-server",
|
||||
registry: fakeRegistry{
|
||||
startTime: fakeStartTime,
|
||||
goVer: fakeGoVersion,
|
||||
binaryVer: fakeBinaryVersion,
|
||||
emulationVer: fakeEmulationVersion,
|
||||
listedPaths: fakeListedPaths,
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantBody: fmt.Sprintf(
|
||||
wantTmpl,
|
||||
"test-server",
|
||||
fakeStartTime.Format(time.UnixDate),
|
||||
fakeUptime,
|
||||
fakeGoVersion,
|
||||
fakeBinaryVersion,
|
||||
fakeEmulationVersion,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad json header fall back wildcard",
|
||||
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Statusz,*/*",
|
||||
componentName: "test-server",
|
||||
registry: fakeRegistry{
|
||||
startTime: fakeStartTime,
|
||||
goVer: fakeGoVersion,
|
||||
binaryVer: fakeBinaryVersion,
|
||||
emulationVer: fakeEmulationVersion,
|
||||
listedPaths: fakeListedPaths,
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantBody: fmt.Sprintf(
|
||||
wantTmpl,
|
||||
"test-server",
|
||||
fakeStartTime.Format(time.UnixDate),
|
||||
fakeUptime,
|
||||
fakeGoVersion,
|
||||
fakeBinaryVersion,
|
||||
fakeEmulationVersion,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "deprecated version request",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz",
|
||||
componentName: "test-server",
|
||||
registry: fakeRegistry{
|
||||
startTime: fakeStartTime,
|
||||
goVer: fakeGoVersion,
|
||||
binaryVer: fakeBinaryVersion,
|
||||
emulationVer: fakeEmulationVersion,
|
||||
listedPaths: fakeListedPaths,
|
||||
deprecated: map[string]bool{
|
||||
"v1alpha1": true,
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantJSONBody: &v1alpha1.Statusz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: Kind,
|
||||
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-server",
|
||||
},
|
||||
StartTime: metav1.Time{Time: fakeStartTime},
|
||||
UptimeSeconds: int64(time.Since(fakeStartTime).Seconds()),
|
||||
GoVersion: fakeGoVersion,
|
||||
BinaryVersion: fakeBvStr,
|
||||
EmulationVersion: fakeEvStr,
|
||||
Paths: []string{"/livez", "/readyz"},
|
||||
},
|
||||
wantWarning: true,
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if tt.acceptHeader != "" {
|
||||
req.Header.Set("Accept", tt.acceptHeader)
|
||||
}
|
||||
|
||||
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 {
|
||||
if tt.wantJSONBody != nil {
|
||||
var got v1alpha1.Statusz
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("unexpected error while unmarshalling response: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(*tt.wantJSONBody, got, timeEqual()); diff != "" {
|
||||
t.Errorf("Unexpected diff on response (-want,+got):\n%s", diff)
|
||||
}
|
||||
if tt.wantWarning {
|
||||
if !strings.Contains(w.Header().Get("Warning"), "deprecated") {
|
||||
t.Errorf("expected deprecation warning in header, but got: %s", w.Header().Get("Warning"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if diff := cmp.Diff(tt.wantBody, string(w.Body.String())); diff != "" {
|
||||
t.Errorf("Unexpected diff on response (-want,+got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
listedPaths []string
|
||||
deprecated map[string]bool
|
||||
}
|
||||
|
||||
func (f fakeRegistry) deprecatedVersions() map[string]bool {
|
||||
return f.deprecated
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (f fakeRegistry) paths() []string {
|
||||
return f.listedPaths
|
||||
}
|
||||
|
||||
func timeEqual() cmp.Option {
|
||||
return cmp.Comparer(func(expectedTime, actualTime metav1.Time) bool {
|
||||
return expectedTime.Truncate(time.Second).Equal(actualTime.Truncate(time.Second))
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
Copyright 2025 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"
|
||||
"html"
|
||||
"io"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
v1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
|
||||
)
|
||||
|
||||
// statuszTextSerializer implements runtime.Serializer for text/plain output.
|
||||
type statuszTextSerializer struct {
|
||||
componentName string
|
||||
reg statuszRegistry
|
||||
}
|
||||
|
||||
// Encode writes the statusz information in plain text format to the given writer, using the provided obj.
|
||||
func (s statuszTextSerializer) Encode(obj runtime.Object, w io.Writer) error {
|
||||
if _, err := fmt.Fprintf(w, headerFmt, s.componentName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
randomIndex := rand.Intn(len(delimiters))
|
||||
delim := html.EscapeString(delimiters[randomIndex])
|
||||
|
||||
statuszObj, ok := obj.(*v1alpha1.Statusz)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected *v1alpha1.Statusz, got %T", obj)
|
||||
}
|
||||
|
||||
startTime := html.EscapeString(statuszObj.StartTime.Time.Format(time.UnixDate))
|
||||
uptimeStr := html.EscapeString(uptime(statuszObj.StartTime.Time))
|
||||
goVersion := html.EscapeString(statuszObj.GoVersion)
|
||||
binaryVersion := html.EscapeString(statuszObj.BinaryVersion)
|
||||
|
||||
var emulationVersion string
|
||||
if statuszObj.EmulationVersion != "" {
|
||||
emulationVersion = fmt.Sprintf(`Emulation version%s %s`, delim, html.EscapeString(statuszObj.EmulationVersion))
|
||||
}
|
||||
|
||||
paths := strings.Join(statuszObj.Paths, " ")
|
||||
if paths != "" {
|
||||
paths = fmt.Sprintf(`Paths%s %s`, delim, html.EscapeString(paths))
|
||||
}
|
||||
|
||||
status := fmt.Sprintf(`
|
||||
Started%[1]s %[2]s
|
||||
Up%[1]s %[3]s
|
||||
Go version%[1]s %[4]s
|
||||
Binary version%[1]s %[5]s
|
||||
%[6]s
|
||||
%[7]s
|
||||
`, delim, startTime, uptimeStr, goVersion, binaryVersion, emulationVersion, paths)
|
||||
_, err := fmt.Fprint(w, status)
|
||||
return err
|
||||
}
|
||||
|
||||
// Decode is not supported for text/plain serialization.
|
||||
func (s statuszTextSerializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
|
||||
return nil, nil, fmt.Errorf("decode not supported for text/plain")
|
||||
}
|
||||
|
||||
// Identifier returns a unique identifier for this serializer.
|
||||
func (s statuszTextSerializer) Identifier() runtime.Identifier {
|
||||
return runtime.Identifier("statuszTextSerializer")
|
||||
}
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
/*
|
||||
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"
|
||||
"html"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/component-base/compatibility"
|
||||
"k8s.io/component-base/zpages/httputil"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
delimiters = []string{":", ": ", "=", " "}
|
||||
nonDebuggingEndpoints = map[string]bool{
|
||||
"/apis": true,
|
||||
"/api": true,
|
||||
"/openid": true,
|
||||
"/openapi": true,
|
||||
"/.well-known": true,
|
||||
}
|
||||
)
|
||||
|
||||
const DefaultStatuszPath = "/statusz"
|
||||
|
||||
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.
|
||||
`
|
||||
|
||||
type mux interface {
|
||||
Handle(path string, handler http.Handler)
|
||||
}
|
||||
|
||||
type ListedPathsOption []string
|
||||
|
||||
func NewRegistry(effectiveVersion compatibility.EffectiveVersion, opts ...func(*registry)) statuszRegistry {
|
||||
r := ®istry{effectiveVersion: effectiveVersion}
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func Install(m mux, componentName string, reg statuszRegistry) {
|
||||
m.Handle(DefaultStatuszPath, handleStatusz(componentName, reg))
|
||||
}
|
||||
|
||||
func handleStatusz(componentName string, reg statuszRegistry) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !httputil.AcceptableMediaType(r) {
|
||||
http.Error(w, httputil.ErrUnsupportedMediaType.Error(), http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, headerFmt, componentName)
|
||||
data, err := populateStatuszData(reg, componentName)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func populateStatuszData(reg statuszRegistry, componentName string) (string, error) {
|
||||
randomIndex := rand.Intn(len(delimiters))
|
||||
delim := html.EscapeString(delimiters[randomIndex])
|
||||
startTime := html.EscapeString(reg.processStartTime().Format(time.UnixDate))
|
||||
uptime := html.EscapeString(uptime(reg.processStartTime()))
|
||||
goVersion := html.EscapeString(reg.goVersion())
|
||||
binaryVersion := html.EscapeString(reg.binaryVersion().String())
|
||||
|
||||
var emulationVersion string
|
||||
if reg.emulationVersion() != nil {
|
||||
emulationVersion = fmt.Sprintf(`Emulation version%s %s`, delim, html.EscapeString(reg.emulationVersion().String()))
|
||||
}
|
||||
paths := aggregatePaths(reg.paths())
|
||||
if paths != "" {
|
||||
paths = fmt.Sprintf(`Paths%s %s`, delim, html.EscapeString(paths))
|
||||
}
|
||||
|
||||
status := fmt.Sprintf(`
|
||||
Started%[1]s %[2]s
|
||||
Up%[1]s %[3]s
|
||||
Go version%[1]s %[4]s
|
||||
Binary version%[1]s %[5]s
|
||||
%[6]s
|
||||
%[7]s
|
||||
`, delim, startTime, uptime, goVersion, binaryVersion, emulationVersion, paths)
|
||||
|
||||
return status, 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)
|
||||
}
|
||||
|
||||
func aggregatePaths(listedPaths []string) string {
|
||||
paths := make(map[string]bool)
|
||||
for _, listedPath := range listedPaths {
|
||||
folder := "/" + strings.Split(listedPath, "/")[1]
|
||||
if !paths[folder] && !nonDebuggingEndpoints[folder] {
|
||||
paths[folder] = true
|
||||
}
|
||||
}
|
||||
|
||||
var sortedPaths []string
|
||||
for p := range paths {
|
||||
sortedPaths = append(sortedPaths, p)
|
||||
}
|
||||
sort.Strings(sortedPaths)
|
||||
|
||||
var path string
|
||||
for _, p := range sortedPaths {
|
||||
path += " " + p
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
/*
|
||||
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
|
||||
Paths: /livez /readyz
|
||||
`
|
||||
|
||||
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
|
||||
|
||||
Paths: /livez /readyz
|
||||
`
|
||||
|
||||
const wantTmplWithKubeApiserverComp = `
|
||||
%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
|
||||
|
||||
Paths: /livez /readyz
|
||||
`
|
||||
|
||||
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)
|
||||
fakeListedPaths := []string{"/livez/poststarthook/peer-discovery-cache-sync", "/livez/post", "/readyz/informer-sync", "/readyz/log", "/readyz/ping"}
|
||||
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,
|
||||
listedPaths: fakeListedPaths,
|
||||
},
|
||||
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,
|
||||
listedPaths: fakeListedPaths,
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantBody: fmt.Sprintf(
|
||||
wantTmplWithoutEmulation,
|
||||
"test-server",
|
||||
fakeStartTime.Format(time.UnixDate),
|
||||
fakeUptime,
|
||||
fakeGoVersion,
|
||||
fakeBinaryVersion,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "valid request for kube-apiserver",
|
||||
componentName: "kube-apiserver",
|
||||
reqHeader: "text/plain; charset=utf-8",
|
||||
registry: fakeRegistry{
|
||||
startTime: fakeStartTime,
|
||||
goVer: fakeGoVersion,
|
||||
binaryVer: fakeBinaryVersion,
|
||||
emulationVer: nil,
|
||||
listedPaths: fakeListedPaths,
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantBody: fmt.Sprintf(
|
||||
wantTmplWithKubeApiserverComp,
|
||||
"kube-apiserver",
|
||||
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 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
|
||||
listedPaths []string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (f fakeRegistry) paths() []string {
|
||||
return f.listedPaths
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
|
|
@ -47,6 +48,8 @@ import (
|
|||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/etcd"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
|
||||
v1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -181,23 +184,111 @@ func TestStatusz(t *testing.T) {
|
|||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
res := client.CoreV1().RESTClient().Get().RequestURI("/statusz").Do(context.TODO())
|
||||
var status int
|
||||
res.StatusCode(&status)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("statusz/ should be healthy, got %v", status)
|
||||
wantBodyStr := "statusz\nWarning: This endpoint is not meant to be machine parseable"
|
||||
wantBodyJSON := &v1alpha1.Statusz{
|
||||
// StartTime, UptimeSeconds, GoVersion, BinaryVersion,
|
||||
// EmulationVersion, Paths are dynamic, so we only check
|
||||
// static fields
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Statusz",
|
||||
APIVersion: "config.k8s.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "apiserver",
|
||||
},
|
||||
Paths: []string{"/healthz", "/livez", "/metrics", "/readyz", "/statusz", "/version"},
|
||||
}
|
||||
|
||||
expectedHeader := `
|
||||
apiserver statusz
|
||||
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.`
|
||||
|
||||
raw, err := res.Raw()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(raw, []byte(expectedHeader)) {
|
||||
t.Fatalf("Header mismatch!\nExpected:\n%s\n\nGot:\n%s", expectedHeader, string(raw))
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
acceptHeader string
|
||||
wantStatus int
|
||||
wantBodySub string // for text/plain responses
|
||||
wantJSON *v1alpha1.Statusz // for structured response
|
||||
}{
|
||||
{
|
||||
name: "text plain response",
|
||||
acceptHeader: "text/plain",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "structured json response",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz",
|
||||
wantStatus: http.StatusOK,
|
||||
wantJSON: wantBodyJSON,
|
||||
},
|
||||
{
|
||||
name: "no accept header (defaults to text)",
|
||||
acceptHeader: "",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "invalid accept header",
|
||||
acceptHeader: "application/xml",
|
||||
wantStatus: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "application/json without params",
|
||||
acceptHeader: "application/json",
|
||||
wantStatus: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "application/json with missing as",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
|
||||
wantStatus: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "wildcard accept header",
|
||||
acceptHeader: "*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "bad json header fall back wildcard",
|
||||
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Statusz,*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := client.CoreV1().RESTClient().Get().RequestURI("/statusz")
|
||||
req.SetHeader("Accept", tc.acceptHeader)
|
||||
res := req.Do(context.TODO())
|
||||
var status int
|
||||
res.StatusCode(&status)
|
||||
if status != tc.wantStatus {
|
||||
t.Fatalf("want status %d, got %d", tc.wantStatus, status)
|
||||
}
|
||||
raw, err := res.Raw()
|
||||
if err != nil && tc.wantStatus == http.StatusOK {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if tc.wantStatus == http.StatusOK {
|
||||
if tc.wantBodySub != "" {
|
||||
if !bytes.Contains(raw, []byte(tc.wantBodySub)) {
|
||||
t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodySub, string(raw))
|
||||
}
|
||||
}
|
||||
if tc.wantJSON != nil {
|
||||
var got v1alpha1.Statusz
|
||||
if err := json.Unmarshal(raw, &got); err != nil {
|
||||
t.Fatalf("error unmarshalling JSON: %v", err)
|
||||
}
|
||||
// Only check static fields, since others are dynamic
|
||||
if got.TypeMeta != tc.wantJSON.TypeMeta {
|
||||
t.Errorf("TypeMeta mismatch: want %+v, got %+v", tc.wantJSON.TypeMeta, got.TypeMeta)
|
||||
}
|
||||
if got.ObjectMeta.Name != tc.wantJSON.ObjectMeta.Name {
|
||||
t.Errorf("ObjectMeta.Name mismatch: want %q, got %q", tc.wantJSON.ObjectMeta.Name, got.ObjectMeta.Name)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantJSON.Paths, got.Paths); diff != "" {
|
||||
t.Errorf("Paths mismatch (-want,+got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package serving
|
|||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
|
@ -28,6 +29,9 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/util/retry"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
|
|
@ -40,8 +44,7 @@ import (
|
|||
|
||||
func TestEndpointHandlers(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
||||
features.ComponentFlagz: true,
|
||||
features.ComponentStatusz: true,
|
||||
features.ComponentFlagz: true,
|
||||
})
|
||||
|
||||
server, configStr, _, err := startTestAPIServer(t)
|
||||
|
|
@ -133,17 +136,6 @@ func TestEndpointHandlers(t *testing.T) {
|
|||
`Warning: This endpoint is not meant to be machine parseable, ` +
|
||||
`has no formatting compatibility guarantees and is for debugging purposes only.`,
|
||||
},
|
||||
{
|
||||
name: "/statusz",
|
||||
path: "/statusz",
|
||||
requestHeader: map[string]string{"Accept": "text/plain"},
|
||||
wantResponseCode: http.StatusOK,
|
||||
wantResponseBodyRegx: `(?s)^\n` +
|
||||
`kube-scheduler statusz\n` +
|
||||
`Warning: This endpoint is not meant to be machine parseable, ` +
|
||||
`has no formatting compatibility guarantees and is for debugging purposes only.+` +
|
||||
`Paths([:=\s]+)/configz /flagz /healthz /livez /metrics /readyz\n$`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -170,7 +162,7 @@ func TestEndpointHandlers(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("Failed to get client from test server: %v", err)
|
||||
}
|
||||
req, err := http.NewRequest("GET", base+tt.path, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, base+tt.path, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to request: %v", err)
|
||||
}
|
||||
|
|
@ -225,6 +217,162 @@ func TestEndpointHandlers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSchedulerStatusz(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
||||
features.ComponentStatusz: true,
|
||||
})
|
||||
|
||||
server, configStr, _, err := startTestAPIServer(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start kube-apiserver server: %v", err)
|
||||
}
|
||||
defer server.TearDownFn()
|
||||
|
||||
apiserverConfig, err := os.CreateTemp("", "kubeconfig")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create config file: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove(apiserverConfig.Name())
|
||||
}()
|
||||
if _, err = apiserverConfig.WriteString(configStr); err != nil {
|
||||
t.Fatalf("Failed to write config file: %v", err)
|
||||
}
|
||||
|
||||
_, ctx := ktesting.NewTestContext(t)
|
||||
result, err := kubeschedulertesting.StartTestServer(
|
||||
t, ctx,
|
||||
[]string{"--kubeconfig", apiserverConfig.Name(), "--leader-elect=false", "--authorization-always-allow-paths=/statusz"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start kube-scheduler server: %v", err)
|
||||
}
|
||||
if result.TearDownFn != nil {
|
||||
defer result.TearDownFn()
|
||||
}
|
||||
|
||||
client, base, err := clientAndURLFromTestServer(result)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get client from test server: %v", err)
|
||||
}
|
||||
|
||||
wantBodyStr := "kube-scheduler statusz\nWarning: This endpoint is not meant to be machine parseable"
|
||||
wantBodyJSON := &v1alpha1.Statusz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Statusz",
|
||||
APIVersion: "config.k8s.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "kube-scheduler",
|
||||
},
|
||||
Paths: []string{"/configz", "/healthz", "/livez", "/metrics", "/readyz"},
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
acceptHeader string
|
||||
wantStatus int
|
||||
wantBodySub string // for text/plain
|
||||
wantJSON *v1alpha1.Statusz // for application/json
|
||||
}{
|
||||
{
|
||||
name: "text plain response",
|
||||
acceptHeader: "text/plain",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "structured json response",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz",
|
||||
wantStatus: http.StatusOK,
|
||||
wantJSON: wantBodyJSON,
|
||||
},
|
||||
{
|
||||
name: "no accept header (defaults to text)",
|
||||
acceptHeader: "",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "invalid accept header",
|
||||
acceptHeader: "application/xml",
|
||||
wantStatus: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "application/json without params",
|
||||
acceptHeader: "application/json",
|
||||
wantStatus: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "application/json with missing as",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
|
||||
wantStatus: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "wildcard accept header",
|
||||
acceptHeader: "*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "bad json header fall back wildcard",
|
||||
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Statusz,*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, base+"/statusz", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", tc.acceptHeader)
|
||||
r, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to GET /statusz: %v", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
if err = r.Body.Close(); err != nil {
|
||||
t.Fatalf("failed to close response body: %v", err)
|
||||
}
|
||||
|
||||
if r.StatusCode != tc.wantStatus {
|
||||
t.Fatalf("want status %d, got %d", tc.wantStatus, r.StatusCode)
|
||||
}
|
||||
|
||||
if tc.wantStatus == http.StatusOK {
|
||||
if tc.wantBodySub != "" {
|
||||
if !strings.Contains(string(body), tc.wantBodySub) {
|
||||
t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodySub, string(body))
|
||||
}
|
||||
}
|
||||
if tc.wantJSON != nil {
|
||||
var got v1alpha1.Statusz
|
||||
if err := json.Unmarshal(body, &got); err != nil {
|
||||
t.Fatalf("error unmarshalling JSON: %v", err)
|
||||
}
|
||||
// Only check static fields, since others are dynamic
|
||||
if got.TypeMeta != tc.wantJSON.TypeMeta {
|
||||
t.Errorf("TypeMeta mismatch: want %+v, got %+v", tc.wantJSON.TypeMeta, got.TypeMeta)
|
||||
}
|
||||
if got.ObjectMeta.Name != tc.wantJSON.ObjectMeta.Name {
|
||||
t.Errorf("ObjectMeta.Name mismatch: want %q, got %q", tc.wantJSON.ObjectMeta.Name, got.ObjectMeta.Name)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantJSON.Paths, got.Paths); diff != "" {
|
||||
t.Errorf("Paths mismatch (-want,+got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make this a util function once there is a unified way to start a testing apiserver so that we can reuse it.
|
||||
func startTestAPIServer(t *testing.T) (server *kubeapiservertesting.TestServer, apiserverConfig, token string, err error) {
|
||||
// Insulate this test from picking up in-cluster config when run inside a pod
|
||||
|
|
|
|||
|
|
@ -20,17 +20,21 @@ import (
|
|||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
|
||||
|
||||
"k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/options"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
|
|
@ -296,7 +300,6 @@ func fakeCloudProviderFactory(io.Reader) (cloudprovider.Interface, error) {
|
|||
}
|
||||
|
||||
func TestKubeControllerManagerServingStatusz(t *testing.T) {
|
||||
|
||||
// authenticate to apiserver via bearer token
|
||||
token := "flwqkenfjasasdfmwerasd" // Fake token for testing.
|
||||
tokenFile, err := os.CreateTemp("", "kubeconfig")
|
||||
|
|
@ -349,131 +352,161 @@ users:
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string
|
||||
path string
|
||||
anonymous bool // to use the token or not
|
||||
wantErr bool
|
||||
wantSecureCode *int
|
||||
wantPaths []string
|
||||
wantBodyStr := "kube-controller-manager statusz\nWarning: This endpoint is not meant to be machine parseable"
|
||||
wantBodyJSON := &v1alpha1.Statusz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Statusz",
|
||||
APIVersion: "config.k8s.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "kube-controller-manager",
|
||||
},
|
||||
Paths: []string{"/configz", "/healthz", "/metrics"},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
acceptHeader string
|
||||
wantStatus int
|
||||
wantBodySub string // for text/plain
|
||||
wantJSON *v1alpha1.Statusz // for structured json
|
||||
}{
|
||||
{
|
||||
name: "serving /statusz",
|
||||
flags: []string{
|
||||
"--authentication-skip-lookup", // to survive inaccessible extensions-apiserver-authentication configmap
|
||||
"--authentication-kubeconfig", apiserverConfig.Name(),
|
||||
"--authorization-kubeconfig", apiserverConfig.Name(),
|
||||
"--authorization-always-allow-paths", "/statusz",
|
||||
"--kubeconfig", apiserverConfig.Name(),
|
||||
"--leader-elect=false",
|
||||
},
|
||||
path: "/statusz",
|
||||
anonymous: false,
|
||||
wantErr: false,
|
||||
wantSecureCode: ptr.To(http.StatusOK),
|
||||
wantPaths: []string{"/configz", "/healthz", "/metrics"},
|
||||
name: "text plain response",
|
||||
acceptHeader: "text/plain",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "structured json response",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz",
|
||||
wantStatus: http.StatusOK,
|
||||
wantJSON: wantBodyJSON,
|
||||
},
|
||||
{
|
||||
name: "no accept header (defaults to text)",
|
||||
acceptHeader: "",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "invalid accept header",
|
||||
acceptHeader: "application/xml",
|
||||
wantStatus: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "application/json without params",
|
||||
acceptHeader: "application/json",
|
||||
wantStatus: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "application/json with missing as",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
|
||||
wantStatus: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "wildcard accept header",
|
||||
acceptHeader: "*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "bad json header fall back wildcard",
|
||||
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Statusz,*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentStatusz, true)
|
||||
_, ctx := ktesting.NewTestContext(t)
|
||||
secureOptions, secureInfo, tearDownFn, err := kubeControllerManagerTester{}.StartTestServer(t, ctx, slices.Concat(tt.flags, []string{}))
|
||||
if tearDownFn != nil {
|
||||
defer tearDownFn()
|
||||
}
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("StartTestServer() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentStatusz, true)
|
||||
_, ctx := ktesting.NewTestContext(t)
|
||||
flags := []string{
|
||||
"--authentication-skip-lookup",
|
||||
"--authentication-kubeconfig", apiserverConfig.Name(),
|
||||
"--authorization-kubeconfig", apiserverConfig.Name(),
|
||||
"--authorization-always-allow-paths", "/statusz",
|
||||
"--kubeconfig", apiserverConfig.Name(),
|
||||
"--leader-elect=false",
|
||||
}
|
||||
secureOptions, secureInfo, tearDownFn, err := kubeControllerManagerTester{}.StartTestServer(t, ctx, flags)
|
||||
if tearDownFn != nil {
|
||||
defer tearDownFn()
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("StartTestServer() error = %v", err)
|
||||
}
|
||||
if secureInfo == nil {
|
||||
t.Fatalf("SecureServing not enabled")
|
||||
}
|
||||
_, port, err := net.SplitHostPort(secureInfo.Listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("could not get host and port from %s : %v", secureInfo.Listener.Addr().String(), err)
|
||||
}
|
||||
url := fmt.Sprintf("https://127.0.0.1:%s/statusz", port)
|
||||
|
||||
// read self-signed server cert disk
|
||||
pool := x509.NewCertPool()
|
||||
serverCertPath := path.Join(secureOptions.ServerCert.CertDirectory, secureOptions.ServerCert.PairName+".crt")
|
||||
serverCert, err := os.ReadFile(serverCertPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read component server cert %q: %v", serverCertPath, err)
|
||||
}
|
||||
pool.AppendCertsFromPEM(serverCert)
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: pool,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want, got := tt.wantSecureCode != nil, secureInfo != nil; want != got {
|
||||
t.Errorf("SecureServing enabled: expected=%v got=%v", want, got)
|
||||
} else if want {
|
||||
// only interested on the port, because we are using always localhost
|
||||
_, port, err := net.SplitHostPort(secureInfo.Listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("could not get host and port from %s : %v", secureInfo.Listener.Addr().String(), err)
|
||||
}
|
||||
// use IPv4 because the self-signed cert does not support [::]
|
||||
url := fmt.Sprintf("https://127.0.0.1:%s%s", port, tt.path)
|
||||
req.Header.Set("Accept", tc.acceptHeader)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Token %s", token))
|
||||
r, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to GET /statusz: %v", err)
|
||||
}
|
||||
|
||||
// read self-signed server cert disk
|
||||
pool := x509.NewCertPool()
|
||||
serverCertPath := path.Join(secureOptions.ServerCert.CertDirectory, secureOptions.ServerCert.PairName+".crt")
|
||||
serverCert, err := os.ReadFile(serverCertPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read component server cert %q: %v", serverCertPath, err)
|
||||
}
|
||||
pool.AppendCertsFromPEM(serverCert)
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: pool,
|
||||
},
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: tr}
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
req.Header.Set("Accept", "text/plain")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !tt.anonymous {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Token %s", token))
|
||||
}
|
||||
r, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to GET %s from component: %v", tt.path, err)
|
||||
}
|
||||
if err = r.Body.Close(); err != nil {
|
||||
t.Fatalf("failed to close response body: %v", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read response body: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := r.Body.Close(); err != nil {
|
||||
t.Fatalf("Error closing response body: %v", err)
|
||||
}
|
||||
}()
|
||||
if r.StatusCode != tc.wantStatus {
|
||||
t.Fatalf("want status %d, got %d", tc.wantStatus, r.StatusCode)
|
||||
}
|
||||
|
||||
if got, expected := r.StatusCode, *tt.wantSecureCode; got != expected {
|
||||
t.Fatalf("expected http %d at %s of component, got: %d", expected, tt.path, got)
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
|
||||
if !strings.Contains(bodyStr, "Paths") {
|
||||
t.Error("response does not contain Paths section")
|
||||
}
|
||||
|
||||
var foundPathsRaw []string
|
||||
for line := range strings.SplitSeq(bodyStr, "\n") {
|
||||
if strings.HasPrefix(line, "Paths") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) > 1 {
|
||||
foundPathsRaw = parts[1:] // Skip "Paths" label
|
||||
}
|
||||
break
|
||||
if tc.wantStatus == http.StatusOK {
|
||||
if tc.wantBodySub != "" {
|
||||
if !strings.Contains(string(body), tc.wantBodySub) {
|
||||
t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodySub, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
expectedPaths := tt.wantPaths
|
||||
|
||||
foundPathsSet := make(map[string]struct{})
|
||||
for _, p := range foundPathsRaw {
|
||||
foundPathsSet[p] = struct{}{}
|
||||
}
|
||||
|
||||
expectedPathsSet := make(map[string]struct{})
|
||||
for _, p := range expectedPaths {
|
||||
expectedPathsSet[p] = struct{}{}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(foundPathsSet, expectedPathsSet) {
|
||||
t.Errorf("path mismatch:\n- want: %v\n- got: %v", expectedPaths, foundPathsRaw)
|
||||
if tc.wantJSON != nil {
|
||||
var got v1alpha1.Statusz
|
||||
if err := json.Unmarshal(body, &got); err != nil {
|
||||
t.Fatalf("error unmarshalling JSON: %v", err)
|
||||
}
|
||||
// Only check static fields, since others are dynamic
|
||||
if got.TypeMeta != tc.wantJSON.TypeMeta {
|
||||
t.Errorf("TypeMeta mismatch: want %+v, got %+v", tc.wantJSON.TypeMeta, got.TypeMeta)
|
||||
}
|
||||
if got.ObjectMeta.Name != tc.wantJSON.ObjectMeta.Name {
|
||||
t.Errorf("ObjectMeta.Name mismatch: want %q, got %q", tc.wantJSON.ObjectMeta.Name, got.ObjectMeta.Name)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantJSON.Paths, got.Paths); diff != "" {
|
||||
t.Errorf("Paths mismatch (-want,+got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue