mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-06-11 01:41:54 -04:00
Merge pull request #134995 from yongruilin/flagz-kk-structure
[KEP-4828] Flagz versioned structured response
This commit is contained in:
commit
5fd9cefd95
43 changed files with 1311 additions and 447 deletions
|
|
@ -34,6 +34,7 @@ import (
|
|||
genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/egressselector"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/apiserver/pkg/util/notfoundhandler"
|
||||
|
|
@ -51,7 +52,6 @@ import (
|
|||
"k8s.io/component-base/term"
|
||||
utilversion "k8s.io/component-base/version"
|
||||
"k8s.io/component-base/version/verflag"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/klog/v2"
|
||||
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
|
||||
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
serveroptions "k8s.io/apiserver/pkg/server/options"
|
||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||
"k8s.io/apiserver/pkg/storageversion"
|
||||
|
|
@ -60,7 +61,6 @@ import (
|
|||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
logsapi "k8s.io/component-base/logs/api/v1"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kube-aggregator/pkg/apiserver"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ package config
|
|||
|
||||
import (
|
||||
apiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/record"
|
||||
basecompatibility "k8s.io/component-base/compatibility"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
kubectrlmgrconfig "k8s.io/kubernetes/pkg/controller/apis/config"
|
||||
"time"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/mux"
|
||||
"k8s.io/apiserver/pkg/server/statusz"
|
||||
|
|
@ -67,7 +68,6 @@ import (
|
|||
utilversion "k8s.io/component-base/version"
|
||||
"k8s.io/component-base/version/verflag"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
genericcontrollermanager "k8s.io/controller-manager/app"
|
||||
"k8s.io/controller-manager/controller"
|
||||
"k8s.io/controller-manager/pkg/clientbuilder"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import (
|
|||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
apiserveroptions "k8s.io/apiserver/pkg/server/options"
|
||||
"k8s.io/apiserver/pkg/util/compatibility"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
|
|
@ -45,7 +46,6 @@ import (
|
|||
"k8s.io/component-base/logs"
|
||||
logsapi "k8s.io/component-base/logs/api/v1"
|
||||
"k8s.io/component-base/metrics"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
cmoptions "k8s.io/controller-manager/options"
|
||||
kubectrlmgrconfigv1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1"
|
||||
kubecontrollerconfig "k8s.io/kubernetes/cmd/kube-controller-manager/app/config"
|
||||
|
|
|
|||
|
|
@ -28,11 +28,11 @@ import (
|
|||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
logsapi "k8s.io/component-base/logs/api/v1"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kube-proxy/config/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/cluster/ports"
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import (
|
|||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/mux"
|
||||
"k8s.io/apiserver/pkg/server/routes"
|
||||
|
|
@ -62,7 +63,6 @@ import (
|
|||
"k8s.io/component-base/version"
|
||||
"k8s.io/component-base/version/verflag"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
nodeutil "k8s.io/component-helpers/node/util"
|
||||
"k8s.io/klog/v2"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"time"
|
||||
|
||||
apiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
"k8s.io/client-go/dynamic/dynamicinformer"
|
||||
"k8s.io/client-go/informers"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
|
|
@ -27,7 +28,6 @@ import (
|
|||
"k8s.io/client-go/tools/events"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
basecompatibility "k8s.io/component-base/compatibility"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
kubeschedulerconfig "k8s.io/kubernetes/pkg/scheduler/apis/config"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
apiserveroptions "k8s.io/apiserver/pkg/server/options"
|
||||
"k8s.io/apiserver/pkg/util/compatibility"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
|
|
@ -47,7 +48,6 @@ import (
|
|||
logsapi "k8s.io/component-base/logs/api/v1"
|
||||
"k8s.io/component-base/metrics"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/klog/v2"
|
||||
schedulerappconfig "k8s.io/kubernetes/cmd/kube-scheduler/app/config"
|
||||
"k8s.io/kubernetes/pkg/scheduler"
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import (
|
|||
apirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/server"
|
||||
genericfilters "k8s.io/apiserver/pkg/server/filters"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/mux"
|
||||
"k8s.io/apiserver/pkg/server/routes"
|
||||
|
|
@ -61,7 +62,6 @@ import (
|
|||
utilversion "k8s.io/component-base/version"
|
||||
"k8s.io/component-base/version/verflag"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/klog/v2"
|
||||
schedulerserverconfig "k8s.io/kubernetes/cmd/kube-scheduler/app/config"
|
||||
"k8s.io/kubernetes/cmd/kube-scheduler/app/options"
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
|
|
@ -87,7 +88,6 @@ import (
|
|||
"k8s.io/component-base/version"
|
||||
"k8s.io/component-base/version/verflag"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
nodeutil "k8s.io/component-helpers/node/util"
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
kubeletconfigv1beta1 "k8s.io/kubelet/config/v1beta1"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import (
|
|||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
peerreconcilers "k8s.io/apiserver/pkg/reconcilers"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
genericoptions "k8s.io/apiserver/pkg/server/options"
|
||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||
"k8s.io/client-go/util/keyutil"
|
||||
|
|
@ -34,7 +35,6 @@ import (
|
|||
"k8s.io/component-base/logs"
|
||||
logsapi "k8s.io/component-base/logs/api/v1"
|
||||
"k8s.io/component-base/metrics"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/klog/v2"
|
||||
netutil "k8s.io/utils/net"
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import (
|
|||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
|
|
@ -43,7 +44,6 @@ import (
|
|||
cliflag "k8s.io/component-base/cli/flag"
|
||||
logsapi "k8s.io/component-base/logs/api/v1"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/klog/v2"
|
||||
controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver/options"
|
||||
"k8s.io/kubernetes/test/utils/ktesting"
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ import (
|
|||
genericregistry "k8s.io/apiserver/pkg/registry/generic"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
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/flagz"
|
||||
"k8s.io/component-helpers/apimachinery/lease"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/clock"
|
||||
|
|
|
|||
58
pkg/generated/openapi/zz_generated.openapi.go
generated
58
pkg/generated/openapi/zz_generated.openapi.go
generated
|
|
@ -90,7 +90,8 @@ 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"
|
||||
apiv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
|
||||
statuszapiv1alpha1 "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"
|
||||
|
|
@ -1339,7 +1340,8 @@ 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),
|
||||
apiv1alpha1.Flagz{}.OpenAPIModelName(): schema_server_flagz_api_v1alpha1_Flagz(ref),
|
||||
statuszapiv1alpha1.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),
|
||||
|
|
@ -64936,6 +64938,58 @@ func schema_pkg_apis_audit_v1_PolicyRule(ref common.ReferenceCallback) common.Op
|
|||
}
|
||||
}
|
||||
|
||||
func schema_server_flagz_api_v1alpha1_Flagz(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Flagz is the structured response for the /flagz 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()),
|
||||
},
|
||||
},
|
||||
"flags": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Flags contains the command-line flags and their values. The keys are the flag names and the values are the flag values, possibly with confidential values redacted.",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
metav1.ObjectMeta{}.OpenAPIModelName()},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_server_statusz_api_v1alpha1_Statusz(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import (
|
|||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
coreinformersv1 "k8s.io/client-go/informers/core/v1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
|
|
@ -69,7 +70,6 @@ import (
|
|||
"k8s.io/client-go/util/certificate"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/component-helpers/apimachinery/lease"
|
||||
resourcehelper "k8s.io/component-helpers/resource"
|
||||
internalapi "k8s.io/cri-api/pkg/apis"
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ import (
|
|||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
"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/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import (
|
|||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/httplog"
|
||||
"k8s.io/apiserver/pkg/server/routes"
|
||||
|
|
@ -70,7 +71,6 @@ import (
|
|||
"k8s.io/component-base/metrics/legacyregistry"
|
||||
"k8s.io/component-base/metrics/prometheus/slis"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
"k8s.io/cri-client/pkg/util"
|
||||
podresourcesapi "k8s.io/kubelet/pkg/apis/podresources/v1"
|
||||
|
|
|
|||
|
|
@ -57,11 +57,11 @@ import (
|
|||
"k8s.io/kubernetes/test/utils/ktesting"
|
||||
|
||||
// Do some initialization to decode the query parameters correctly.
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
zpagesfeatures "k8s.io/component-base/zpages/features"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/kubelet/pkg/cri/streaming"
|
||||
"k8s.io/kubelet/pkg/cri/streaming/portforward"
|
||||
remotecommandserver "k8s.io/kubelet/pkg/cri/streaming/remotecommand"
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import (
|
|||
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||
"k8s.io/apiserver/pkg/server/egressselector"
|
||||
genericfilters "k8s.io/apiserver/pkg/server/filters"
|
||||
"k8s.io/apiserver/pkg/server/flagz"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/routes"
|
||||
"k8s.io/apiserver/pkg/server/routine"
|
||||
|
|
@ -80,7 +81,6 @@ import (
|
|||
"k8s.io/component-base/metrics/features"
|
||||
"k8s.io/component-base/metrics/prometheus/slis"
|
||||
"k8s.io/component-base/tracing"
|
||||
"k8s.io/component-base/zpages/flagz"
|
||||
"k8s.io/klog/v2"
|
||||
openapicommon "k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
|
|
|
|||
7
staging/src/k8s.io/apiserver/pkg/server/flagz/OWNERS
Normal file
7
staging/src/k8s.io/apiserver/pkg/server/flagz/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/flagz/api/OWNERS
Normal file
11
staging/src/k8s.io/apiserver/pkg/server/flagz/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.flagz.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,
|
||||
&Flagz{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
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
|
||||
// Flagz is the structured response for the /flagz endpoint.
|
||||
type Flagz struct {
|
||||
// TypeMeta is the type metadata for the object.
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// Standard object's metadata.
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Flags contains the command-line flags and their values.
|
||||
// The keys are the flag names and the values are the flag values,
|
||||
// possibly with confidential values redacted.
|
||||
// +optional
|
||||
Flags map[string]string `json:"flags,omitempty"`
|
||||
}
|
||||
59
staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/zz_generated.deepcopy.go
generated
Normal file
59
staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/zz_generated.deepcopy.go
generated
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
//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 *Flagz) DeepCopyInto(out *Flagz) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
if in.Flags != nil {
|
||||
in, out := &in.Flags, &out.Flags
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Flagz.
|
||||
func (in *Flagz) DeepCopy() *Flagz {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Flagz)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Flagz) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
27
staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/zz_generated.model_name.go
generated
Normal file
27
staging/src/k8s.io/apiserver/pkg/server/flagz/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 Flagz) OpenAPIModelName() string {
|
||||
return "io.k8s.apiserver.pkg.server.flagz.api.v1alpha1.Flagz"
|
||||
}
|
||||
198
staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go
Normal file
198
staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package flagz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
v1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
|
||||
"k8s.io/apiserver/pkg/server/flagz/negotiate"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultFlagzPath = "/flagz"
|
||||
Kind = "Flagz"
|
||||
GroupName = "config.k8s.io"
|
||||
Version = "v1alpha1"
|
||||
)
|
||||
|
||||
type mux interface {
|
||||
Handle(path string, handler http.Handler)
|
||||
}
|
||||
|
||||
// Install installs the flagz endpoint to the given mux.
|
||||
func Install(m mux, componentName string, flagReader Reader, opts ...Option) {
|
||||
reg := ®istry{
|
||||
reader: flagReader,
|
||||
deprecatedVersionsMap: map[string]bool{},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(reg)
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(v1alpha1.AddToScheme(scheme))
|
||||
codecFactory := serializer.NewCodecFactory(
|
||||
scheme,
|
||||
serializer.WithSerializer(func(_ runtime.ObjectCreater, _ runtime.ObjectTyper) runtime.SerializerInfo {
|
||||
textSerializer := flagzTextSerializer{componentName, reg.reader}
|
||||
return runtime.SerializerInfo{
|
||||
MediaType: "text/plain",
|
||||
MediaTypeType: "text",
|
||||
MediaTypeSubType: "plain",
|
||||
EncodesAsText: true,
|
||||
Serializer: textSerializer,
|
||||
PrettySerializer: textSerializer,
|
||||
}
|
||||
}),
|
||||
)
|
||||
m.Handle(DefaultFlagzPath, handleFlagz(componentName, reg, codecFactory, negotiate.FlagzEndpointRestrictions{}))
|
||||
}
|
||||
|
||||
func handleFlagz(componentName string, reg *registry, serializer runtime.NegotiatedSerializer, restrictions negotiate.FlagzEndpointRestrictions) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
obj := flagz(componentName, reg.reader)
|
||||
acceptHeader := r.Header.Get("Accept")
|
||||
if strings.TrimSpace(acceptHeader) == "" {
|
||||
writePlainTextResponse(obj, serializer, w, reg)
|
||||
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()
|
||||
if reg.deprecatedVersions()[targetGV.Version] {
|
||||
w.Header().Set("Warning", `299 - "This version of the flagz endpoint is deprecated. Please use a newer version."`)
|
||||
}
|
||||
case "text/plain":
|
||||
writePlainTextResponse(obj, serializer, w, reg)
|
||||
return
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func writePlainTextResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, w http.ResponseWriter, reg *registry) {
|
||||
reg.cachedPlainTextResponseLock.Lock()
|
||||
defer reg.cachedPlainTextResponseLock.Unlock()
|
||||
if reg.cachedPlainTextResponse != nil {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
if _, err := w.Write(reg.cachedPlainTextResponse); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("error writing cached flagz as text/plain: %w", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
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
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
if err := textSerializer.Encode(obj, &buf); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("error encoding flagz as text/plain: %w", err))
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
reg.cachedPlainTextResponse = []byte(buf.String())
|
||||
if _, err := w.Write(reg.cachedPlainTextResponse); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("error writing flagz as text/plain: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
func writeResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, targetGV schema.GroupVersion, restrictions negotiate.FlagzEndpointRestrictions, w http.ResponseWriter, r *http.Request) {
|
||||
responsewriters.WriteObjectNegotiated(
|
||||
serializer,
|
||||
restrictions,
|
||||
targetGV,
|
||||
w,
|
||||
r,
|
||||
http.StatusOK,
|
||||
obj,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func flagz(componentName string, flagReader Reader) *v1alpha1.Flagz {
|
||||
flags := flagReader.GetFlagz()
|
||||
return &v1alpha1.Flagz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: Kind,
|
||||
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: componentName,
|
||||
},
|
||||
Flags: flags,
|
||||
}
|
||||
}
|
||||
293
staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go
Normal file
293
staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package flagz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/spf13/pflag"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
)
|
||||
|
||||
const wantTmpl = `
|
||||
%s flagz
|
||||
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.
|
||||
`
|
||||
|
||||
func TestHandleFlagz(t *testing.T) {
|
||||
fakeFlagName := "test-flag"
|
||||
fakeFlagValue := "test-value"
|
||||
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
fs.String(fakeFlagName, fakeFlagValue, "usage")
|
||||
fakeReader := NamedFlagSetsReader{
|
||||
FlagSets: cliflag.NamedFlagSets{
|
||||
FlagSets: map[string]*pflag.FlagSet{
|
||||
"test": fs,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
acceptHeader string
|
||||
componentName string
|
||||
registry *registry
|
||||
wantStatusCode int
|
||||
wantBody string
|
||||
wantJSONBody *v1alpha1.Flagz
|
||||
wantWarning bool
|
||||
}{
|
||||
{
|
||||
name: "valid request for text/plain",
|
||||
acceptHeader: "text/plain",
|
||||
componentName: "test-server",
|
||||
registry: ®istry{
|
||||
reader: fakeReader,
|
||||
deprecatedVersionsMap: map[string]bool{},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantBody: fmt.Sprintf(
|
||||
wantTmpl,
|
||||
"test-server",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "valid request for v1alpha1",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
|
||||
componentName: "test-server",
|
||||
registry: ®istry{
|
||||
reader: fakeReader,
|
||||
deprecatedVersionsMap: map[string]bool{},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantJSONBody: &v1alpha1.Flagz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: Kind,
|
||||
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-server",
|
||||
},
|
||||
Flags: map[string]string{
|
||||
fakeFlagName: fakeFlagValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deprecated version request",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
|
||||
componentName: "test-server",
|
||||
registry: ®istry{
|
||||
reader: fakeReader,
|
||||
deprecatedVersionsMap: map[string]bool{"v1alpha1": true},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantJSONBody: &v1alpha1.Flagz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: Kind,
|
||||
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-server",
|
||||
},
|
||||
Flags: map[string]string{
|
||||
fakeFlagName: fakeFlagValue,
|
||||
},
|
||||
},
|
||||
wantWarning: true,
|
||||
},
|
||||
{
|
||||
name: "no accept header falls back to text/plain",
|
||||
acceptHeader: "",
|
||||
componentName: "test-server",
|
||||
registry: ®istry{
|
||||
reader: fakeReader,
|
||||
deprecatedVersionsMap: map[string]bool{},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantBody: fmt.Sprintf(
|
||||
wantTmpl,
|
||||
"test-server",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "wildcard accept header falls back to text/plain",
|
||||
acceptHeader: "*/*",
|
||||
componentName: "test-server",
|
||||
registry: ®istry{
|
||||
reader: fakeReader,
|
||||
deprecatedVersionsMap: map[string]bool{},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantBody: fmt.Sprintf(
|
||||
wantTmpl,
|
||||
"test-server",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad json header falls back to text/plain with wildcard",
|
||||
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*",
|
||||
componentName: "test-server",
|
||||
registry: ®istry{
|
||||
reader: fakeReader,
|
||||
deprecatedVersionsMap: map[string]bool{},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantBody: fmt.Sprintf(
|
||||
wantTmpl,
|
||||
"test-server",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "unsupported accept header",
|
||||
acceptHeader: "application/xml",
|
||||
componentName: "test-server",
|
||||
registry: ®istry{
|
||||
reader: fakeReader,
|
||||
deprecatedVersionsMap: map[string]bool{},
|
||||
},
|
||||
wantStatusCode: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "unsupported application/json without params",
|
||||
acceptHeader: "application/json",
|
||||
componentName: "test-server",
|
||||
registry: ®istry{
|
||||
reader: fakeReader,
|
||||
deprecatedVersionsMap: map[string]bool{},
|
||||
},
|
||||
wantStatusCode: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "unsupported application/json with missing params",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
|
||||
componentName: "test-server",
|
||||
registry: ®istry{
|
||||
reader: fakeReader,
|
||||
deprecatedVersionsMap: map[string]bool{},
|
||||
},
|
||||
wantStatusCode: http.StatusNotAcceptable,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
var opts []Option
|
||||
var capturedReg *registry
|
||||
opts = append(opts, func(reg *registry) {
|
||||
capturedReg = reg
|
||||
if tt.registry != nil {
|
||||
reg.deprecatedVersionsMap = tt.registry.deprecatedVersionsMap
|
||||
}
|
||||
})
|
||||
Install(mux, tt.componentName, fakeReader, opts...)
|
||||
// Assign the captured registry to tt.registry for consistency with existing test logic
|
||||
tt.registry = capturedReg
|
||||
|
||||
path := "/flagz"
|
||||
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.Flagz
|
||||
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); 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 !strings.Contains(string(w.Body.String()), tt.wantBody) {
|
||||
t.Errorf("Unexpected response body:\n- want: %s\n- got: %s", tt.wantBody, string(w.Body.String()))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
fakeFlagName := "test-flag"
|
||||
fakeFlagValue := "test-value"
|
||||
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
fs.String(fakeFlagName, fakeFlagValue, "usage")
|
||||
fakeReader := NamedFlagSetsReader{
|
||||
FlagSets: cliflag.NamedFlagSets{
|
||||
FlagSets: map[string]*pflag.FlagSet{
|
||||
"test": fs,
|
||||
},
|
||||
},
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
var capturedReg *registry
|
||||
Install(mux, "test-server", fakeReader, func(reg *registry) {
|
||||
capturedReg = reg
|
||||
})
|
||||
|
||||
path := "/flagz"
|
||||
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")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("want status code: %v, got: %v", http.StatusOK, w.Code)
|
||||
}
|
||||
if capturedReg.cachedPlainTextResponse == nil {
|
||||
t.Fatalf("cached response should not be nil")
|
||||
}
|
||||
cached := capturedReg.cachedPlainTextResponse
|
||||
fs.String("new-flag", "new-value", "usage")
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("want status code: %v, got: %v", http.StatusOK, w.Code)
|
||||
}
|
||||
if diff := cmp.Diff(cached, capturedReg.cachedPlainTextResponse); diff != "" {
|
||||
t.Errorf("Unexpected diff on cached response (-want,+got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
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"
|
||||
)
|
||||
|
||||
// FlagzEndpointRestrictions implements content negotiation restrictions for the z-pages.
|
||||
// It is used to validate and restrict which GroupVersionKinds are allowed for structured responses.
|
||||
type FlagzEndpointRestrictions struct{}
|
||||
|
||||
// AllowsMediaTypeTransform checks if the provided GVK is supported for structured z-page responses.
|
||||
func (FlagzEndpointRestrictions) AllowsMediaTypeTransform(mimeType string, mimeSubType string, gvk *schema.GroupVersionKind) bool {
|
||||
if mimeType == "text" && mimeSubType == "plain" {
|
||||
return gvk == nil
|
||||
}
|
||||
return isStructured(gvk)
|
||||
}
|
||||
|
||||
func (FlagzEndpointRestrictions) AllowsServerVersion(string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (FlagzEndpointRestrictions) AllowsStreamSchema(s string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isStructured(gvk *schema.GroupVersionKind) bool {
|
||||
if gvk != nil {
|
||||
if gvk.Group == "config.k8s.io" && gvk.Version == "v1alpha1" {
|
||||
if gvk.Kind == "Flagz" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
39
staging/src/k8s.io/apiserver/pkg/server/flagz/registry.go
Normal file
39
staging/src/k8s.io/apiserver/pkg/server/flagz/registry.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
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 flagz
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type registry struct {
|
||||
// reader is a Reader where we can get the flags.
|
||||
reader Reader
|
||||
// deprecatedVersionsMap is a map of deprecated flagz versions.
|
||||
deprecatedVersionsMap map[string]bool
|
||||
// cachedPlainTextResponse is a cached response of the flagz endpoint.
|
||||
cachedPlainTextResponse []byte
|
||||
// cachedPlainTextResponseLock is a lock for the cachedPlainTextResponse.
|
||||
cachedPlainTextResponseLock sync.Mutex
|
||||
}
|
||||
|
||||
// Option is a function to configure registry.
|
||||
type Option func(reg *registry)
|
||||
|
||||
func (r *registry) deprecatedVersions() map[string]bool {
|
||||
return r.deprecatedVersionsMap
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package flagz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"sort"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
delimiters = []string{":", ": ", "=", " "}
|
||||
)
|
||||
|
||||
const headerFmt = `
|
||||
%s flagz
|
||||
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.
|
||||
`
|
||||
|
||||
// flagzTextSerializer implements runtime.Serializer for text/plain output.
|
||||
type flagzTextSerializer struct {
|
||||
componentName string
|
||||
flagReader Reader
|
||||
}
|
||||
|
||||
// Encode writes the flagz information in plain text format to the given writer, using the provided obj.
|
||||
func (s flagzTextSerializer) 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))
|
||||
separator := delimiters[randomIndex]
|
||||
|
||||
flags := s.flagReader.GetFlagz()
|
||||
var sortedKeys []string
|
||||
for key := range flags {
|
||||
sortedKeys = append(sortedKeys, key)
|
||||
}
|
||||
|
||||
sort.Strings(sortedKeys)
|
||||
for _, key := range sortedKeys {
|
||||
if _, err := fmt.Fprintf(w, "%s%s%s\n", key, separator, flags[key]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decode is not supported for text/plain serialization.
|
||||
func (s flagzTextSerializer) 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 flagzTextSerializer) Identifier() runtime.Identifier {
|
||||
return runtime.Identifier("flagzTextSerializer")
|
||||
}
|
||||
|
|
@ -43,7 +43,6 @@ func (StatuszEndpointRestrictions) AllowsStreamSchema(s string) bool {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ require (
|
|||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/moby/term v0.5.0
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/prometheus/common v0.66.1
|
||||
|
|
@ -59,6 +58,7 @@ 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.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
|
|
|
|||
|
|
@ -1,102 +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 flagz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"k8s.io/component-base/zpages/httputil"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultFlagzPath = "/flagz"
|
||||
|
||||
flagzHeaderFmt = `
|
||||
%s flags
|
||||
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
delimiters = []string{":", ": ", "=", " "}
|
||||
)
|
||||
|
||||
type registry struct {
|
||||
response bytes.Buffer
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
type mux interface {
|
||||
Handle(path string, handler http.Handler)
|
||||
}
|
||||
|
||||
func Install(m mux, componentName string, flagReader Reader) {
|
||||
var reg registry
|
||||
reg.installHandler(m, componentName, flagReader)
|
||||
}
|
||||
|
||||
func (reg *registry) installHandler(m mux, componentName string, flagReader Reader) {
|
||||
m.Handle(DefaultFlagzPath, reg.handleFlags(componentName, flagReader))
|
||||
}
|
||||
|
||||
func (reg *registry) handleFlags(componentName string, flagReader Reader) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !httputil.AcceptableMediaType(r) {
|
||||
http.Error(w, httputil.ErrUnsupportedMediaType.Error(), http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
|
||||
reg.once.Do(func() {
|
||||
fmt.Fprintf(®.response, flagzHeaderFmt, componentName)
|
||||
if flagReader == nil {
|
||||
klog.Error("received nil flagReader")
|
||||
return
|
||||
}
|
||||
|
||||
randomIndex := rand.Intn(len(delimiters))
|
||||
separator := delimiters[randomIndex]
|
||||
// Randomize the delimiter for printing to prevent scraping of the response.
|
||||
printSortedFlags(®.response, flagReader.GetFlagz(), separator)
|
||||
})
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, err := w.Write(reg.response.Bytes())
|
||||
if err != nil {
|
||||
klog.Errorf("error writing response: %v", err)
|
||||
http.Error(w, "error writing response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printSortedFlags(w io.Writer, flags map[string]string, separator string) {
|
||||
var sortedKeys []string
|
||||
for key := range flags {
|
||||
sortedKeys = append(sortedKeys, key)
|
||||
}
|
||||
|
||||
sort.Strings(sortedKeys)
|
||||
for _, key := range sortedKeys {
|
||||
fmt.Fprintf(w, "%s%s%s\n", key, separator, flags[key])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,126 +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 flagz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
)
|
||||
|
||||
const wantTmpl = `%s flags
|
||||
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.
|
||||
`
|
||||
|
||||
func TestFlagz(t *testing.T) {
|
||||
componentName := "test-server"
|
||||
delimiters = []string{"="}
|
||||
wantHeaderLines := strings.Split(fmt.Sprintf(wantTmpl, componentName), "\n")
|
||||
tests := []struct {
|
||||
name string
|
||||
header string
|
||||
flagzReader Reader
|
||||
wantStatus int
|
||||
wantResp []string
|
||||
}{
|
||||
{
|
||||
name: "nil flags",
|
||||
wantStatus: http.StatusOK,
|
||||
wantResp: wantHeaderLines,
|
||||
},
|
||||
{
|
||||
name: "unaccepted header",
|
||||
header: "some header",
|
||||
wantStatus: http.StatusNotAcceptable,
|
||||
},
|
||||
{
|
||||
name: "test flags",
|
||||
flagzReader: NamedFlagSetsReader{
|
||||
FlagSets: cliflag.NamedFlagSets{
|
||||
FlagSets: map[string]*pflag.FlagSet{
|
||||
"test": flagSet(t, map[string]flagValue{
|
||||
"test-flag-bar": {
|
||||
value: "test-value-bar",
|
||||
sensitive: false,
|
||||
},
|
||||
"test-flag-foo": {
|
||||
value: "test-value-foo",
|
||||
sensitive: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantResp: append(wantHeaderLines,
|
||||
"test-flag-bar=test-value-bar",
|
||||
"test-flag-foo=test-value-foo",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
Install(mux, componentName, test.flagzReader)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", DefaultFlagzPath), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("case[%d] Unexpected error: %v", i, err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "text/plain; charset=utf-8")
|
||||
if test.header != "" {
|
||||
req.Header.Set("Accept", test.header)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, req)
|
||||
assert.Equal(t, test.wantStatus, w.Code, "case[%s] Expected status code %d, got %d", test.name, test.wantStatus, w.Code)
|
||||
|
||||
if test.wantStatus == http.StatusOK {
|
||||
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"), "case[%s] Incorrect Content-Type header", test.name)
|
||||
|
||||
gotLines := strings.Split(w.Body.String(), "\n")
|
||||
gotLines = trimEmptyLines(gotLines)
|
||||
sort.Strings(gotLines)
|
||||
|
||||
sort.Strings(test.wantResp)
|
||||
wantLines := trimEmptyLines(test.wantResp)
|
||||
|
||||
assert.Equal(t, wantLines, gotLines, "case[%s] Response body mismatch", test.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func trimEmptyLines(lines []string) []string {
|
||||
var result []string
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -1,54 +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 httputil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/munnerz/goautoneg"
|
||||
)
|
||||
|
||||
// ErrUnsupportedMediaType is the error returned when the request's
|
||||
// Accept header does not contain "text/plain".
|
||||
var ErrUnsupportedMediaType = fmt.Errorf("media type not acceptable, must be: text/plain")
|
||||
|
||||
// AcceptableMediaType checks if the request's Accept header contains
|
||||
// a supported media type with optional "charset=utf-8" parameter.
|
||||
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 == "*")
|
||||
}
|
||||
|
|
@ -1,74 +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 httputil
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ import (
|
|||
"k8s.io/kubernetes/test/integration/etcd"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
|
||||
flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
|
||||
v1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
|
||||
)
|
||||
|
||||
|
|
@ -135,7 +136,7 @@ func TestLivezAndReadyz(t *testing.T) {
|
|||
|
||||
func TestFlagz(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ComponentFlagz, true)
|
||||
testServerFlags := append(framework.DefaultTestServerFlags(), "--emulated-version=1.32")
|
||||
testServerFlags := append(framework.DefaultTestServerFlags(), "--v=2")
|
||||
server := kubeapiservertesting.StartTestServerOrDie(t, nil, testServerFlags, framework.SharedEtcd())
|
||||
defer server.TearDownFn()
|
||||
|
||||
|
|
@ -144,33 +145,107 @@ func TestFlagz(t *testing.T) {
|
|||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
res := client.CoreV1().RESTClient().Get().RequestURI("/flagz").Do(context.TODO())
|
||||
var status int
|
||||
res.StatusCode(&status)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("flagz/ should be healthy, got %v", status)
|
||||
wantBodyStr := "kube-apiserver flagz\nWarning: This endpoint is not meant to be machine parseable"
|
||||
wantBodyJSON := &flagzv1alpha1.Flagz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Flagz",
|
||||
APIVersion: "config.k8s.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "kube-apiserver",
|
||||
},
|
||||
}
|
||||
|
||||
expectedHeader := `
|
||||
kube-apiserver flags
|
||||
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.HasPrefix(raw, []byte(expectedHeader)) {
|
||||
t.Fatalf("Header mismatch!\nExpected:\n%s\n\nGot:\n%s", expectedHeader, string(raw))
|
||||
}
|
||||
found := false
|
||||
for _, line := range strings.Split(string(raw), "\n") {
|
||||
if strings.Contains(line, "emulated-version") && strings.Contains(line, "1.32") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("Expected flag --emulated-version=[1.32] to be reflected in /flagz output, got:\n%s", string(raw))
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
acceptHeader string
|
||||
wantStatus int
|
||||
wantBodySub string // for text/plain
|
||||
wantJSON *flagzv1alpha1.Flagz // 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=Flagz",
|
||||
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=Flagz,*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := client.CoreV1().RESTClient().Get().RequestURI("/flagz")
|
||||
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 flagzv1alpha1.Flagz
|
||||
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 got.Flags["v"] != "2" {
|
||||
t.Errorf("v mismatch: want %q, got %q", "2", got.Flags["v"])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import (
|
|||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
|
||||
"k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/util/retry"
|
||||
|
|
@ -43,10 +44,6 @@ import (
|
|||
)
|
||||
|
||||
func TestEndpointHandlers(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
||||
features.ComponentFlagz: true,
|
||||
})
|
||||
|
||||
server, configStr, _, err := startTestAPIServer(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start kube-apiserver server: %v", err)
|
||||
|
|
@ -126,16 +123,6 @@ func TestEndpointHandlers(t *testing.T) {
|
|||
useBrokenConfig: true,
|
||||
wantResponseCode: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "/flagz",
|
||||
path: "/flagz",
|
||||
requestHeader: map[string]string{"Accept": "text/plain"},
|
||||
wantResponseCode: http.StatusOK,
|
||||
wantResponseBodyRegx: `^\n` +
|
||||
`kube-scheduler flags\n` +
|
||||
`Warning: This endpoint is not meant to be machine parseable, ` +
|
||||
`has no formatting compatibility guarantees and is for debugging purposes only.`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -217,9 +204,10 @@ func TestEndpointHandlers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSchedulerStatusz(t *testing.T) {
|
||||
func TestSchedulerZPages(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
||||
features.ComponentStatusz: true,
|
||||
features.ComponentFlagz: true,
|
||||
})
|
||||
|
||||
server, configStr, _, err := startTestAPIServer(t)
|
||||
|
|
@ -242,7 +230,7 @@ func TestSchedulerStatusz(t *testing.T) {
|
|||
_, ctx := ktesting.NewTestContext(t)
|
||||
result, err := kubeschedulertesting.StartTestServer(
|
||||
t, ctx,
|
||||
[]string{"--kubeconfig", apiserverConfig.Name(), "--leader-elect=false", "--authorization-always-allow-paths=/statusz"},
|
||||
[]string{"--kubeconfig", apiserverConfig.Name(), "--leader-elect=false", "--authorization-always-allow-paths=/statusz,/flagz"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start kube-scheduler server: %v", err)
|
||||
|
|
@ -256,8 +244,8 @@ func TestSchedulerStatusz(t *testing.T) {
|
|||
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{
|
||||
statuszWantBodyStr := "kube-scheduler statusz\nWarning: This endpoint is not meant to be machine parseable"
|
||||
statuszWantBodyJSON := &v1alpha1.Statusz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Statusz",
|
||||
APIVersion: "config.k8s.io/v1alpha1",
|
||||
|
|
@ -265,10 +253,21 @@ func TestSchedulerStatusz(t *testing.T) {
|
|||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "kube-scheduler",
|
||||
},
|
||||
Paths: []string{"/configz", "/healthz", "/livez", "/metrics", "/readyz"},
|
||||
Paths: []string{"/configz", "/flagz", "/healthz", "/livez", "/metrics", "/readyz"},
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
flagzWantBodyStr := "kube-scheduler flagz\nWarning: This endpoint is not meant to be machine parseable"
|
||||
flagzWantBodyJSON := &flagzv1alpha1.Flagz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Flagz",
|
||||
APIVersion: "config.k8s.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "kube-scheduler",
|
||||
},
|
||||
}
|
||||
|
||||
statuszTestCases := []struct {
|
||||
name string
|
||||
acceptHeader string
|
||||
wantStatus int
|
||||
|
|
@ -279,19 +278,19 @@ func TestSchedulerStatusz(t *testing.T) {
|
|||
name: "text plain response",
|
||||
acceptHeader: "text/plain",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
wantBodySub: statuszWantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "structured json response",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz",
|
||||
wantStatus: http.StatusOK,
|
||||
wantJSON: wantBodyJSON,
|
||||
wantJSON: statuszWantBodyJSON,
|
||||
},
|
||||
{
|
||||
name: "no accept header (defaults to text)",
|
||||
acceptHeader: "",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
wantBodySub: statuszWantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "invalid accept header",
|
||||
|
|
@ -312,16 +311,72 @@ func TestSchedulerStatusz(t *testing.T) {
|
|||
name: "wildcard accept header",
|
||||
acceptHeader: "*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
wantBodySub: statuszWantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "bad json header fall back wildcard",
|
||||
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Statusz,*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
wantBodySub: statuszWantBodyStr,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
}
|
||||
|
||||
flagzTestCases := []struct {
|
||||
name string
|
||||
acceptHeader string
|
||||
wantStatus int
|
||||
wantBodySub string // for text/plain
|
||||
wantJSON *flagzv1alpha1.Flagz // for structured json
|
||||
}{
|
||||
{
|
||||
name: "text plain response",
|
||||
acceptHeader: "text/plain",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: flagzWantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "structured json response",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
|
||||
wantStatus: http.StatusOK,
|
||||
wantJSON: flagzWantBodyJSON,
|
||||
},
|
||||
{
|
||||
name: "no accept header (defaults to text)",
|
||||
acceptHeader: "",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: flagzWantBodyStr,
|
||||
},
|
||||
{
|
||||
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: flagzWantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "bad json header fall back wildcard",
|
||||
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: flagzWantBodyStr,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range statuszTestCases {
|
||||
t.Run("statusz_"+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)
|
||||
|
|
@ -371,6 +426,55 @@ func TestSchedulerStatusz(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, tc := range flagzTestCases {
|
||||
t.Run("flagz_"+tc.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, base+"/flagz", 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 /flagz: %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 flagzv1alpha1.Flagz
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make this a util function once there is a unified way to start a testing apiserver so that we can reuse it.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import (
|
|||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
|
||||
"k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
|
||||
|
||||
"k8s.io/apiserver/pkg/server"
|
||||
|
|
@ -299,7 +300,7 @@ func fakeCloudProviderFactory(io.Reader) (cloudprovider.Interface, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func TestKubeControllerManagerServingStatusz(t *testing.T) {
|
||||
func TestKubeControllerManagerServingZPages(t *testing.T) {
|
||||
// authenticate to apiserver via bearer token
|
||||
token := "flwqkenfjasasdfmwerasd" // Fake token for testing.
|
||||
tokenFile, err := os.CreateTemp("", "kubeconfig")
|
||||
|
|
@ -352,8 +353,8 @@ users:
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantBodyStr := "kube-controller-manager statusz\nWarning: This endpoint is not meant to be machine parseable"
|
||||
wantBodyJSON := &v1alpha1.Statusz{
|
||||
statuszWantBodyStr := "kube-controller-manager statusz\nWarning: This endpoint is not meant to be machine parseable"
|
||||
statuszWantBodyJSON := &v1alpha1.Statusz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Statusz",
|
||||
APIVersion: "config.k8s.io/v1alpha1",
|
||||
|
|
@ -361,10 +362,21 @@ users:
|
|||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "kube-controller-manager",
|
||||
},
|
||||
Paths: []string{"/configz", "/healthz", "/metrics"},
|
||||
Paths: []string{"/configz", "/flagz", "/healthz", "/metrics"},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
flagzWantBodyStr := "kube-controller-manager flagz\nWarning: This endpoint is not meant to be machine parseable"
|
||||
flagzWantBodyJSON := &flagzv1alpha1.Flagz{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Flagz",
|
||||
APIVersion: "config.k8s.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "kube-controller-manager",
|
||||
},
|
||||
}
|
||||
|
||||
statuszTestCases := []struct {
|
||||
name string
|
||||
acceptHeader string
|
||||
wantStatus int
|
||||
|
|
@ -375,19 +387,19 @@ users:
|
|||
name: "text plain response",
|
||||
acceptHeader: "text/plain",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
wantBodySub: statuszWantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "structured json response",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz",
|
||||
wantStatus: http.StatusOK,
|
||||
wantJSON: wantBodyJSON,
|
||||
wantJSON: statuszWantBodyJSON,
|
||||
},
|
||||
{
|
||||
name: "no accept header (defaults to text)",
|
||||
acceptHeader: "",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
wantBodySub: statuszWantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "invalid accept header",
|
||||
|
|
@ -408,23 +420,78 @@ users:
|
|||
name: "wildcard accept header",
|
||||
acceptHeader: "*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
wantBodySub: statuszWantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "bad json header fall back wildcard",
|
||||
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Statusz,*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: wantBodyStr,
|
||||
wantBodySub: statuszWantBodyStr,
|
||||
},
|
||||
}
|
||||
|
||||
flagzTestCases := []struct {
|
||||
name string
|
||||
acceptHeader string
|
||||
wantStatus int
|
||||
wantBodySub string // for text/plain
|
||||
wantJSON *flagzv1alpha1.Flagz // for structured json
|
||||
}{
|
||||
{
|
||||
name: "text plain response",
|
||||
acceptHeader: "text/plain",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: flagzWantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "structured json response",
|
||||
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
|
||||
wantStatus: http.StatusOK,
|
||||
wantJSON: flagzWantBodyJSON,
|
||||
},
|
||||
{
|
||||
name: "no accept header (defaults to text)",
|
||||
acceptHeader: "",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: flagzWantBodyStr,
|
||||
},
|
||||
{
|
||||
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: flagzWantBodyStr,
|
||||
},
|
||||
{
|
||||
name: "bad json header fall back wildcard",
|
||||
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBodySub: flagzWantBodyStr,
|
||||
},
|
||||
}
|
||||
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentStatusz, true)
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentFlagz, true)
|
||||
_, ctx := ktesting.NewTestContext(t)
|
||||
flags := []string{
|
||||
"--authentication-skip-lookup",
|
||||
"--authentication-kubeconfig", apiserverConfig.Name(),
|
||||
"--authorization-kubeconfig", apiserverConfig.Name(),
|
||||
"--authorization-always-allow-paths", "/statusz",
|
||||
"--authorization-always-allow-paths", "/statusz,/flagz",
|
||||
"--kubeconfig", apiserverConfig.Name(),
|
||||
"--leader-elect=false",
|
||||
}
|
||||
|
|
@ -442,7 +509,8 @@ users:
|
|||
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)
|
||||
statuszURL := fmt.Sprintf("https://127.0.0.1:%s/statusz", port)
|
||||
flagzURL := fmt.Sprintf("https://127.0.0.1:%s/flagz", port)
|
||||
|
||||
// read self-signed server cert disk
|
||||
pool := x509.NewCertPool()
|
||||
|
|
@ -459,9 +527,9 @@ users:
|
|||
}
|
||||
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)
|
||||
for _, tc := range statuszTestCases {
|
||||
t.Run("statusz_"+tc.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, statuszURL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -511,4 +579,54 @@ users:
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, tc := range flagzTestCases {
|
||||
t.Run("flagz_"+tc.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, flagzURL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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 /flagz: %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 flagzv1alpha1.Flagz
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue