rebase: bump k8s.io/kubernetes in the k8s-dependencies group

Bumps the k8s-dependencies group with 1 update: [k8s.io/kubernetes](https://github.com/kubernetes/kubernetes).

Updates `k8s.io/kubernetes` from 1.32.3 to 1.33.0
- [Release notes](https://github.com/kubernetes/kubernetes/releases)
- [Commits](https://github.com/kubernetes/kubernetes/compare/v1.32.3...v1.33.0)

---
updated-dependencies:
- dependency-name: k8s.io/kubernetes
  dependency-version: 1.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: k8s-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Niels de Vos <ndevos@ibm.com>
This commit is contained in:
dependabot[bot]
2025-04-28 22:16:28 +00:00
committed by mergify[bot]
parent 4147d5d15a
commit 51895f8619
699 changed files with 51590 additions and 17096 deletions

View File

@ -15,4 +15,4 @@ limitations under the License.
*/
// Package errors provides detailed error types for api field validation.
package errors // import "k8s.io/apimachinery/pkg/api/errors"
package errors

View File

@ -16,4 +16,4 @@ limitations under the License.
// Package meta provides functions for retrieving API metadata from objects
// belonging to the Kubernetes API
package meta // import "k8s.io/apimachinery/pkg/api/meta"
package meta

View File

@ -221,6 +221,9 @@ func extractList(obj runtime.Object, allocNew bool) ([]runtime.Object, error) {
if err != nil {
return nil, err
}
if items.IsNil() {
return nil, nil
}
list := make([]runtime.Object, items.Len())
if len(list) == 0 {
return list, nil

View File

@ -0,0 +1,56 @@
/*
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 operation
import "k8s.io/apimachinery/pkg/util/sets"
// Operation provides contextual information about a validation request and the API
// operation being validated.
// This type is intended for use with generate validation code and may be enhanced
// in the future to include other information needed to validate requests.
type Operation struct {
// Type is the category of operation being validated. This does not
// differentiate between HTTP verbs like PUT and PATCH, but rather merges
// those into a single "Update" category.
Type Type
// Options declare the options enabled for validation.
//
// Options should be set according to a resource validation strategy before validation
// is performed, and must be treated as read-only during validation.
//
// Options are identified by string names. Option string names may match the name of a feature
// gate, in which case the presence of the name in the set indicates that the feature is
// considered enabled for the resource being validated. Note that a resource may have a
// feature enabled even when the feature gate is disabled. This can happen when feature is
// already in-use by a resource, often because the feature gate was enabled when the
// resource first began using the feature.
//
// Unset options are disabled/false.
Options sets.Set[string]
}
// Code is the request operation to be validated.
type Type uint32
const (
// Create indicates the request being validated is for a resource create operation.
Create Type = iota
// Update indicates the request being validated is for a resource update operation.
Update
)

View File

@ -15,4 +15,4 @@ limitations under the License.
*/
// Package validation contains generic api type validation functions.
package validation // import "k8s.io/apimachinery/pkg/api/validation"
package validation

View File

@ -82,7 +82,7 @@ func maskTrailingDash(name string) string {
func ValidateNonnegativeField(value int64, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if value < 0 {
allErrs = append(allErrs, field.Invalid(fldPath, value, IsNegativeErrorMsg))
allErrs = append(allErrs, field.Invalid(fldPath, value, IsNegativeErrorMsg).WithOrigin("minimum"))
}
return allErrs
}

View File

@ -17,4 +17,4 @@ limitations under the License.
// +k8s:deepcopy-gen=package
// +k8s:conversion-gen=k8s.io/apimachinery/pkg/apis/meta/v1
package internalversion // import "k8s.io/apimachinery/pkg/apis/meta/internalversion"
package internalversion

View File

@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package scheme // import "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
package scheme

View File

@ -41,8 +41,6 @@ type ListOptions struct {
// assume bookmarks are returned at any specific interval, nor may they
// assume the server will send any BOOKMARK event during a session.
// If this is not a watch, this field is ignored.
// If the feature gate WatchBookmarks is not enabled in apiserver,
// this field is ignored.
AllowWatchBookmarks bool
// resourceVersion sets a constraint on what resource versions a request may be served from.
// See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for

View File

@ -21,4 +21,4 @@ limitations under the License.
// +groupName=meta.k8s.io
package v1 // import "k8s.io/apimachinery/pkg/apis/meta/v1"
package v1

View File

@ -20,21 +20,22 @@ limitations under the License.
package v1
import (
"math/rand"
"time"
fuzz "github.com/google/gofuzz"
"sigs.k8s.io/randfill"
)
// Fuzz satisfies fuzz.Interface.
func (t *MicroTime) Fuzz(c fuzz.Continue) {
// Fuzz satisfies randfill.SimpleSelfFiller.
func (t *MicroTime) RandFill(r *rand.Rand) {
if t == nil {
return
}
// Allow for about 1000 years of randomness. Accurate to a tenth of
// micro second. Leave off nanoseconds because JSON doesn't
// represent them so they can't round-trip properly.
t.Time = time.Unix(c.Rand.Int63n(1000*365*24*60*60), 1000*c.Rand.Int63n(1000000))
t.Time = time.Unix(r.Int63n(1000*365*24*60*60), 1000*r.Int63n(1000000))
}
// ensure MicroTime implements fuzz.Interface
var _ fuzz.Interface = &MicroTime{}
// ensure MicroTime implements randfill.Interface
var _ randfill.SimpleSelfFiller = &MicroTime{}

View File

@ -20,21 +20,22 @@ limitations under the License.
package v1
import (
"math/rand"
"time"
fuzz "github.com/google/gofuzz"
"sigs.k8s.io/randfill"
)
// Fuzz satisfies fuzz.Interface.
func (t *Time) Fuzz(c fuzz.Continue) {
// Fuzz satisfies randfill.SimpleSelfFiller.
func (t *Time) RandFill(r *rand.Rand) {
if t == nil {
return
}
// Allow for about 1000 years of randomness. Leave off nanoseconds
// because JSON doesn't represent them so they can't round-trip
// properly.
t.Time = time.Unix(c.Rand.Int63n(1000*365*24*60*60), 0)
t.Time = time.Unix(r.Int63n(1000*365*24*60*60), 0)
}
// ensure Time implements fuzz.Interface
var _ fuzz.Interface = &Time{}
// ensure Time implements randfill.SimpleSelfFiller
var _ randfill.SimpleSelfFiller = &Time{}

View File

@ -188,7 +188,7 @@ func NestedSlice(obj map[string]interface{}, fields ...string) ([]interface{}, b
// NestedStringMap returns a copy of map[string]string value of a nested field.
// Returns false if value is not found and an error if not a map[string]interface{} or contains non-string values in the map.
func NestedStringMap(obj map[string]interface{}, fields ...string) (map[string]string, bool, error) {
m, found, err := nestedMapNoCopy(obj, fields...)
m, found, err := nestedMapNoCopy(obj, false, fields...)
if !found || err != nil {
return nil, found, err
}
@ -203,10 +203,32 @@ func NestedStringMap(obj map[string]interface{}, fields ...string) (map[string]s
return strMap, true, nil
}
// NestedNullCoercingStringMap returns a copy of map[string]string value of a nested field.
// Returns `nil, true, nil` if the value exists and is explicitly null.
// Returns `nil, false, err` if the value is not a map or a null value, or is a map and contains non-string non-null values.
// Null values in the map are coerced to "" to match json decoding behavior.
func NestedNullCoercingStringMap(obj map[string]interface{}, fields ...string) (map[string]string, bool, error) {
m, found, err := nestedMapNoCopy(obj, true, fields...)
if !found || err != nil || m == nil {
return nil, found, err
}
strMap := make(map[string]string, len(m))
for k, v := range m {
if str, ok := v.(string); ok {
strMap[k] = str
} else if v == nil {
strMap[k] = ""
} else {
return nil, false, fmt.Errorf("%v accessor error: contains non-string value in the map under key %q: %v is of the type %T, expected string", jsonPath(fields), k, v, v)
}
}
return strMap, true, nil
}
// NestedMap returns a deep copy of map[string]interface{} value of a nested field.
// Returns false if value is not found and an error if not a map[string]interface{}.
func NestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, bool, error) {
m, found, err := nestedMapNoCopy(obj, fields...)
m, found, err := nestedMapNoCopy(obj, false, fields...)
if !found || err != nil {
return nil, found, err
}
@ -215,11 +237,14 @@ func NestedMap(obj map[string]interface{}, fields ...string) (map[string]interfa
// nestedMapNoCopy returns a map[string]interface{} value of a nested field.
// Returns false if value is not found and an error if not a map[string]interface{}.
func nestedMapNoCopy(obj map[string]interface{}, fields ...string) (map[string]interface{}, bool, error) {
func nestedMapNoCopy(obj map[string]interface{}, tolerateNil bool, fields ...string) (map[string]interface{}, bool, error) {
val, found, err := NestedFieldNoCopy(obj, fields...)
if !found || err != nil {
return nil, found, err
}
if val == nil && tolerateNil {
return nil, true, nil
}
m, ok := val.(map[string]interface{})
if !ok {
return nil, false, fmt.Errorf("%v accessor error: %v is of the type %T, expected map[string]interface{}", jsonPath(fields), val, val)

View File

@ -397,7 +397,7 @@ func (u *Unstructured) SetDeletionGracePeriodSeconds(deletionGracePeriodSeconds
}
func (u *Unstructured) GetLabels() map[string]string {
m, _, _ := NestedStringMap(u.Object, "metadata", "labels")
m, _, _ := NestedNullCoercingStringMap(u.Object, "metadata", "labels")
return m
}
@ -410,7 +410,7 @@ func (u *Unstructured) SetLabels(labels map[string]string) {
}
func (u *Unstructured) GetAnnotations() map[string]string {
m, _, _ := NestedStringMap(u.Object, "metadata", "annotations")
m, _, _ := NestedNullCoercingStringMap(u.Object, "metadata", "annotations")
return m
}

View File

@ -104,7 +104,7 @@ func ValidateLabelSelectorRequirement(sr metav1.LabelSelectorRequirement, opts L
func ValidateLabelName(labelName string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
for _, msg := range validation.IsQualifiedName(labelName) {
allErrs = append(allErrs, field.Invalid(fldPath, labelName, msg))
allErrs = append(allErrs, field.Invalid(fldPath, labelName, msg).WithOrigin("labelKey"))
}
return allErrs
}

View File

@ -20,4 +20,4 @@ limitations under the License.
// +groupName=meta.k8s.io
package v1beta1 // import "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
package v1beta1

View File

@ -21,4 +21,4 @@ limitations under the License.
// but for the fields which did not change, copying is automated. This makes it
// easy to modify the structures you use in memory without affecting the format
// you store on disk or respond to in your external API calls.
package conversion // import "k8s.io/apimachinery/pkg/conversion"
package conversion

View File

@ -16,4 +16,4 @@ limitations under the License.
// Package queryparams provides conversion from versioned
// runtime objects to URL query values
package queryparams // import "k8s.io/apimachinery/pkg/conversion/queryparams"
package queryparams

View File

@ -16,4 +16,4 @@ limitations under the License.
// Package fields implements a simple field system, parsing and matching
// selectors with sets of fields.
package fields // import "k8s.io/apimachinery/pkg/fields"
package fields

View File

@ -16,4 +16,4 @@ limitations under the License.
// Package labels implements a simple label system, parsing and matching
// selectors with sets of labels.
package labels // import "k8s.io/apimachinery/pkg/labels"
package labels

View File

@ -48,4 +48,4 @@ limitations under the License.
//
// As a bonus, a few common types useful from all api objects and versions
// are provided in types.go.
package runtime // import "k8s.io/apimachinery/pkg/runtime"
package runtime

View File

@ -259,6 +259,7 @@ type ObjectDefaulter interface {
type ObjectVersioner interface {
ConvertToVersion(in Object, gv GroupVersioner) (out Object, err error)
PrioritizedVersionsForGroup(group string) []schema.GroupVersion
}
// ObjectConvertor converts an object to a different version.

View File

@ -17,15 +17,18 @@ limitations under the License.
package runtime
import (
"context"
"fmt"
"reflect"
"strings"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/naming"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// Scheme defines methods for serializing and deserializing API objects, a type
@ -68,6 +71,12 @@ type Scheme struct {
// the provided object must be a pointer.
defaulterFuncs map[reflect.Type]func(interface{})
// validationFuncs is a map to funcs to be called with an object to perform validation.
// The provided object must be a pointer.
// If oldObject is non-nil, update validation is performed and may perform additional
// validation such as transition rules and immutability checks.
validationFuncs map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList
// converter stores all registered conversion functions. It also has
// default converting behavior.
converter *conversion.Converter
@ -96,6 +105,7 @@ func NewScheme() *Scheme {
unversionedKinds: map[string]reflect.Type{},
fieldLabelConversionFuncs: map[schema.GroupVersionKind]FieldLabelConversionFunc{},
defaulterFuncs: map[reflect.Type]func(interface{}){},
validationFuncs: map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresource ...string) field.ErrorList{},
versionPriority: map[string][]string{},
schemeName: naming.GetNameFromCallsite(internalPackages...),
}
@ -347,6 +357,35 @@ func (s *Scheme) Default(src Object) {
}
}
// AddValidationFunc registered a function that can validate the object, and
// oldObject. These functions will be invoked when Validate() or ValidateUpdate()
// is called. The function will never be called unless the validated object
// matches srcType. If this function is invoked twice with the same srcType, the
// fn passed to the later call will be used instead.
func (s *Scheme) AddValidationFunc(srcType Object, fn func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList) {
s.validationFuncs[reflect.TypeOf(srcType)] = fn
}
// Validate validates the provided Object according to the generated declarative validation code.
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
func (s *Scheme) Validate(ctx context.Context, options sets.Set[string], object Object, subresources ...string) field.ErrorList {
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
return fn(ctx, operation.Operation{Type: operation.Create, Options: options}, object, nil, subresources...)
}
return nil
}
// ValidateUpdate validates the provided object and oldObject according to the generated declarative validation code.
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
func (s *Scheme) ValidateUpdate(ctx context.Context, options sets.Set[string], object, oldObject Object, subresources ...string) field.ErrorList {
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
return fn(ctx, operation.Operation{Type: operation.Update, Options: options}, object, oldObject, subresources...)
}
return nil
}
// Convert will attempt to convert in into out. Both must be pointers. For easy
// testing of conversion functions. Returns an error if the conversion isn't
// possible. You can call this with types that haven't been registered (for example,

View File

@ -140,7 +140,7 @@ func (cache *checkers) getCheckerInternal(rt reflect.Type, parent *path) (c chec
var wg sync.WaitGroup
wg.Add(1)
defer wg.Done()
c = checker{
placeholder := checker{
safe: func() bool {
wg.Wait()
return c.safe()
@ -150,7 +150,7 @@ func (cache *checkers) getCheckerInternal(rt reflect.Type, parent *path) (c chec
return c.check(rv, depth)
},
}
if actual, loaded := cache.m.LoadOrStore(rt, &c); loaded {
if actual, loaded := cache.m.LoadOrStore(rt, &placeholder); loaded {
// Someone else stored an entry for this type, use it.
return *actual.(*checker)
}

View File

@ -28,7 +28,7 @@ import (
func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, options CodecFactoryOptions) []runtime.SerializerInfo {
jsonSerializer := json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: false, Pretty: false, Strict: options.Strict},
json.SerializerOptions{Yaml: false, Pretty: false, Strict: options.Strict, StreamingCollectionsEncoding: options.StreamingCollectionsEncodingToJSON},
)
jsonSerializerType := runtime.SerializerInfo{
MediaType: runtime.ContentTypeJSON,
@ -38,7 +38,7 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option
Serializer: jsonSerializer,
StrictSerializer: json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: false, Pretty: false, Strict: true},
json.SerializerOptions{Yaml: false, Pretty: false, Strict: true, StreamingCollectionsEncoding: options.StreamingCollectionsEncodingToJSON},
),
StreamSerializer: &runtime.StreamSerializerInfo{
EncodesAsText: true,
@ -61,7 +61,9 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option
mf, scheme, scheme,
json.SerializerOptions{Yaml: true, Pretty: false, Strict: true},
)
protoSerializer := protobuf.NewSerializer(scheme, scheme)
protoSerializer := protobuf.NewSerializerWithOptions(scheme, scheme, protobuf.SerializerOptions{
StreamingCollectionsEncoding: options.StreamingCollectionsEncodingToProtobuf,
})
protoRawSerializer := protobuf.NewRawSerializer(scheme, scheme)
serializers := []runtime.SerializerInfo{
@ -113,6 +115,9 @@ type CodecFactoryOptions struct {
// Pretty includes a pretty serializer along with the non-pretty one
Pretty bool
StreamingCollectionsEncodingToJSON bool
StreamingCollectionsEncodingToProtobuf bool
serializers []func(runtime.ObjectCreater, runtime.ObjectTyper) runtime.SerializerInfo
}
@ -147,6 +152,18 @@ func WithSerializer(f func(runtime.ObjectCreater, runtime.ObjectTyper) runtime.S
}
}
func WithStreamingCollectionEncodingToJSON() CodecFactoryOptionsMutator {
return func(options *CodecFactoryOptions) {
options.StreamingCollectionsEncodingToJSON = true
}
}
func WithStreamingCollectionEncodingToProtobuf() CodecFactoryOptionsMutator {
return func(options *CodecFactoryOptions) {
options.StreamingCollectionsEncodingToProtobuf = true
}
}
// NewCodecFactory provides methods for retrieving serializers for the supported wire formats
// and conversion wrappers to define preferred internal and external versions. In the future,
// as the internal version is used less, callers may instead use a defaulting serializer and

View File

@ -0,0 +1,230 @@
/*
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 json
import (
"encoding/json"
"fmt"
"io"
"maps"
"slices"
"sort"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/conversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
func streamEncodeCollections(obj runtime.Object, w io.Writer) (bool, error) {
list, ok := obj.(*unstructured.UnstructuredList)
if ok {
return true, streamingEncodeUnstructuredList(w, list)
}
if _, ok := obj.(json.Marshaler); ok {
return false, nil
}
typeMeta, listMeta, items, err := getListMeta(obj)
if err == nil {
return true, streamingEncodeList(w, typeMeta, listMeta, items)
}
return false, nil
}
// getListMeta implements list extraction logic for json stream serialization.
//
// Reason for a custom logic instead of reusing accessors from meta package:
// * Validate json tags to prevent incompatibility with json standard package.
// * ListMetaAccessor doesn't distinguish empty from nil value.
// * TypeAccessort reparsing "apiVersion" and serializing it with "{group}/{version}"
func getListMeta(list runtime.Object) (metav1.TypeMeta, metav1.ListMeta, []runtime.Object, error) {
listValue, err := conversion.EnforcePtr(list)
if err != nil {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, err
}
listType := listValue.Type()
if listType.NumField() != 3 {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf("expected ListType to have 3 fields")
}
// TypeMeta
typeMeta, ok := listValue.Field(0).Interface().(metav1.TypeMeta)
if !ok {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf("expected TypeMeta field to have TypeMeta type")
}
if listType.Field(0).Tag.Get("json") != ",inline" {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf(`expected TypeMeta json field tag to be ",inline"`)
}
// ListMeta
listMeta, ok := listValue.Field(1).Interface().(metav1.ListMeta)
if !ok {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf("expected ListMeta field to have ListMeta type")
}
if listType.Field(1).Tag.Get("json") != "metadata,omitempty" {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf(`expected ListMeta json field tag to be "metadata,omitempty"`)
}
// Items
items, err := meta.ExtractList(list)
if err != nil {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, err
}
if listType.Field(2).Tag.Get("json") != "items" {
return metav1.TypeMeta{}, metav1.ListMeta{}, nil, fmt.Errorf(`expected Items json field tag to be "items"`)
}
return typeMeta, listMeta, items, nil
}
func streamingEncodeList(w io.Writer, typeMeta metav1.TypeMeta, listMeta metav1.ListMeta, items []runtime.Object) error {
// Start
if _, err := w.Write([]byte(`{`)); err != nil {
return err
}
// TypeMeta
if typeMeta.Kind != "" {
if err := encodeKeyValuePair(w, "kind", typeMeta.Kind, []byte(",")); err != nil {
return err
}
}
if typeMeta.APIVersion != "" {
if err := encodeKeyValuePair(w, "apiVersion", typeMeta.APIVersion, []byte(",")); err != nil {
return err
}
}
// ListMeta
if err := encodeKeyValuePair(w, "metadata", listMeta, []byte(",")); err != nil {
return err
}
// Items
if err := encodeItemsObjectSlice(w, items); err != nil {
return err
}
// End
_, err := w.Write([]byte("}\n"))
return err
}
func encodeItemsObjectSlice(w io.Writer, items []runtime.Object) (err error) {
if items == nil {
err := encodeKeyValuePair(w, "items", nil, nil)
return err
}
_, err = w.Write([]byte(`"items":[`))
if err != nil {
return err
}
suffix := []byte(",")
for i, item := range items {
if i == len(items)-1 {
suffix = nil
}
err := encodeValue(w, item, suffix)
if err != nil {
return err
}
}
_, err = w.Write([]byte("]"))
if err != nil {
return err
}
return err
}
func streamingEncodeUnstructuredList(w io.Writer, list *unstructured.UnstructuredList) error {
_, err := w.Write([]byte(`{`))
if err != nil {
return err
}
keys := slices.Collect(maps.Keys(list.Object))
if _, exists := list.Object["items"]; !exists {
keys = append(keys, "items")
}
sort.Strings(keys)
suffix := []byte(",")
for i, key := range keys {
if i == len(keys)-1 {
suffix = nil
}
if key == "items" {
err = encodeItemsUnstructuredSlice(w, list.Items, suffix)
} else {
err = encodeKeyValuePair(w, key, list.Object[key], suffix)
}
if err != nil {
return err
}
}
_, err = w.Write([]byte("}\n"))
return err
}
func encodeItemsUnstructuredSlice(w io.Writer, items []unstructured.Unstructured, suffix []byte) (err error) {
_, err = w.Write([]byte(`"items":[`))
if err != nil {
return err
}
comma := []byte(",")
for i, item := range items {
if i == len(items)-1 {
comma = nil
}
err := encodeValue(w, item.Object, comma)
if err != nil {
return err
}
}
_, err = w.Write([]byte("]"))
if err != nil {
return err
}
if len(suffix) > 0 {
_, err = w.Write(suffix)
}
return err
}
func encodeKeyValuePair(w io.Writer, key string, value any, suffix []byte) (err error) {
err = encodeValue(w, key, []byte(":"))
if err != nil {
return err
}
err = encodeValue(w, value, suffix)
if err != nil {
return err
}
return err
}
func encodeValue(w io.Writer, value any, suffix []byte) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
_, err = w.Write(data)
if err != nil {
return err
}
if len(suffix) > 0 {
_, err = w.Write(suffix)
}
return err
}

View File

@ -36,7 +36,7 @@ import (
// is not nil, the object has the group, version, and kind fields set.
// Deprecated: use NewSerializerWithOptions instead.
func NewSerializer(meta MetaFactory, creater runtime.ObjectCreater, typer runtime.ObjectTyper, pretty bool) *Serializer {
return NewSerializerWithOptions(meta, creater, typer, SerializerOptions{false, pretty, false})
return NewSerializerWithOptions(meta, creater, typer, SerializerOptions{false, pretty, false, false})
}
// NewYAMLSerializer creates a YAML serializer that handles encoding versioned objects into the proper YAML form. If typer
@ -44,7 +44,7 @@ func NewSerializer(meta MetaFactory, creater runtime.ObjectCreater, typer runtim
// matches JSON, and will error if constructs are used that do not serialize to JSON.
// Deprecated: use NewSerializerWithOptions instead.
func NewYAMLSerializer(meta MetaFactory, creater runtime.ObjectCreater, typer runtime.ObjectTyper) *Serializer {
return NewSerializerWithOptions(meta, creater, typer, SerializerOptions{true, false, false})
return NewSerializerWithOptions(meta, creater, typer, SerializerOptions{true, false, false, false})
}
// NewSerializerWithOptions creates a JSON/YAML serializer that handles encoding versioned objects into the proper JSON/YAML
@ -93,6 +93,9 @@ type SerializerOptions struct {
// Strict: configures the Serializer to return strictDecodingError's when duplicate fields are present decoding JSON or YAML.
// Note that enabling this option is not as performant as the non-strict variant, and should not be used in fast paths.
Strict bool
// StreamingCollectionsEncoding enables encoding collection, one item at the time, drastically reducing memory needed.
StreamingCollectionsEncoding bool
}
// Serializer handles encoding versioned objects into the proper JSON form
@ -242,6 +245,15 @@ func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error {
_, err = w.Write(data)
return err
}
if s.options.StreamingCollectionsEncoding {
ok, err := streamEncodeCollections(obj, w)
if err != nil {
return err
}
if ok {
return nil
}
}
encoder := json.NewEncoder(w)
return encoder.Encode(obj)
}

View File

@ -0,0 +1,174 @@
/*
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 protobuf
import (
"errors"
"io"
"math/bits"
"github.com/gogo/protobuf/proto"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
)
var (
errFieldCount = errors.New("expected ListType to have 3 fields")
errTypeMetaField = errors.New("expected TypeMeta field to have TypeMeta type")
errTypeMetaProtobufTag = errors.New(`expected TypeMeta protobuf field tag to be ""`)
errListMetaField = errors.New("expected ListMeta field to have ListMeta type")
errListMetaProtobufTag = errors.New(`expected ListMeta protobuf field tag to be "bytes,1,opt,name=metadata"`)
errItemsProtobufTag = errors.New(`expected Items protobuf field tag to be "bytes,2,rep,name=items"`)
errItemsSizer = errors.New(`expected Items elements to implement proto.Sizer`)
)
// getStreamingListData implements list extraction logic for protobuf stream serialization.
//
// Reason for a custom logic instead of reusing accessors from meta package:
// * Validate proto tags to prevent incompatibility with proto standard package.
// * ListMetaAccessor doesn't distinguish empty from nil value.
// * TypeAccessor reparsing "apiVersion" and serializing it with "{group}/{version}"
func getStreamingListData(list runtime.Object) (data streamingListData, err error) {
listValue, err := conversion.EnforcePtr(list)
if err != nil {
return data, err
}
listType := listValue.Type()
if listType.NumField() != 3 {
return data, errFieldCount
}
// TypeMeta: validated, but not returned as is not serialized.
_, ok := listValue.Field(0).Interface().(metav1.TypeMeta)
if !ok {
return data, errTypeMetaField
}
if listType.Field(0).Tag.Get("protobuf") != "" {
return data, errTypeMetaProtobufTag
}
// ListMeta
listMeta, ok := listValue.Field(1).Interface().(metav1.ListMeta)
if !ok {
return data, errListMetaField
}
// if we were ever to relax the protobuf tag check we should update the hardcoded `0xa` below when writing ListMeta.
if listType.Field(1).Tag.Get("protobuf") != "bytes,1,opt,name=metadata" {
return data, errListMetaProtobufTag
}
data.listMeta = listMeta
// Items; if we were ever to relax the protobuf tag check we should update the hardcoded `0x12` below when writing Items.
if listType.Field(2).Tag.Get("protobuf") != "bytes,2,rep,name=items" {
return data, errItemsProtobufTag
}
items, err := meta.ExtractList(list)
if err != nil {
return data, err
}
data.items = items
data.totalSize, data.listMetaSize, data.itemsSizes, err = listSize(listMeta, items)
return data, err
}
type streamingListData struct {
// totalSize is the total size of the serialized List object, including their proto headers/size bytes
totalSize int
// listMetaSize caches results from .Size() call to listMeta, doesn't include header bytes (field identifier, size)
listMetaSize int
listMeta metav1.ListMeta
// itemsSizes caches results from .Size() call to items, doesn't include header bytes (field identifier, size)
itemsSizes []int
items []runtime.Object
}
// listSize return size of ListMeta and items to be later used for preallocations.
// listMetaSize and itemSizes do not include header bytes (field identifier, size).
func listSize(listMeta metav1.ListMeta, items []runtime.Object) (totalSize, listMetaSize int, itemSizes []int, err error) {
// ListMeta
listMetaSize = listMeta.Size()
totalSize += 1 + sovGenerated(uint64(listMetaSize)) + listMetaSize
// Items
itemSizes = make([]int, len(items))
for i, item := range items {
sizer, ok := item.(proto.Sizer)
if !ok {
return totalSize, listMetaSize, nil, errItemsSizer
}
n := sizer.Size()
itemSizes[i] = n
totalSize += 1 + sovGenerated(uint64(n)) + n
}
return totalSize, listMetaSize, itemSizes, nil
}
func streamingEncodeUnknownList(w io.Writer, unk runtime.Unknown, listData streamingListData, memAlloc runtime.MemoryAllocator) error {
_, err := w.Write(protoEncodingPrefix)
if err != nil {
return err
}
// encodeList is responsible for encoding the List into the unknown Raw.
encodeList := func(writer io.Writer) (int, error) {
return streamingEncodeList(writer, listData, memAlloc)
}
_, err = unk.MarshalToWriter(w, listData.totalSize, encodeList)
return err
}
func streamingEncodeList(w io.Writer, listData streamingListData, memAlloc runtime.MemoryAllocator) (size int, err error) {
// ListMeta; 0xa = (1 << 3) | 2; field number: 1, type: 2 (LEN). https://protobuf.dev/programming-guides/encoding/#structure
n, err := doEncodeWithHeader(&listData.listMeta, w, 0xa, listData.listMetaSize, memAlloc)
size += n
if err != nil {
return size, err
}
// Items; 0x12 = (2 << 3) | 2; field number: 2, type: 2 (LEN). https://protobuf.dev/programming-guides/encoding/#structure
for i, item := range listData.items {
n, err := doEncodeWithHeader(item, w, 0x12, listData.itemsSizes[i], memAlloc)
size += n
if err != nil {
return size, err
}
}
return size, nil
}
func writeVarintGenerated(w io.Writer, v int) (int, error) {
buf := make([]byte, sovGenerated(uint64(v)))
encodeVarintGenerated(buf, len(buf), uint64(v))
return w.Write(buf)
}
// sovGenerated is copied from `generated.pb.go` returns size of varint.
func sovGenerated(v uint64) int {
return (bits.Len64(v|1) + 6) / 7
}
// encodeVarintGenerated is copied from `generated.pb.go` encodes varint.
func encodeVarintGenerated(dAtA []byte, offset int, v uint64) int {
offset -= sovGenerated(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return base
}

View File

@ -15,4 +15,4 @@ limitations under the License.
*/
// Package protobuf provides a Kubernetes serializer for the protobuf format.
package protobuf // import "k8s.io/apimachinery/pkg/runtime/serializer/protobuf"
package protobuf

View File

@ -72,10 +72,18 @@ func IsNotMarshalable(err error) bool {
// is passed, the encoded object will have group, version, and kind fields set. If typer is nil, the objects will be written
// as-is (any type info passed with the object will be used).
func NewSerializer(creater runtime.ObjectCreater, typer runtime.ObjectTyper) *Serializer {
return NewSerializerWithOptions(creater, typer, SerializerOptions{})
}
// NewSerializerWithOptions creates a Protobuf serializer that handles encoding versioned objects into the proper wire form. If a typer
// is passed, the encoded object will have group, version, and kind fields set. If typer is nil, the objects will be written
// as-is (any type info passed with the object will be used).
func NewSerializerWithOptions(creater runtime.ObjectCreater, typer runtime.ObjectTyper, opts SerializerOptions) *Serializer {
return &Serializer{
prefix: protoEncodingPrefix,
creater: creater,
typer: typer,
options: opts,
}
}
@ -84,6 +92,14 @@ type Serializer struct {
prefix []byte
creater runtime.ObjectCreater
typer runtime.ObjectTyper
options SerializerOptions
}
// SerializerOptions holds the options which are used to configure a Proto serializer.
type SerializerOptions struct {
// StreamingCollectionsEncoding enables encoding collection, one item at the time, drastically reducing memory needed.
StreamingCollectionsEncoding bool
}
var _ runtime.Serializer = &Serializer{}
@ -209,6 +225,13 @@ func (s *Serializer) doEncode(obj runtime.Object, w io.Writer, memAlloc runtime.
},
}
}
if s.options.StreamingCollectionsEncoding {
listData, err := getStreamingListData(obj)
if err == nil {
// Doesn't honor custom proto marshaling methods (like json streaming), because all proto objects implement proto methods.
return streamingEncodeUnknownList(w, unk, listData, memAlloc)
}
}
switch t := obj.(type) {
case bufferedMarshaller:
@ -428,6 +451,39 @@ func (s *RawSerializer) encode(obj runtime.Object, w io.Writer, memAlloc runtime
}
func (s *RawSerializer) doEncode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error {
_, err := doEncode(obj, w, nil, memAlloc)
return err
}
func doEncodeWithHeader(obj any, w io.Writer, field byte, precomputedSize int, memAlloc runtime.MemoryAllocator) (size int, err error) {
// Field identifier
n, err := w.Write([]byte{field})
size += n
if err != nil {
return size, err
}
// Size
n, err = writeVarintGenerated(w, precomputedSize)
size += n
if err != nil {
return size, err
}
// Obj
n, err = doEncode(obj, w, &precomputedSize, memAlloc)
size += n
if err != nil {
return size, err
}
if n != precomputedSize {
return size, fmt.Errorf("the size value was %d, but doEncode wrote %d bytes to data", precomputedSize, n)
}
return size, nil
}
// doEncode encodes provided object into writer using a allocator if possible.
// Avoids call by object Size if precomputedObjSize is provided.
// precomputedObjSize should not include header bytes (field identifier, size).
func doEncode(obj any, w io.Writer, precomputedObjSize *int, memAlloc runtime.MemoryAllocator) (int, error) {
if memAlloc == nil {
klog.Error("a mandatory memory allocator wasn't provided, this might have a negative impact on performance, check invocations of EncodeWithAllocator method, falling back on runtime.SimpleAllocator")
memAlloc = &runtime.SimpleAllocator{}
@ -436,40 +492,43 @@ func (s *RawSerializer) doEncode(obj runtime.Object, w io.Writer, memAlloc runti
case bufferedReverseMarshaller:
// this path performs a single allocation during write only when the Allocator wasn't provided
// it also requires the caller to implement the more efficient Size and MarshalToSizedBuffer methods
encodedSize := uint64(t.Size())
data := memAlloc.Allocate(encodedSize)
if precomputedObjSize == nil {
s := t.Size()
precomputedObjSize = &s
}
data := memAlloc.Allocate(uint64(*precomputedObjSize))
n, err := t.MarshalToSizedBuffer(data)
if err != nil {
return err
return 0, err
}
_, err = w.Write(data[:n])
return err
return w.Write(data[:n])
case bufferedMarshaller:
// this path performs a single allocation during write only when the Allocator wasn't provided
// it also requires the caller to implement the more efficient Size and MarshalTo methods
encodedSize := uint64(t.Size())
data := memAlloc.Allocate(encodedSize)
if precomputedObjSize == nil {
s := t.Size()
precomputedObjSize = &s
}
data := memAlloc.Allocate(uint64(*precomputedObjSize))
n, err := t.MarshalTo(data)
if err != nil {
return err
return 0, err
}
_, err = w.Write(data[:n])
return err
return w.Write(data[:n])
case proto.Marshaler:
// this path performs extra allocations
data, err := t.Marshal()
if err != nil {
return err
return 0, err
}
_, err = w.Write(data)
return err
return w.Write(data)
default:
return errNotMarshalable{reflect.TypeOf(obj)}
return 0, errNotMarshalable{reflect.TypeOf(obj)}
}
}

View File

@ -18,6 +18,7 @@ package runtime
import (
"fmt"
"io"
)
type ProtobufMarshaller interface {
@ -28,6 +29,124 @@ type ProtobufReverseMarshaller interface {
MarshalToSizedBuffer(data []byte) (int, error)
}
const (
typeMetaTag = 0xa
rawTag = 0x12
contentEncodingTag = 0x1a
contentTypeTag = 0x22
// max length of a varint for a uint64
maxUint64VarIntLength = 10
)
// MarshalToWriter allows a caller to provide a streaming writer for raw bytes,
// instead of populating them inside the Unknown struct.
// rawSize is the number of bytes rawWriter will write in a success case.
// writeRaw is called when it is time to write the raw bytes. It must return `rawSize, nil` or an error.
func (m *Unknown) MarshalToWriter(w io.Writer, rawSize int, writeRaw func(io.Writer) (int, error)) (int, error) {
size := 0
// reuse the buffer for varint marshaling
varintBuffer := make([]byte, maxUint64VarIntLength)
writeVarint := func(i int) (int, error) {
offset := encodeVarintGenerated(varintBuffer, len(varintBuffer), uint64(i))
return w.Write(varintBuffer[offset:])
}
// TypeMeta
{
n, err := w.Write([]byte{typeMetaTag})
size += n
if err != nil {
return size, err
}
typeMetaBytes, err := m.TypeMeta.Marshal()
if err != nil {
return size, err
}
n, err = writeVarint(len(typeMetaBytes))
size += n
if err != nil {
return size, err
}
n, err = w.Write(typeMetaBytes)
size += n
if err != nil {
return size, err
}
}
// Raw, delegating write to writeRaw()
{
n, err := w.Write([]byte{rawTag})
size += n
if err != nil {
return size, err
}
n, err = writeVarint(rawSize)
size += n
if err != nil {
return size, err
}
n, err = writeRaw(w)
size += n
if err != nil {
return size, err
}
if n != int(rawSize) {
return size, fmt.Errorf("the size value was %d, but encoding wrote %d bytes to data", rawSize, n)
}
}
// ContentEncoding
{
n, err := w.Write([]byte{contentEncodingTag})
size += n
if err != nil {
return size, err
}
n, err = writeVarint(len(m.ContentEncoding))
size += n
if err != nil {
return size, err
}
n, err = w.Write([]byte(m.ContentEncoding))
size += n
if err != nil {
return size, err
}
}
// ContentEncoding
{
n, err := w.Write([]byte{contentTypeTag})
size += n
if err != nil {
return size, err
}
n, err = writeVarint(len(m.ContentType))
size += n
if err != nil {
return size, err
}
n, err = w.Write([]byte(m.ContentType))
size += n
if err != nil {
return size, err
}
}
return size, nil
}
// NestedMarshalTo allows a caller to avoid extra allocations during serialization of an Unknown
// that will contain an object that implements ProtobufMarshaller or ProtobufReverseMarshaller.
func (m *Unknown) NestedMarshalTo(data []byte, b ProtobufMarshaller, size uint64) (int, error) {
@ -43,12 +162,12 @@ func (m *Unknown) NestedMarshalTo(data []byte, b ProtobufMarshaller, size uint64
copy(data[i:], m.ContentType)
i = encodeVarintGenerated(data, i, uint64(len(m.ContentType)))
i--
data[i] = 0x22
data[i] = contentTypeTag
i -= len(m.ContentEncoding)
copy(data[i:], m.ContentEncoding)
i = encodeVarintGenerated(data, i, uint64(len(m.ContentEncoding)))
i--
data[i] = 0x1a
data[i] = contentEncodingTag
if b != nil {
if r, ok := b.(ProtobufReverseMarshaller); ok {
n1, err := r.MarshalToSizedBuffer(data[:i])
@ -75,7 +194,7 @@ func (m *Unknown) NestedMarshalTo(data []byte, b ProtobufMarshaller, size uint64
}
i = encodeVarintGenerated(data, i, size)
i--
data[i] = 0x12
data[i] = rawTag
}
n2, err := m.TypeMeta.MarshalToSizedBuffer(data[:i])
if err != nil {
@ -84,6 +203,6 @@ func (m *Unknown) NestedMarshalTo(data []byte, b ProtobufMarshaller, size uint64
i -= n2
i = encodeVarintGenerated(data, i, uint64(n2))
i--
data[i] = 0xa
data[i] = typeMetaTag
return msgSize - i, nil
}

View File

@ -15,4 +15,4 @@ limitations under the License.
*/
// Package types implements various generic types used throughout kubernetes.
package types // import "k8s.io/apimachinery/pkg/types"
package types

View File

@ -23,7 +23,7 @@ import (
"strings"
"text/tabwriter"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp" //nolint:depguard
"k8s.io/apimachinery/pkg/util/dump"
)

View File

@ -15,4 +15,4 @@ limitations under the License.
*/
// Package errors implements various utility functions and types around errors.
package errors // import "k8s.io/apimachinery/pkg/util/errors"
package errors

View File

@ -91,12 +91,12 @@ func (r *lengthDelimitedFrameReader) Read(data []byte) (int, error) {
}
n, err := io.ReadAtLeast(r.r, data[:max], int(max))
r.remaining -= n
if err == io.ErrShortBuffer || r.remaining > 0 {
return n, io.ErrShortBuffer
}
if err != nil {
return n, err
}
if r.remaining > 0 {
return n, io.ErrShortBuffer
}
if n != expect {
return n, io.ErrUnexpectedEOF
}

View File

@ -20,24 +20,24 @@ limitations under the License.
package intstr
import (
fuzz "github.com/google/gofuzz"
"sigs.k8s.io/randfill"
)
// Fuzz satisfies fuzz.Interface
func (intstr *IntOrString) Fuzz(c fuzz.Continue) {
// RandFill satisfies randfill.NativeSelfFiller
func (intstr *IntOrString) RandFill(c randfill.Continue) {
if intstr == nil {
return
}
if c.RandBool() {
if c.Bool() {
intstr.Type = Int
c.Fuzz(&intstr.IntVal)
c.Fill(&intstr.IntVal)
intstr.StrVal = ""
} else {
intstr.Type = String
intstr.IntVal = 0
c.Fuzz(&intstr.StrVal)
c.Fill(&intstr.StrVal)
}
}
// ensure IntOrString implements fuzz.Interface
var _ fuzz.Interface = &IntOrString{}
var _ randfill.NativeSelfFiller = &IntOrString{}

View File

@ -36,6 +36,11 @@ var (
)
// PanicHandlers is a list of functions which will be invoked when a panic happens.
//
// The code invoking these handlers prepares a contextual logger so that
// klog.FromContext(ctx) already skips over the panic handler itself and
// several other intermediate functions, ideally such that the log output
// is attributed to the code which triggered the panic.
var PanicHandlers = []func(context.Context, interface{}){logPanic}
// HandleCrash simply catches a crash and logs an error. Meant to be called via
@ -45,7 +50,7 @@ var PanicHandlers = []func(context.Context, interface{}){logPanic}
//
// E.g., you can provide one or more additional handlers for something like shutting down go routines gracefully.
//
// Contextual logging: HandleCrashWithContext should be used instead of HandleCrash in code which supports contextual logging.
// Contextual logging: HandleCrashWithContext or HandleCrashWithLogger should be used instead of HandleCrash in code which supports contextual logging.
func HandleCrash(additionalHandlers ...func(interface{})) {
if r := recover(); r != nil {
additionalHandlersWithContext := make([]func(context.Context, interface{}), len(additionalHandlers))
@ -74,10 +79,30 @@ func HandleCrashWithContext(ctx context.Context, additionalHandlers ...func(cont
}
}
// handleCrash is the common implementation of HandleCrash and HandleCrash.
// HandleCrashWithLogger simply catches a crash and logs an error. Meant to be called via
// defer. Additional context-specific handlers can be provided, and will be
// called in case of panic. HandleCrash actually crashes, after calling the
// handlers and logging the panic message.
//
// E.g., you can provide one or more additional handlers for something like shutting down go routines gracefully.
func HandleCrashWithLogger(logger klog.Logger, additionalHandlers ...func(context.Context, interface{})) {
if r := recover(); r != nil {
ctx := klog.NewContext(context.Background(), logger)
handleCrash(ctx, r, additionalHandlers...)
}
}
// handleCrash is the common implementation of the HandleCrash* variants.
// Having those call a common implementation ensures that the stack depth
// is the same regardless through which path the handlers get invoked.
func handleCrash(ctx context.Context, r any, additionalHandlers ...func(context.Context, interface{})) {
// We don't really know how many call frames to skip because the Go
// panic handler is between us and the code where the panic occurred.
// If it's one function (as in Go 1.21), then skipping four levels
// gets us to the function which called the `defer HandleCrashWithontext(...)`.
logger := klog.FromContext(ctx).WithCallDepth(4)
ctx = klog.NewContext(ctx, logger)
for _, fn := range PanicHandlers {
fn(ctx, r)
}
@ -106,11 +131,7 @@ func logPanic(ctx context.Context, r interface{}) {
stacktrace := make([]byte, size)
stacktrace = stacktrace[:runtime.Stack(stacktrace, false)]
// We don't really know how many call frames to skip because the Go
// panic handler is between us and the code where the panic occurred.
// If it's one function (as in Go 1.21), then skipping four levels
// gets us to the function which called the `defer HandleCrashWithontext(...)`.
logger := klog.FromContext(ctx).WithCallDepth(4)
logger := klog.FromContext(ctx)
// For backwards compatibility, conversion to string
// is handled here instead of defering to the logging
@ -176,12 +197,19 @@ func HandleError(err error) {
// and key/value pairs.
//
// This variant should be used instead of HandleError because it supports
// structured, contextual logging.
// structured, contextual logging. Alternatively, [HandleErrorWithLogger] can
// be used if a logger is available instead of a context.
func HandleErrorWithContext(ctx context.Context, err error, msg string, keysAndValues ...interface{}) {
handleError(ctx, err, msg, keysAndValues...)
}
// handleError is the common implementation of HandleError and HandleErrorWithContext.
// HandleErrorWithLogger is an alternative to [HandlerErrorWithContext] which accepts
// a logger for contextual logging.
func HandleErrorWithLogger(logger klog.Logger, err error, msg string, keysAndValues ...interface{}) {
handleError(klog.NewContext(context.Background(), logger), err, msg, keysAndValues...)
}
// handleError is the common implementation of the HandleError* variants.
// Using this common implementation ensures that the stack depth
// is the same regardless through which path the handlers get invoked.
func handleError(ctx context.Context, err error, msg string, keysAndValues ...interface{}) {

View File

@ -16,4 +16,4 @@ limitations under the License.
// Package sets has generic set and specified sets. Generic set will
// replace specified ones over time. And specific ones are deprecated.
package sets // import "k8s.io/apimachinery/pkg/util/sets"
package sets

View File

@ -0,0 +1,212 @@
/*
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 field
import (
"fmt"
"reflect"
"regexp"
"strings"
)
// ErrorMatcher is a helper for comparing Error objects.
type ErrorMatcher struct {
// TODO(thockin): consider whether type is ever NOT required, maybe just
// assume it.
matchType bool
// TODO(thockin): consider whether field could be assumed - if the
// "want" error has a nil field, don't match on field.
matchField bool
// TODO(thockin): consider whether value could be assumed - if the
// "want" error has a nil value, don't match on field.
matchValue bool
matchOrigin bool
matchDetail func(want, got string) bool
requireOriginWhenInvalid bool
}
// Matches returns true if the two Error objects match according to the
// configured criteria.
func (m ErrorMatcher) Matches(want, got *Error) bool {
if m.matchType && want.Type != got.Type {
return false
}
if m.matchField && want.Field != got.Field {
return false
}
if m.matchValue && !reflect.DeepEqual(want.BadValue, got.BadValue) {
return false
}
if m.matchOrigin {
if want.Origin != got.Origin {
return false
}
if m.requireOriginWhenInvalid && want.Type == ErrorTypeInvalid {
if want.Origin == "" || got.Origin == "" {
return false
}
}
}
if m.matchDetail != nil && !m.matchDetail(want.Detail, got.Detail) {
return false
}
return true
}
// Render returns a string representation of the specified Error object,
// according to the criteria configured in the ErrorMatcher.
func (m ErrorMatcher) Render(e *Error) string {
buf := strings.Builder{}
comma := func() {
if buf.Len() > 0 {
buf.WriteString(", ")
}
}
if m.matchType {
comma()
buf.WriteString(fmt.Sprintf("Type=%q", e.Type))
}
if m.matchField {
comma()
buf.WriteString(fmt.Sprintf("Field=%q", e.Field))
}
if m.matchValue {
comma()
buf.WriteString(fmt.Sprintf("Value=%v", e.BadValue))
}
if m.matchOrigin || m.requireOriginWhenInvalid && e.Type == ErrorTypeInvalid {
comma()
buf.WriteString(fmt.Sprintf("Origin=%q", e.Origin))
}
if m.matchDetail != nil {
comma()
buf.WriteString(fmt.Sprintf("Detail=%q", e.Detail))
}
return "{" + buf.String() + "}"
}
// Exactly returns a derived ErrorMatcher which matches all fields exactly.
func (m ErrorMatcher) Exactly() ErrorMatcher {
return m.ByType().ByField().ByValue().ByOrigin().ByDetailExact()
}
// ByType returns a derived ErrorMatcher which also matches by type.
func (m ErrorMatcher) ByType() ErrorMatcher {
m.matchType = true
return m
}
// ByField returns a derived ErrorMatcher which also matches by field path.
func (m ErrorMatcher) ByField() ErrorMatcher {
m.matchField = true
return m
}
// ByValue returns a derived ErrorMatcher which also matches by the errant
// value.
func (m ErrorMatcher) ByValue() ErrorMatcher {
m.matchValue = true
return m
}
// ByOrigin returns a derived ErrorMatcher which also matches by the origin.
func (m ErrorMatcher) ByOrigin() ErrorMatcher {
m.matchOrigin = true
return m
}
// RequireOriginWhenInvalid returns a derived ErrorMatcher which also requires
// the Origin field to be set when the Type is Invalid and the matcher is
// matching by Origin.
func (m ErrorMatcher) RequireOriginWhenInvalid() ErrorMatcher {
m.requireOriginWhenInvalid = true
return m
}
// ByDetailExact returns a derived ErrorMatcher which also matches errors by
// the exact detail string.
func (m ErrorMatcher) ByDetailExact() ErrorMatcher {
m.matchDetail = func(want, got string) bool {
return got == want
}
return m
}
// ByDetailSubstring returns a derived ErrorMatcher which also matches errors
// by a substring of the detail string.
func (m ErrorMatcher) ByDetailSubstring() ErrorMatcher {
m.matchDetail = func(want, got string) bool {
return strings.Contains(got, want)
}
return m
}
// ByDetailRegexp returns a derived ErrorMatcher which also matches errors by a
// regular expression of the detail string, where the "want" string is assumed
// to be a valid regular expression.
func (m ErrorMatcher) ByDetailRegexp() ErrorMatcher {
m.matchDetail = func(want, got string) bool {
return regexp.MustCompile(want).MatchString(got)
}
return m
}
// TestIntf lets users pass a testing.T while not coupling this package to Go's
// testing package.
type TestIntf interface {
Helper()
Errorf(format string, args ...any)
Logf(format string, args ...any)
}
// Test compares two ErrorLists by the criteria configured in this matcher, and
// fails the test if they don't match. If a given "want" error matches multiple
// "got" errors, they will all be consumed. This might be OK (e.g. if there are
// multiple errors on the same field from the same origin) or it might be an
// insufficiently specific matcher, so these will be logged.
func (m ErrorMatcher) Test(tb TestIntf, want, got ErrorList) {
tb.Helper()
remaining := got
for _, w := range want {
tmp := make(ErrorList, 0, len(remaining))
n := 0
for _, g := range remaining {
if m.Matches(w, g) {
n++
} else {
tmp = append(tmp, g)
}
}
if n == 0 {
tb.Errorf("expected an error matching:\n%s", m.Render(w))
} else if n > 1 {
// This is not necessarily and error, but it's worth logging in
// case it's not what the test author intended.
tb.Logf("multiple errors matched:\n%s", m.Render(w))
}
remaining = tmp
}
if len(remaining) > 0 {
for _, e := range remaining {
exactly := m.Exactly() // makes a copy
tb.Errorf("unmatched error:\n%s", exactly.Render(e))
}
}
}

View File

@ -33,13 +33,35 @@ type Error struct {
Field string
BadValue interface{}
Detail string
// Origin uniquely identifies where this error was generated from. It is used in testing to
// compare expected errors against actual errors without relying on exact detail string matching.
// This allows tests to verify the correct validation logic triggered the error
// regardless of how the error message might be formatted or localized.
//
// The value should be either:
// - A simple camelCase identifier (e.g., "maximum", "maxItems")
// - A structured format using "format=<dash-style-identifier>" for validation errors related to specific formats
// (e.g., "format=dns-label", "format=qualified-name")
//
// If the Origin corresponds to an existing declarative validation tag or JSON Schema keyword,
// use that same name for consistency.
//
// Origin should be set in the most deeply nested validation function that
// can still identify the unique source of the error.
Origin string
// CoveredByDeclarative is true when this error is covered by declarative
// validation. This field is to identify errors from imperative validation
// that should also be caught by declarative validation.
CoveredByDeclarative bool
}
var _ error = &Error{}
// Error implements the error interface.
func (v *Error) Error() string {
return fmt.Sprintf("%s: %s", v.Field, v.ErrorBody())
func (e *Error) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.ErrorBody())
}
type OmitValueType struct{}
@ -48,21 +70,21 @@ var omitValue = OmitValueType{}
// ErrorBody returns the error message without the field name. This is useful
// for building nice-looking higher-level error reporting.
func (v *Error) ErrorBody() string {
func (e *Error) ErrorBody() string {
var s string
switch {
case v.Type == ErrorTypeRequired:
s = v.Type.String()
case v.Type == ErrorTypeForbidden:
s = v.Type.String()
case v.Type == ErrorTypeTooLong:
s = v.Type.String()
case v.Type == ErrorTypeInternal:
s = v.Type.String()
case v.BadValue == omitValue:
s = v.Type.String()
case e.Type == ErrorTypeRequired:
s = e.Type.String()
case e.Type == ErrorTypeForbidden:
s = e.Type.String()
case e.Type == ErrorTypeTooLong:
s = e.Type.String()
case e.Type == ErrorTypeInternal:
s = e.Type.String()
case e.BadValue == omitValue:
s = e.Type.String()
default:
value := v.BadValue
value := e.BadValue
valueType := reflect.TypeOf(value)
if value == nil || valueType == nil {
value = "null"
@ -76,26 +98,38 @@ func (v *Error) ErrorBody() string {
switch t := value.(type) {
case int64, int32, float64, float32, bool:
// use simple printer for simple types
s = fmt.Sprintf("%s: %v", v.Type, value)
s = fmt.Sprintf("%s: %v", e.Type, value)
case string:
s = fmt.Sprintf("%s: %q", v.Type, t)
s = fmt.Sprintf("%s: %q", e.Type, t)
case fmt.Stringer:
// anything that defines String() is better than raw struct
s = fmt.Sprintf("%s: %s", v.Type, t.String())
s = fmt.Sprintf("%s: %s", e.Type, t.String())
default:
// fallback to raw struct
// TODO: internal types have panic guards against json.Marshalling to prevent
// accidental use of internal types in external serialized form. For now, use
// %#v, although it would be better to show a more expressive output in the future
s = fmt.Sprintf("%s: %#v", v.Type, value)
s = fmt.Sprintf("%s: %#v", e.Type, value)
}
}
if len(v.Detail) != 0 {
s += fmt.Sprintf(": %s", v.Detail)
if len(e.Detail) != 0 {
s += fmt.Sprintf(": %s", e.Detail)
}
return s
}
// WithOrigin adds origin information to the FieldError
func (e *Error) WithOrigin(o string) *Error {
e.Origin = o
return e
}
// MarkCoveredByDeclarative marks the error as covered by declarative validation.
func (e *Error) MarkCoveredByDeclarative() *Error {
e.CoveredByDeclarative = true
return e
}
// ErrorType is a machine readable value providing more detail about why
// a field is invalid. These values are expected to match 1-1 with
// CauseType in api/types.go.
@ -169,32 +203,32 @@ func (t ErrorType) String() string {
// TypeInvalid returns a *Error indicating "type is invalid"
func TypeInvalid(field *Path, value interface{}, detail string) *Error {
return &Error{ErrorTypeTypeInvalid, field.String(), value, detail}
return &Error{ErrorTypeTypeInvalid, field.String(), value, detail, "", false}
}
// NotFound returns a *Error indicating "value not found". This is
// used to report failure to find a requested value (e.g. looking up an ID).
func NotFound(field *Path, value interface{}) *Error {
return &Error{ErrorTypeNotFound, field.String(), value, ""}
return &Error{ErrorTypeNotFound, field.String(), value, "", "", false}
}
// Required returns a *Error indicating "value required". This is used
// to report required values that are not provided (e.g. empty strings, null
// values, or empty arrays).
func Required(field *Path, detail string) *Error {
return &Error{ErrorTypeRequired, field.String(), "", detail}
return &Error{ErrorTypeRequired, field.String(), "", detail, "", false}
}
// Duplicate returns a *Error indicating "duplicate value". This is
// used to report collisions of values that must be unique (e.g. names or IDs).
func Duplicate(field *Path, value interface{}) *Error {
return &Error{ErrorTypeDuplicate, field.String(), value, ""}
return &Error{ErrorTypeDuplicate, field.String(), value, "", "", false}
}
// Invalid returns a *Error indicating "invalid value". This is used
// to report malformed values (e.g. failed regex match, too long, out of bounds).
func Invalid(field *Path, value interface{}, detail string) *Error {
return &Error{ErrorTypeInvalid, field.String(), value, detail}
return &Error{ErrorTypeInvalid, field.String(), value, detail, "", false}
}
// NotSupported returns a *Error indicating "unsupported value".
@ -209,7 +243,7 @@ func NotSupported[T ~string](field *Path, value interface{}, validValues []T) *E
}
detail = "supported values: " + strings.Join(quotedValues, ", ")
}
return &Error{ErrorTypeNotSupported, field.String(), value, detail}
return &Error{ErrorTypeNotSupported, field.String(), value, detail, "", false}
}
// Forbidden returns a *Error indicating "forbidden". This is used to
@ -217,7 +251,7 @@ func NotSupported[T ~string](field *Path, value interface{}, validValues []T) *E
// some conditions, but which are not permitted by current conditions (e.g.
// security policy).
func Forbidden(field *Path, detail string) *Error {
return &Error{ErrorTypeForbidden, field.String(), "", detail}
return &Error{ErrorTypeForbidden, field.String(), "", detail, "", false}
}
// TooLong returns a *Error indicating "too long". This is used to report that
@ -231,7 +265,7 @@ func TooLong(field *Path, value interface{}, maxLength int) *Error {
} else {
msg = "value is too long"
}
return &Error{ErrorTypeTooLong, field.String(), "<value omitted>", msg}
return &Error{ErrorTypeTooLong, field.String(), "<value omitted>", msg, "", false}
}
// TooLongMaxLength returns a *Error indicating "too long".
@ -259,14 +293,14 @@ func TooMany(field *Path, actualQuantity, maxQuantity int) *Error {
actual = omitValue
}
return &Error{ErrorTypeTooMany, field.String(), actual, msg}
return &Error{ErrorTypeTooMany, field.String(), actual, msg, "", false}
}
// InternalError returns a *Error indicating "internal error". This is used
// to signal that an error was found that was not directly related to user
// input. The err argument must be non-nil.
func InternalError(field *Path, err error) *Error {
return &Error{ErrorTypeInternal, field.String(), nil, err.Error()}
return &Error{ErrorTypeInternal, field.String(), nil, err.Error(), "", false}
}
// ErrorList holds a set of Errors. It is plausible that we might one day have
@ -285,6 +319,22 @@ func NewErrorTypeMatcher(t ErrorType) utilerrors.Matcher {
}
}
// WithOrigin sets the origin for all errors in the list and returns the updated list.
func (list ErrorList) WithOrigin(origin string) ErrorList {
for _, err := range list {
err.Origin = origin
}
return list
}
// MarkCoveredByDeclarative marks all errors in the list as covered by declarative validation.
func (list ErrorList) MarkCoveredByDeclarative() ErrorList {
for _, err := range list {
err.CoveredByDeclarative = true
}
return list
}
// ToAggregate converts the ErrorList into an errors.Aggregate.
func (list ErrorList) ToAggregate() utilerrors.Aggregate {
if len(list) == 0 {
@ -321,3 +371,25 @@ func (list ErrorList) Filter(fns ...utilerrors.Matcher) ErrorList {
// FilterOut takes an Aggregate and returns an Aggregate
return fromAggregate(err.(utilerrors.Aggregate))
}
// ExtractCoveredByDeclarative returns a new ErrorList containing only the errors that should be covered by declarative validation.
func (list ErrorList) ExtractCoveredByDeclarative() ErrorList {
newList := ErrorList{}
for _, err := range list {
if err.CoveredByDeclarative {
newList = append(newList, err)
}
}
return newList
}
// RemoveCoveredByDeclarative returns a new ErrorList containing only the errors that should not be covered by declarative validation.
func (list ErrorList) RemoveCoveredByDeclarative() ErrorList {
newList := ErrorList{}
for _, err := range list {
if !err.CoveredByDeclarative {
newList = append(newList, err)
}
}
return newList
}

278
vendor/k8s.io/apimachinery/pkg/util/validation/ip.go generated vendored Normal file
View File

@ -0,0 +1,278 @@
/*
Copyright 2023 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 validation
import (
"fmt"
"net"
"net/netip"
"slices"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/klog/v2"
netutils "k8s.io/utils/net"
)
func parseIP(fldPath *field.Path, value string, strictValidation bool) (net.IP, field.ErrorList) {
var allErrors field.ErrorList
ip := netutils.ParseIPSloppy(value)
if ip == nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)"))
return nil, allErrors
}
if strictValidation {
addr, err := netip.ParseAddr(value)
if err != nil {
// If netutils.ParseIPSloppy parsed it, but netip.ParseAddr
// doesn't, then it must have illegal leading 0s.
allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have leading 0s"))
}
if addr.Is4In6() {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must not be an IPv4-mapped IPv6 address"))
}
}
return ip, allErrors
}
// IsValidIPForLegacyField tests that the argument is a valid IP address for a "legacy"
// API field that predates strict IP validation. In particular, this allows IPs that are
// not in canonical form (e.g., "FE80:0:0:0:0:0:0:0abc" instead of "fe80::abc").
//
// If strictValidation is false, this also allows IPs in certain invalid or ambiguous
// formats:
//
// 1. IPv4 IPs are allowed to have leading "0"s in octets (e.g. "010.002.003.004").
// Historically, net.ParseIP (and later netutils.ParseIPSloppy) simply ignored leading
// "0"s in IPv4 addresses, but most libc-based software treats 0-prefixed IPv4 octets
// as octal, meaning different software might interpret the same string as two
// different IPs, potentially leading to security issues. (Current net.ParseIP and
// netip.ParseAddr simply reject inputs with leading "0"s.)
//
// 2. IPv4-mapped IPv6 IPs (e.g. "::ffff:1.2.3.4") are allowed. These can also lead to
// different software interpreting the value in different ways, because they may be
// treated as IPv4 by some software and IPv6 by other software. (net.ParseIP and
// netip.ParseAddr both allow these, but there are no use cases for representing IPv4
// addresses as IPv4-mapped IPv6 addresses in Kubernetes.)
//
// Alternatively, when validating an update to an existing field, you can pass a list of
// IP values from the old object that should be accepted if they appear in the new object
// even if they are not valid.
//
// This function should only be used to validate the existing fields that were
// historically validated in this way, and strictValidation should be true unless the
// StrictIPCIDRValidation feature gate is disabled. Use IsValidIP for parsing new fields.
func IsValidIPForLegacyField(fldPath *field.Path, value string, strictValidation bool, validOldIPs []string) field.ErrorList {
if slices.Contains(validOldIPs, value) {
return nil
}
_, allErrors := parseIP(fldPath, value, strictValidation)
return allErrors.WithOrigin("format=ip-sloppy")
}
// IsValidIP tests that the argument is a valid IP address, according to current
// Kubernetes standards for IP address validation.
func IsValidIP(fldPath *field.Path, value string) field.ErrorList {
ip, allErrors := parseIP(fldPath, value, true)
if len(allErrors) != 0 {
return allErrors.WithOrigin("format=ip-strict")
}
if value != ip.String() {
allErrors = append(allErrors, field.Invalid(fldPath, value, fmt.Sprintf("must be in canonical form (%q)", ip.String())))
}
return allErrors.WithOrigin("format=ip-strict")
}
// GetWarningsForIP returns warnings for IP address values in non-standard forms. This
// should only be used with fields that are validated with IsValidIPForLegacyField().
func GetWarningsForIP(fldPath *field.Path, value string) []string {
ip := netutils.ParseIPSloppy(value)
if ip == nil {
klog.ErrorS(nil, "GetWarningsForIP called on value that was not validated with IsValidIPForLegacyField", "field", fldPath, "value", value)
return nil
}
addr, _ := netip.ParseAddr(value)
if !addr.IsValid() || addr.Is4In6() {
// This catches 2 cases: leading 0s (if ParseIPSloppy() accepted it but
// ParseAddr() doesn't) or IPv4-mapped IPv6 (.Is4In6()). Either way,
// re-stringifying the net.IP value will give the preferred form.
return []string{
fmt.Sprintf("%s: non-standard IP address %q will be considered invalid in a future Kubernetes release: use %q", fldPath, value, ip.String()),
}
}
// If ParseIPSloppy() and ParseAddr() both accept it then it's fully valid, though
// it may be non-canonical.
if addr.Is6() && addr.String() != value {
return []string{
fmt.Sprintf("%s: IPv6 address %q should be in RFC 5952 canonical format (%q)", fldPath, value, addr.String()),
}
}
return nil
}
func parseCIDR(fldPath *field.Path, value string, strictValidation bool) (*net.IPNet, field.ErrorList) {
var allErrors field.ErrorList
_, ipnet, err := netutils.ParseCIDRSloppy(value)
if err != nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)"))
return nil, allErrors
}
if strictValidation {
prefix, err := netip.ParsePrefix(value)
if err != nil {
// If netutils.ParseCIDRSloppy parsed it, but netip.ParsePrefix
// doesn't, then it must have illegal leading 0s (either in the
// IP part or the prefix).
allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have leading 0s in IP or prefix length"))
} else if prefix.Addr().Is4In6() {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have an IPv4-mapped IPv6 address"))
} else if prefix.Addr() != prefix.Masked().Addr() {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have bits set beyond the prefix length"))
}
}
return ipnet, allErrors
}
// IsValidCIDRForLegacyField tests that the argument is a valid CIDR value for a "legacy"
// API field that predates strict IP validation. In particular, this allows IPs that are
// not in canonical form (e.g., "FE80:0abc:0:0:0:0:0:0/64" instead of "fe80:abc::/64").
//
// If strictValidation is false, this also allows CIDR values in certain invalid or
// ambiguous formats:
//
// 1. The IP part of the CIDR value is parsed as with IsValidIPForLegacyField with
// strictValidation=false.
//
// 2. The CIDR value is allowed to be either a "subnet"/"mask" (with the lower bits after
// the prefix length all being 0), or an "interface address" as with `ip addr` (with a
// complete IP address and associated subnet length). With strict validation, the
// value is required to be in "subnet"/"mask" form.
//
// 3. The prefix length is allowed to have leading 0s.
//
// Alternatively, when validating an update to an existing field, you can pass a list of
// CIDR values from the old object that should be accepted if they appear in the new
// object even if they are not valid.
//
// This function should only be used to validate the existing fields that were
// historically validated in this way, and strictValidation should be true unless the
// StrictIPCIDRValidation feature gate is disabled. Use IsValidCIDR or
// IsValidInterfaceAddress for parsing new fields.
func IsValidCIDRForLegacyField(fldPath *field.Path, value string, strictValidation bool, validOldCIDRs []string) field.ErrorList {
if slices.Contains(validOldCIDRs, value) {
return nil
}
_, allErrors := parseCIDR(fldPath, value, strictValidation)
return allErrors
}
// IsValidCIDR tests that the argument is a valid CIDR value, according to current
// Kubernetes standards for CIDR validation. This function is only for
// "subnet"/"mask"-style CIDR values (e.g., "192.168.1.0/24", with no bits set beyond the
// prefix length). Use IsValidInterfaceAddress for "ifaddr"-style CIDR values.
func IsValidCIDR(fldPath *field.Path, value string) field.ErrorList {
ipnet, allErrors := parseCIDR(fldPath, value, true)
if len(allErrors) != 0 {
return allErrors
}
if value != ipnet.String() {
allErrors = append(allErrors, field.Invalid(fldPath, value, fmt.Sprintf("must be in canonical form (%q)", ipnet.String())))
}
return allErrors
}
// GetWarningsForCIDR returns warnings for CIDR values in non-standard forms. This should
// only be used with fields that are validated with IsValidCIDRForLegacyField().
func GetWarningsForCIDR(fldPath *field.Path, value string) []string {
ip, ipnet, err := netutils.ParseCIDRSloppy(value)
if err != nil {
klog.ErrorS(err, "GetWarningsForCIDR called on value that was not validated with IsValidCIDRForLegacyField", "field", fldPath, "value", value)
return nil
}
var warnings []string
// Check for bits set after prefix length
if !ip.Equal(ipnet.IP) {
_, addrlen := ipnet.Mask.Size()
singleIPCIDR := fmt.Sprintf("%s/%d", ip.String(), addrlen)
warnings = append(warnings,
fmt.Sprintf("%s: CIDR value %q is ambiguous in this context (should be %q or %q?)", fldPath, value, ipnet.String(), singleIPCIDR),
)
}
prefix, _ := netip.ParsePrefix(value)
addr := prefix.Addr()
if !prefix.IsValid() || addr.Is4In6() {
// This catches 2 cases: leading 0s (if ParseCIDRSloppy() accepted it but
// ParsePrefix() doesn't) or IPv4-mapped IPv6 (.Is4In6()). Either way,
// re-stringifying the net.IPNet value will give the preferred form.
warnings = append(warnings,
fmt.Sprintf("%s: non-standard CIDR value %q will be considered invalid in a future Kubernetes release: use %q", fldPath, value, ipnet.String()),
)
}
// If ParseCIDRSloppy() and ParsePrefix() both accept it then it's fully valid,
// though it may be non-canonical. But only check this if there are no other
// warnings, since either of the other warnings would also cause a round-trip
// failure.
if len(warnings) == 0 && addr.Is6() && prefix.String() != value {
warnings = append(warnings,
fmt.Sprintf("%s: IPv6 CIDR value %q should be in RFC 5952 canonical format (%q)", fldPath, value, prefix.String()),
)
}
return warnings
}
// IsValidInterfaceAddress tests that the argument is a valid "ifaddr"-style CIDR value in
// canonical form (e.g., "192.168.1.5/24", with a complete IP address and associated
// subnet length). Use IsValidCIDR for "subnet"/"mask"-style CIDR values (e.g.,
// "192.168.1.0/24").
func IsValidInterfaceAddress(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
ip, ipnet, err := netutils.ParseCIDRSloppy(value)
if err != nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid address in CIDR form, (e.g. 10.9.8.7/24 or 2001:db8::1/64)"))
return allErrors
}
// The canonical form of `value` is not `ipnet.String()`, because `ipnet` doesn't
// include the bits after the prefix. We need to construct the canonical form
// ourselves from `ip` and `ipnet.Mask`.
maskSize, _ := ipnet.Mask.Size()
if netutils.IsIPv4(ip) && maskSize > net.IPv4len*8 {
// "::ffff:192.168.0.1/120" -> "192.168.0.1/24"
maskSize -= (net.IPv6len - net.IPv4len) * 8
}
canonical := fmt.Sprintf("%s/%d", ip.String(), maskSize)
if value != canonical {
allErrors = append(allErrors, field.Invalid(fldPath, value, fmt.Sprintf("must be in canonical form (%q)", canonical)))
}
return allErrors
}

View File

@ -24,7 +24,6 @@ import (
"unicode"
"k8s.io/apimachinery/pkg/util/validation/field"
netutils "k8s.io/utils/net"
)
const qnameCharFmt string = "[A-Za-z0-9]"
@ -369,45 +368,6 @@ func IsValidPortName(port string) []string {
return errs
}
// IsValidIP tests that the argument is a valid IP address.
func IsValidIP(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
if netutils.ParseIPSloppy(value) == nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)"))
}
return allErrors
}
// IsValidIPv4Address tests that the argument is a valid IPv4 address.
func IsValidIPv4Address(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
ip := netutils.ParseIPSloppy(value)
if ip == nil || ip.To4() == nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv4 address"))
}
return allErrors
}
// IsValidIPv6Address tests that the argument is a valid IPv6 address.
func IsValidIPv6Address(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
ip := netutils.ParseIPSloppy(value)
if ip == nil || ip.To4() != nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv6 address"))
}
return allErrors
}
// IsValidCIDR tests that the argument is a valid CIDR value.
func IsValidCIDR(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
_, _, err := netutils.ParseCIDRSloppy(value)
if err != nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)"))
}
return allErrors
}
const percentFmt string = "[0-9]+%"
const percentErrMsg string = "a valid percent string must be a numeric string followed by an ending '%'"

View File

@ -15,4 +15,4 @@ limitations under the License.
*/
// Package version provides utilities for version number comparisons
package version // import "k8s.io/apimachinery/pkg/util/version"
package version

View File

@ -33,7 +33,6 @@ type Version struct {
semver bool
preRelease string
buildMetadata string
info apimachineryversion.Info
}
var (
@ -456,27 +455,28 @@ func (v *Version) Compare(other string) (int, error) {
return v.compareInternal(ov), nil
}
// WithInfo returns copy of the version object with requested info
// WithInfo returns copy of the version object.
// Deprecated: The Info field has been removed from the Version struct. This method no longer modifies the Version object.
func (v *Version) WithInfo(info apimachineryversion.Info) *Version {
result := *v
result.info = info
return &result
}
// Info returns the version information of a component.
// Deprecated: Use Info() from effective version instead.
func (v *Version) Info() *apimachineryversion.Info {
if v == nil {
return nil
}
// in case info is empty, or the major and minor in info is different from the actual major and minor
v.info.Major = itoa(v.Major())
v.info.Minor = itoa(v.Minor())
if v.info.GitVersion == "" {
v.info.GitVersion = v.String()
return &apimachineryversion.Info{
Major: Itoa(v.Major()),
Minor: Itoa(v.Minor()),
GitVersion: v.String(),
}
return &v.info
}
func itoa(i uint) string {
func Itoa(i uint) string {
if i == 0 {
return ""
}

View File

@ -157,6 +157,8 @@ func (b Backoff) DelayWithReset(c clock.Clock, resetInterval time.Duration) Dela
// Until is syntactic sugar on top of JitterUntil with zero jitter factor and
// with sliding = true (which means the timer for period starts after the f
// completes).
//
// Contextual logging: UntilWithContext should be used instead of Until in code which supports contextual logging.
func Until(f func(), period time.Duration, stopCh <-chan struct{}) {
JitterUntil(f, period, 0.0, true, stopCh)
}
@ -176,6 +178,8 @@ func UntilWithContext(ctx context.Context, f func(context.Context), period time.
// NonSlidingUntil is syntactic sugar on top of JitterUntil with zero jitter
// factor, with sliding = false (meaning the timer for period starts at the same
// time as the function starts).
//
// Contextual logging: NonSlidingUntilWithContext should be used instead of NonSlidingUntil in code which supports contextual logging.
func NonSlidingUntil(f func(), period time.Duration, stopCh <-chan struct{}) {
JitterUntil(f, period, 0.0, false, stopCh)
}
@ -200,19 +204,44 @@ func NonSlidingUntilWithContext(ctx context.Context, f func(context.Context), pe
//
// Close stopCh to stop. f may not be invoked if stop channel is already
// closed. Pass NeverStop to if you don't want it stop.
//
// Contextual logging: JitterUntilWithContext should be used instead of JitterUntil in code which supports contextual logging.
func JitterUntil(f func(), period time.Duration, jitterFactor float64, sliding bool, stopCh <-chan struct{}) {
BackoffUntil(f, NewJitteredBackoffManager(period, jitterFactor, &clock.RealClock{}), sliding, stopCh)
}
// JitterUntilWithContext loops until context is done, running f every period.
//
// If jitterFactor is positive, the period is jittered before every run of f.
// If jitterFactor is not positive, the period is unchanged and not jittered.
//
// If sliding is true, the period is computed after f runs. If it is false then
// period includes the runtime for f.
//
// Cancel context to stop. f may not be invoked if context is already done.
func JitterUntilWithContext(ctx context.Context, f func(context.Context), period time.Duration, jitterFactor float64, sliding bool) {
BackoffUntilWithContext(ctx, f, NewJitteredBackoffManager(period, jitterFactor, &clock.RealClock{}), sliding)
}
// BackoffUntil loops until stop channel is closed, run f every duration given by BackoffManager.
//
// If sliding is true, the period is computed after f runs. If it is false then
// period includes the runtime for f.
//
// Contextual logging: BackoffUntilWithContext should be used instead of BackoffUntil in code which supports contextual logging.
func BackoffUntil(f func(), backoff BackoffManager, sliding bool, stopCh <-chan struct{}) {
BackoffUntilWithContext(ContextForChannel(stopCh), func(context.Context) { f() }, backoff, sliding)
}
// BackoffUntilWithContext loops until context is done, run f every duration given by BackoffManager.
//
// If sliding is true, the period is computed after f runs. If it is false then
// period includes the runtime for f.
func BackoffUntilWithContext(ctx context.Context, f func(ctx context.Context), backoff BackoffManager, sliding bool) {
var t clock.Timer
for {
select {
case <-stopCh:
case <-ctx.Done():
return
default:
}
@ -222,8 +251,8 @@ func BackoffUntil(f func(), backoff BackoffManager, sliding bool, stopCh <-chan
}
func() {
defer runtime.HandleCrash()
f()
defer runtime.HandleCrashWithContext(ctx)
f(ctx)
}()
if sliding {
@ -236,7 +265,7 @@ func BackoffUntil(f func(), backoff BackoffManager, sliding bool, stopCh <-chan
// In order to mitigate we re-check stopCh at the beginning
// of every loop to prevent extra executions of f().
select {
case <-stopCh:
case <-ctx.Done():
if !t.Stop() {
<-t.C()
}
@ -246,19 +275,6 @@ func BackoffUntil(f func(), backoff BackoffManager, sliding bool, stopCh <-chan
}
}
// JitterUntilWithContext loops until context is done, running f every period.
//
// If jitterFactor is positive, the period is jittered before every run of f.
// If jitterFactor is not positive, the period is unchanged and not jittered.
//
// If sliding is true, the period is computed after f runs. If it is false then
// period includes the runtime for f.
//
// Cancel context to stop. f may not be invoked if context is already expired.
func JitterUntilWithContext(ctx context.Context, f func(context.Context), period time.Duration, jitterFactor float64, sliding bool) {
JitterUntil(func() { f(ctx) }, period, jitterFactor, sliding, ctx.Done())
}
// backoffManager provides simple backoff behavior in a threadsafe manner to a caller.
type backoffManager struct {
backoff Backoff

View File

@ -16,4 +16,4 @@ limitations under the License.
// Package wait provides tools for polling or listening for changes
// to a condition.
package wait // import "k8s.io/apimachinery/pkg/util/wait"
package wait

View File

@ -49,7 +49,7 @@ func loopConditionUntilContext(ctx context.Context, t Timer, immediate, sliding
// if we haven't requested immediate execution, delay once
if immediate {
if ok, err := func() (bool, error) {
defer runtime.HandleCrash()
defer runtime.HandleCrashWithContext(ctx)
return condition(ctx)
}(); err != nil || ok {
return err
@ -83,7 +83,7 @@ func loopConditionUntilContext(ctx context.Context, t Timer, immediate, sliding
t.Next()
}
if ok, err := func() (bool, error) {
defer runtime.HandleCrash()
defer runtime.HandleCrashWithContext(ctx)
return condition(ctx)
}(); err != nil || ok {
return err

View File

@ -80,6 +80,10 @@ func Forever(f func(), period time.Duration) {
Until(f, period, NeverStop)
}
// jitterRand is a dedicated random source for jitter calculations.
// It defaults to rand.Float64, but is a package variable so it can be overridden to make unit tests deterministic.
var jitterRand = rand.Float64
// Jitter returns a time.Duration between duration and duration + maxFactor *
// duration.
//
@ -89,7 +93,7 @@ func Jitter(duration time.Duration, maxFactor float64) time.Duration {
if maxFactor <= 0.0 {
maxFactor = 1.0
}
wait := duration + time.Duration(rand.Float64()*maxFactor*float64(duration))
wait := duration + time.Duration(jitterRand()*maxFactor*float64(duration))
return wait
}
@ -141,6 +145,7 @@ func (c channelContext) Value(key any) any { return nil }
//
// Deprecated: Will be removed when the legacy polling methods are removed.
func runConditionWithCrashProtection(condition ConditionFunc) (bool, error) {
//nolint:logcheck // Already deprecated.
defer runtime.HandleCrash()
return condition()
}
@ -150,7 +155,7 @@ func runConditionWithCrashProtection(condition ConditionFunc) (bool, error) {
//
// Deprecated: Will be removed when the legacy polling methods are removed.
func runConditionWithCrashProtectionWithContext(ctx context.Context, condition ConditionWithContextFunc) (bool, error) {
defer runtime.HandleCrash()
defer runtime.HandleCrashWithContext(ctx)
return condition(ctx)
}

View File

@ -20,10 +20,12 @@ import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"unicode"
"unicode/utf8"
jsonutil "k8s.io/apimachinery/pkg/util/json"
@ -92,7 +94,7 @@ func UnmarshalStrict(data []byte, v interface{}) error {
// YAML decoding path is not used (so that error messages are
// JSON specific).
func ToJSON(data []byte) ([]byte, error) {
if hasJSONPrefix(data) {
if IsJSONBuffer(data) {
return data, nil
}
return yaml.YAMLToJSON(data)
@ -102,7 +104,8 @@ func ToJSON(data []byte) ([]byte, error) {
// separating individual documents. It first converts the YAML
// body to JSON, then unmarshals the JSON.
type YAMLToJSONDecoder struct {
reader Reader
reader Reader
inputOffset int
}
// NewYAMLToJSONDecoder decodes YAML documents from the provided
@ -121,7 +124,7 @@ func NewYAMLToJSONDecoder(r io.Reader) *YAMLToJSONDecoder {
// yaml.Unmarshal.
func (d *YAMLToJSONDecoder) Decode(into interface{}) error {
bytes, err := d.reader.Read()
if err != nil && err != io.EOF {
if err != nil && err != io.EOF { //nolint:errorlint
return err
}
@ -131,9 +134,14 @@ func (d *YAMLToJSONDecoder) Decode(into interface{}) error {
return YAMLSyntaxError{err}
}
}
d.inputOffset += len(bytes)
return err
}
func (d *YAMLToJSONDecoder) InputOffset() int {
return d.inputOffset
}
// YAMLDecoder reads chunks of objects and returns ErrShortBuffer if
// the data is not sufficient.
type YAMLDecoder struct {
@ -229,18 +237,20 @@ func splitYAMLDocument(data []byte, atEOF bool) (advance int, token []byte, err
return 0, nil, nil
}
// decoder is a convenience interface for Decode.
type decoder interface {
Decode(into interface{}) error
}
// YAMLOrJSONDecoder attempts to decode a stream of JSON documents or
// YAML documents by sniffing for a leading { character.
// YAMLOrJSONDecoder attempts to decode a stream of JSON or YAML documents.
// While JSON is YAML, the way Go's JSON decode defines a multi-document stream
// is a series of JSON objects (e.g. {}{}), but YAML defines a multi-document
// stream as a series of documents separated by "---".
//
// This decoder will attempt to decode the stream as JSON first, and if that
// fails, it will switch to YAML. Once it determines the stream is JSON (by
// finding a non-YAML-delimited series of objects), it will not switch to YAML.
// Once it switches to YAML it will not switch back to JSON.
type YAMLOrJSONDecoder struct {
r io.Reader
bufferSize int
decoder decoder
json *json.Decoder
yaml *YAMLToJSONDecoder
stream *StreamReader
count int // how many objects have been decoded
}
type JSONSyntaxError struct {
@ -265,31 +275,108 @@ func (e YAMLSyntaxError) Error() string {
// how far into the stream the decoder will look to figure out whether this
// is a JSON stream (has whitespace followed by an open brace).
func NewYAMLOrJSONDecoder(r io.Reader, bufferSize int) *YAMLOrJSONDecoder {
return &YAMLOrJSONDecoder{
r: r,
bufferSize: bufferSize,
d := &YAMLOrJSONDecoder{}
reader, _, mightBeJSON := GuessJSONStream(r, bufferSize)
d.stream = reader
if mightBeJSON {
d.json = json.NewDecoder(reader)
} else {
d.yaml = NewYAMLToJSONDecoder(reader)
}
return d
}
// Decode unmarshals the next object from the underlying stream into the
// provide object, or returns an error.
func (d *YAMLOrJSONDecoder) Decode(into interface{}) error {
if d.decoder == nil {
buffer, _, isJSON := GuessJSONStream(d.r, d.bufferSize)
if isJSON {
d.decoder = json.NewDecoder(buffer)
// Because we don't know if this is a JSON or YAML stream, a failure from
// both decoders is ambiguous. When in doubt, it will return the error from
// the JSON decoder. Unfortunately, this means that if the first document
// is invalid YAML, the error won't be awesome.
// TODO: the errors from YAML are not great, we could improve them a lot.
var firstErr error
if d.json != nil {
err := d.json.Decode(into)
if err == nil {
d.stream.Consume(int(d.json.InputOffset()) - d.stream.Consumed())
d.count++
return nil
}
if err == io.EOF { //nolint:errorlint
return err
}
var syntax *json.SyntaxError
if ok := errors.As(err, &syntax); ok {
firstErr = JSONSyntaxError{
Offset: syntax.Offset,
Err: syntax,
}
} else {
d.decoder = NewYAMLToJSONDecoder(buffer)
firstErr = err
}
if d.count > 1 {
// If we found 0 or 1 JSON object(s), this stream is still
// ambiguous. But if we found more than 1 JSON object, then this
// is an unambiguous JSON stream, and we should not switch to YAML.
return err
}
// If JSON decoding hits the end of one object and then fails on the
// next, it leaves any leading whitespace in the buffer, which can
// confuse the YAML decoder. We just eat any whitespace we find, up to
// and including the first newline.
d.stream.Rewind()
if err := d.consumeWhitespace(); err == nil {
d.yaml = NewYAMLToJSONDecoder(d.stream)
}
d.json = nil
}
if d.yaml != nil {
err := d.yaml.Decode(into)
if err == nil {
d.stream.Consume(d.yaml.InputOffset() - d.stream.Consumed())
d.count++
return nil
}
if err == io.EOF { //nolint:errorlint
return err
}
if firstErr == nil {
firstErr = err
}
}
err := d.decoder.Decode(into)
if syntax, ok := err.(*json.SyntaxError); ok {
return JSONSyntaxError{
Offset: syntax.Offset,
Err: syntax,
if firstErr != nil {
return firstErr
}
return fmt.Errorf("decoding failed as both JSON and YAML")
}
func (d *YAMLOrJSONDecoder) consumeWhitespace() error {
consumed := 0
for {
buf, err := d.stream.ReadN(4)
if err != nil && err == io.EOF { //nolint:errorlint
return err
}
r, sz := utf8.DecodeRune(buf)
if r == utf8.RuneError || sz == 0 {
return fmt.Errorf("invalid utf8 rune")
}
d.stream.RewindN(len(buf) - sz)
if !unicode.IsSpace(r) {
d.stream.RewindN(sz)
d.stream.Consume(consumed)
return nil
}
if r == '\n' {
d.stream.Consume(consumed)
return nil
}
if err == io.EOF { //nolint:errorlint
break
}
}
return err
return io.EOF
}
type Reader interface {
@ -311,7 +398,7 @@ func (r *YAMLReader) Read() ([]byte, error) {
var buffer bytes.Buffer
for {
line, err := r.reader.Read()
if err != nil && err != io.EOF {
if err != nil && err != io.EOF { //nolint:errorlint
return nil, err
}
@ -329,11 +416,11 @@ func (r *YAMLReader) Read() ([]byte, error) {
if buffer.Len() != 0 {
return buffer.Bytes(), nil
}
if err == io.EOF {
if err == io.EOF { //nolint:errorlint
return nil, err
}
}
if err == io.EOF {
if err == io.EOF { //nolint:errorlint
if buffer.Len() != 0 {
// If we're at EOF, we have a final, non-terminated line. Return it.
return buffer.Bytes(), nil
@ -369,26 +456,20 @@ func (r *LineReader) Read() ([]byte, error) {
// GuessJSONStream scans the provided reader up to size, looking
// for an open brace indicating this is JSON. It will return the
// bufio.Reader it creates for the consumer.
func GuessJSONStream(r io.Reader, size int) (io.Reader, []byte, bool) {
buffer := bufio.NewReaderSize(r, size)
func GuessJSONStream(r io.Reader, size int) (*StreamReader, []byte, bool) {
buffer := NewStreamReader(r, size)
b, _ := buffer.Peek(size)
return buffer, b, hasJSONPrefix(b)
return buffer, b, IsJSONBuffer(b)
}
// IsJSONBuffer scans the provided buffer, looking
// for an open brace indicating this is JSON.
func IsJSONBuffer(buf []byte) bool {
return hasJSONPrefix(buf)
return hasPrefix(buf, jsonPrefix)
}
var jsonPrefix = []byte("{")
// hasJSONPrefix returns true if the provided buffer appears to start with
// a JSON open brace.
func hasJSONPrefix(buf []byte) bool {
return hasPrefix(buf, jsonPrefix)
}
// Return true if the first non-whitespace bytes in buf is
// prefix.
func hasPrefix(buf []byte, prefix []byte) bool {

View File

@ -0,0 +1,130 @@
/*
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 yaml
import "io"
// StreamReader is a reader designed for consuming streams of variable-length
// messages. It buffers data until it is explicitly consumed, and can be
// rewound to re-read previous data.
type StreamReader struct {
r io.Reader
buf []byte
head int // current read offset into buf
ttlConsumed int // number of bytes which have been consumed
}
// NewStreamReader creates a new StreamReader wrapping the provided
// io.Reader.
func NewStreamReader(r io.Reader, size int) *StreamReader {
if size == 0 {
size = 4096
}
return &StreamReader{
r: r,
buf: make([]byte, 0, size), // Start with a reasonable capacity
}
}
// Read implements io.Reader. It first returns any buffered data after the
// current offset, and if that's exhausted, reads from the underlying reader
// and buffers the data. The returned data is not considered consumed until the
// Consume method is called.
func (r *StreamReader) Read(p []byte) (n int, err error) {
// If we have buffered data, return it
if r.head < len(r.buf) {
n = copy(p, r.buf[r.head:])
r.head += n
return n, nil
}
// If we've already hit EOF, return it
if r.r == nil {
return 0, io.EOF
}
// Read from the underlying reader
n, err = r.r.Read(p)
if n > 0 {
r.buf = append(r.buf, p[:n]...)
r.head += n
}
if err == nil {
return n, nil
}
if err == io.EOF {
// Store that we've hit EOF by setting r to nil
r.r = nil
}
return n, err
}
// ReadN reads exactly n bytes from the reader, blocking until all bytes are
// read or an error occurs. If an error occurs, the number of bytes read is
// returned along with the error. If EOF is hit before n bytes are read, this
// will return the bytes read so far, along with io.EOF. The returned data is
// not considered consumed until the Consume method is called.
func (r *StreamReader) ReadN(want int) ([]byte, error) {
ret := make([]byte, want)
off := 0
for off < want {
n, err := r.Read(ret[off:])
if err != nil {
return ret[:off+n], err
}
off += n
}
return ret, nil
}
// Peek returns the next n bytes without advancing the reader. The returned
// bytes are valid until the next call to Consume.
func (r *StreamReader) Peek(n int) ([]byte, error) {
buf, err := r.ReadN(n)
r.RewindN(len(buf))
if err != nil {
return buf, err
}
return buf, nil
}
// Rewind resets the reader to the beginning of the buffered data.
func (r *StreamReader) Rewind() {
r.head = 0
}
// RewindN rewinds the reader by n bytes. If n is greater than the current
// buffer, the reader is rewound to the beginning of the buffer.
func (r *StreamReader) RewindN(n int) {
r.head -= min(n, r.head)
}
// Consume discards up to n bytes of previously read data from the beginning of
// the buffer. Once consumed, that data is no longer available for rewinding.
// If n is greater than the current buffer, the buffer is cleared. Consume
// never consume data from the underlying reader.
func (r *StreamReader) Consume(n int) {
n = min(n, len(r.buf))
r.buf = r.buf[n:]
r.head -= n
r.ttlConsumed += n
}
// Consumed returns the number of bytes consumed from the input reader.
func (r *StreamReader) Consumed() int {
return r.ttlConsumed
}

View File

@ -16,5 +16,5 @@ limitations under the License.
// +k8s:openapi-gen=true
// Package version supplies the type for version information collected at build time.
package version // import "k8s.io/apimachinery/pkg/version"
// Package version supplies the type for version information.
package version

View File

@ -20,15 +20,25 @@ package version
// TODO: Add []string of api versions supported? It's still unclear
// how we'll want to distribute that information.
type Info struct {
Major string `json:"major"`
Minor string `json:"minor"`
GitVersion string `json:"gitVersion"`
GitCommit string `json:"gitCommit"`
GitTreeState string `json:"gitTreeState"`
BuildDate string `json:"buildDate"`
GoVersion string `json:"goVersion"`
Compiler string `json:"compiler"`
Platform string `json:"platform"`
// Major is the major version of the binary version
Major string `json:"major"`
// Minor is the minor version of the binary version
Minor string `json:"minor"`
// EmulationMajor is the major version of the emulation version
EmulationMajor string `json:"emulationMajor,omitempty"`
// EmulationMinor is the minor version of the emulation version
EmulationMinor string `json:"emulationMinor,omitempty"`
// MinCompatibilityMajor is the major version of the minimum compatibility version
MinCompatibilityMajor string `json:"minCompatibilityMajor,omitempty"`
// MinCompatibilityMinor is the minor version of the minimum compatibility version
MinCompatibilityMinor string `json:"minCompatibilityMinor,omitempty"`
GitVersion string `json:"gitVersion"`
GitCommit string `json:"gitCommit"`
GitTreeState string `json:"gitTreeState"`
BuildDate string `json:"buildDate"`
GoVersion string `json:"goVersion"`
Compiler string `json:"compiler"`
Platform string `json:"platform"`
}
// String returns info as a human-friendly version string.

View File

@ -16,4 +16,4 @@ limitations under the License.
// Package watch contains a generic watchable interface, and a fake for
// testing code that uses the watch interface.
package watch // import "k8s.io/apimachinery/pkg/watch"
package watch

View File

@ -51,6 +51,7 @@ type Reporter interface {
// StreamWatcher turns any stream for which you can write a Decoder interface
// into a watch.Interface.
type StreamWatcher struct {
logger klog.Logger
sync.Mutex
source Decoder
reporter Reporter
@ -59,8 +60,16 @@ type StreamWatcher struct {
}
// NewStreamWatcher creates a StreamWatcher from the given decoder.
//
// Contextual logging: NewStreamWatcherWithLogger should be used instead of NewStreamWatcher in code which supports contextual logging.
func NewStreamWatcher(d Decoder, r Reporter) *StreamWatcher {
return NewStreamWatcherWithLogger(klog.Background(), d, r)
}
// NewStreamWatcherWithLogger creates a StreamWatcher from the given decoder and logger.
func NewStreamWatcherWithLogger(logger klog.Logger, d Decoder, r Reporter) *StreamWatcher {
sw := &StreamWatcher{
logger: logger,
source: d,
reporter: r,
// It's easy for a consumer to add buffering via an extra
@ -98,7 +107,7 @@ func (sw *StreamWatcher) Stop() {
// receive reads result from the decoder in a loop and sends down the result channel.
func (sw *StreamWatcher) receive() {
defer utilruntime.HandleCrash()
defer utilruntime.HandleCrashWithLogger(sw.logger)
defer close(sw.result)
defer sw.Stop()
for {
@ -108,10 +117,10 @@ func (sw *StreamWatcher) receive() {
case io.EOF:
// watch closed normally
case io.ErrUnexpectedEOF:
klog.V(1).Infof("Unexpected EOF during watch stream event decoding: %v", err)
sw.logger.V(1).Info("Unexpected EOF during watch stream event decoding", "err", err)
default:
if net.IsProbableEOF(err) || net.IsTimeout(err) {
klog.V(5).Infof("Unable to decode an event from the watch stream: %v", err)
sw.logger.V(5).Info("Unable to decode an event from the watch stream", "err", err)
} else {
select {
case <-sw.done:

View File

@ -23,6 +23,7 @@ import (
"k8s.io/klog/v2"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
)
// Interface can be implemented by anything that knows how to watch and report changes.
@ -103,21 +104,34 @@ func (w emptyWatch) ResultChan() <-chan Event {
// FakeWatcher lets you test anything that consumes a watch.Interface; threadsafe.
type FakeWatcher struct {
logger klog.Logger
result chan Event
stopped bool
sync.Mutex
}
var _ Interface = &FakeWatcher{}
// Contextual logging: NewFakeWithOptions and a logger in the FakeOptions should be used instead in code which supports contextual logging.
func NewFake() *FakeWatcher {
return NewFakeWithOptions(FakeOptions{})
}
// Contextual logging: NewFakeWithOptions and a logger in the FakeOptions should be used instead in code which supports contextual logging.
func NewFakeWithChanSize(size int, blocking bool) *FakeWatcher {
return NewFakeWithOptions(FakeOptions{ChannelSize: size})
}
func NewFakeWithOptions(options FakeOptions) *FakeWatcher {
return &FakeWatcher{
result: make(chan Event),
logger: ptr.Deref(options.Logger, klog.Background()),
result: make(chan Event, options.ChannelSize),
}
}
func NewFakeWithChanSize(size int, blocking bool) *FakeWatcher {
return &FakeWatcher{
result: make(chan Event, size),
}
type FakeOptions struct {
Logger *klog.Logger
ChannelSize int
}
// Stop implements Interface.Stop().
@ -125,7 +139,7 @@ func (f *FakeWatcher) Stop() {
f.Lock()
defer f.Unlock()
if !f.stopped {
klog.V(4).Infof("Stopping fake watcher.")
f.logger.V(4).Info("Stopping fake watcher")
close(f.result)
f.stopped = true
}
@ -176,13 +190,22 @@ func (f *FakeWatcher) Action(action EventType, obj runtime.Object) {
// RaceFreeFakeWatcher lets you test anything that consumes a watch.Interface; threadsafe.
type RaceFreeFakeWatcher struct {
logger klog.Logger
result chan Event
Stopped bool
sync.Mutex
}
var _ Interface = &RaceFreeFakeWatcher{}
// Contextual logging: RaceFreeFakeWatcherWithLogger should be used instead of NewRaceFreeFake in code which supports contextual logging.
func NewRaceFreeFake() *RaceFreeFakeWatcher {
return NewRaceFreeFakeWithLogger(klog.Background())
}
func NewRaceFreeFakeWithLogger(logger klog.Logger) *RaceFreeFakeWatcher {
return &RaceFreeFakeWatcher{
logger: logger,
result: make(chan Event, DefaultChanSize),
}
}
@ -192,7 +215,7 @@ func (f *RaceFreeFakeWatcher) Stop() {
f.Lock()
defer f.Unlock()
if !f.Stopped {
klog.V(4).Infof("Stopping fake watcher.")
f.logger.V(4).Info("Stopping fake watcher")
close(f.result)
f.Stopped = true
}