app: added helm tgz handle
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2021 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 dump
|
||||
|
||||
import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
var prettyPrintConfig = &spew.ConfigState{
|
||||
Indent: " ",
|
||||
DisableMethods: true,
|
||||
DisablePointerAddresses: true,
|
||||
DisableCapacities: true,
|
||||
}
|
||||
|
||||
// The config MUST NOT be changed because that could change the result of a hash operation
|
||||
var prettyPrintConfigForHash = &spew.ConfigState{
|
||||
Indent: " ",
|
||||
SortKeys: true,
|
||||
DisableMethods: true,
|
||||
SpewKeys: true,
|
||||
DisablePointerAddresses: true,
|
||||
DisableCapacities: true,
|
||||
}
|
||||
|
||||
// Pretty wrap the spew.Sdump with Indent, and disabled methods like error() and String()
|
||||
// The output may change over time, so for guaranteed output please take more direct control
|
||||
func Pretty(a interface{}) string {
|
||||
return prettyPrintConfig.Sdump(a)
|
||||
}
|
||||
|
||||
// ForHash keeps the original Spew.Sprintf format to ensure the same checksum
|
||||
func ForHash(a interface{}) string {
|
||||
return prettyPrintConfigForHash.Sprintf("%#v", a)
|
||||
}
|
||||
|
||||
// OneLine outputs the object in one line
|
||||
func OneLine(a interface{}) string {
|
||||
return prettyPrintConfig.Sprintf("%#v", a)
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
Copyright 2015 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 errors implements various utility functions and types around errors.
|
||||
package errors
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
Copyright 2015 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 errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
// MessageCountMap contains occurrence for each error message.
|
||||
// Deprecated: Not used anymore in the k8s.io codebase, use `errors.Join` instead.
|
||||
type MessageCountMap map[string]int
|
||||
|
||||
// Aggregate represents an object that contains multiple errors, but does not
|
||||
// necessarily have singular semantic meaning.
|
||||
// The aggregate can be used with `errors.Is()` to check for the occurrence of
|
||||
// a specific error type.
|
||||
// Errors.As() is not supported, because the caller presumably cares about a
|
||||
// specific error of potentially multiple that match the given type.
|
||||
type Aggregate interface {
|
||||
error
|
||||
Errors() []error
|
||||
Is(error) bool
|
||||
}
|
||||
|
||||
// NewAggregate converts a slice of errors into an Aggregate interface, which
|
||||
// is itself an implementation of the error interface. If the slice is empty,
|
||||
// this returns nil.
|
||||
// It will check if any of the element of input error list is nil, to avoid
|
||||
// nil pointer panic when call Error().
|
||||
func NewAggregate(errlist []error) Aggregate {
|
||||
if len(errlist) == 0 {
|
||||
return nil
|
||||
}
|
||||
// In case of input error list contains nil
|
||||
var errs []error
|
||||
for _, e := range errlist {
|
||||
if e != nil {
|
||||
errs = append(errs, e)
|
||||
}
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return aggregate(errs)
|
||||
}
|
||||
|
||||
// This helper implements the error and Errors interfaces. Keeping it private
|
||||
// prevents people from making an aggregate of 0 errors, which is not
|
||||
// an error, but does satisfy the error interface.
|
||||
type aggregate []error
|
||||
|
||||
// Error is part of the error interface.
|
||||
func (agg aggregate) Error() string {
|
||||
if len(agg) == 0 {
|
||||
// This should never happen, really.
|
||||
return ""
|
||||
}
|
||||
if len(agg) == 1 {
|
||||
return agg[0].Error()
|
||||
}
|
||||
seenerrs := sets.NewString()
|
||||
result := ""
|
||||
agg.visit(func(err error) bool {
|
||||
msg := err.Error()
|
||||
if seenerrs.Has(msg) {
|
||||
return false
|
||||
}
|
||||
seenerrs.Insert(msg)
|
||||
if len(seenerrs) > 1 {
|
||||
result += ", "
|
||||
}
|
||||
result += msg
|
||||
return false
|
||||
})
|
||||
if len(seenerrs) == 1 {
|
||||
return result
|
||||
}
|
||||
return "[" + result + "]"
|
||||
}
|
||||
|
||||
func (agg aggregate) Is(target error) bool {
|
||||
return agg.visit(func(err error) bool {
|
||||
return errors.Is(err, target)
|
||||
})
|
||||
}
|
||||
|
||||
func (agg aggregate) visit(f func(err error) bool) bool {
|
||||
for _, err := range agg {
|
||||
switch err := err.(type) {
|
||||
case aggregate:
|
||||
if match := err.visit(f); match {
|
||||
return match
|
||||
}
|
||||
case Aggregate:
|
||||
for _, nestedErr := range err.Errors() {
|
||||
if match := f(nestedErr); match {
|
||||
return match
|
||||
}
|
||||
}
|
||||
default:
|
||||
if match := f(err); match {
|
||||
return match
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Errors is part of the Aggregate interface.
|
||||
func (agg aggregate) Errors() []error {
|
||||
return []error(agg)
|
||||
}
|
||||
|
||||
// Matcher is used to match errors. Returns true if the error matches.
|
||||
type Matcher func(error) bool
|
||||
|
||||
// FilterOut removes all errors that match any of the matchers from the input
|
||||
// error. If the input is a singular error, only that error is tested. If the
|
||||
// input implements the Aggregate interface, the list of errors will be
|
||||
// processed recursively.
|
||||
//
|
||||
// This can be used, for example, to remove known-OK errors (such as io.EOF or
|
||||
// os.PathNotFound) from a list of errors.
|
||||
func FilterOut(err error, fns ...Matcher) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if agg, ok := err.(Aggregate); ok {
|
||||
return NewAggregate(filterErrors(agg.Errors(), fns...))
|
||||
}
|
||||
if !matchesError(err, fns...) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchesError returns true if any Matcher returns true
|
||||
func matchesError(err error, fns ...Matcher) bool {
|
||||
for _, fn := range fns {
|
||||
if fn(err) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// filterErrors returns any errors (or nested errors, if the list contains
|
||||
// nested Errors) for which all fns return false. If no errors
|
||||
// remain a nil list is returned. The resulting slice will have all
|
||||
// nested slices flattened as a side effect.
|
||||
func filterErrors(list []error, fns ...Matcher) []error {
|
||||
result := []error{}
|
||||
for _, err := range list {
|
||||
r := FilterOut(err, fns...)
|
||||
if r != nil {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Flatten takes an Aggregate, which may hold other Aggregates in arbitrary
|
||||
// nesting, and flattens them all into a single Aggregate, recursively.
|
||||
func Flatten(agg Aggregate) Aggregate {
|
||||
result := []error{}
|
||||
if agg == nil {
|
||||
return nil
|
||||
}
|
||||
for _, err := range agg.Errors() {
|
||||
if a, ok := err.(Aggregate); ok {
|
||||
r := Flatten(a)
|
||||
if r != nil {
|
||||
result = append(result, r.Errors()...)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
result = append(result, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewAggregate(result)
|
||||
}
|
||||
|
||||
// CreateAggregateFromMessageCountMap converts MessageCountMap Aggregate
|
||||
// Deprecated: Not used anymore in the k8s.io codebase, use `errors.Join` instead.
|
||||
func CreateAggregateFromMessageCountMap(m MessageCountMap) Aggregate {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]error, 0, len(m))
|
||||
for errStr, count := range m {
|
||||
var countStr string
|
||||
if count > 1 {
|
||||
countStr = fmt.Sprintf(" (repeated %v times)", count)
|
||||
}
|
||||
result = append(result, fmt.Errorf("%v%v", errStr, countStr))
|
||||
}
|
||||
return NewAggregate(result)
|
||||
}
|
||||
|
||||
// Reduce will return err or nil, if err is an Aggregate and only has one item,
|
||||
// the first item in the aggregate.
|
||||
func Reduce(err error) error {
|
||||
if agg, ok := err.(Aggregate); ok && err != nil {
|
||||
switch len(agg.Errors()) {
|
||||
case 1:
|
||||
return agg.Errors()[0]
|
||||
case 0:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// AggregateGoroutines runs the provided functions in parallel, stuffing all
|
||||
// non-nil errors into the returned Aggregate.
|
||||
// Returns nil if all the functions complete successfully.
|
||||
func AggregateGoroutines(funcs ...func() error) Aggregate {
|
||||
errChan := make(chan error, len(funcs))
|
||||
for _, f := range funcs {
|
||||
go func(f func() error) { errChan <- f() }(f)
|
||||
}
|
||||
errs := make([]error, 0)
|
||||
for i := 0; i < cap(errChan); i++ {
|
||||
if err := <-errChan; err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return NewAggregate(errs)
|
||||
}
|
||||
|
||||
// ErrPreconditionViolated is returned when the precondition is violated
|
||||
var ErrPreconditionViolated = errors.New("precondition is violated")
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
Copyright 2015 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 framer implements simple frame decoding techniques for an io.ReadCloser
|
||||
package framer
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
type lengthDelimitedFrameWriter struct {
|
||||
w io.Writer
|
||||
h [4]byte
|
||||
}
|
||||
|
||||
func NewLengthDelimitedFrameWriter(w io.Writer) io.Writer {
|
||||
return &lengthDelimitedFrameWriter{w: w}
|
||||
}
|
||||
|
||||
// Write writes a single frame to the nested writer, prepending it with the length
|
||||
// in bytes of data (as a 4 byte, bigendian uint32).
|
||||
func (w *lengthDelimitedFrameWriter) Write(data []byte) (int, error) {
|
||||
binary.BigEndian.PutUint32(w.h[:], uint32(len(data)))
|
||||
n, err := w.w.Write(w.h[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n != len(w.h) {
|
||||
return 0, io.ErrShortWrite
|
||||
}
|
||||
return w.w.Write(data)
|
||||
}
|
||||
|
||||
type lengthDelimitedFrameReader struct {
|
||||
r io.ReadCloser
|
||||
remaining int
|
||||
}
|
||||
|
||||
// NewLengthDelimitedFrameReader returns an io.Reader that will decode length-prefixed
|
||||
// frames off of a stream.
|
||||
//
|
||||
// The protocol is:
|
||||
//
|
||||
// stream: message ...
|
||||
// message: prefix body
|
||||
// prefix: 4 byte uint32 in BigEndian order, denotes length of body
|
||||
// body: bytes (0..prefix)
|
||||
//
|
||||
// If the buffer passed to Read is not long enough to contain an entire frame, io.ErrShortRead
|
||||
// will be returned along with the number of bytes read.
|
||||
func NewLengthDelimitedFrameReader(r io.ReadCloser) io.ReadCloser {
|
||||
return &lengthDelimitedFrameReader{r: r}
|
||||
}
|
||||
|
||||
// Read attempts to read an entire frame into data. If that is not possible, io.ErrShortBuffer
|
||||
// is returned and subsequent calls will attempt to read the last frame. A frame is complete when
|
||||
// err is nil.
|
||||
func (r *lengthDelimitedFrameReader) Read(data []byte) (int, error) {
|
||||
if r.remaining <= 0 {
|
||||
header := [4]byte{}
|
||||
n, err := io.ReadAtLeast(r.r, header[:4], 4)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n != 4 {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
frameLength := int(binary.BigEndian.Uint32(header[:]))
|
||||
r.remaining = frameLength
|
||||
}
|
||||
|
||||
expect := r.remaining
|
||||
max := expect
|
||||
if max > len(data) {
|
||||
max = len(data)
|
||||
}
|
||||
n, err := io.ReadAtLeast(r.r, data[:max], int(max))
|
||||
r.remaining -= n
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
if r.remaining > 0 {
|
||||
return n, io.ErrShortBuffer
|
||||
}
|
||||
if n != expect {
|
||||
return n, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *lengthDelimitedFrameReader) Close() error {
|
||||
return r.r.Close()
|
||||
}
|
||||
|
||||
type jsonFrameReader struct {
|
||||
r io.ReadCloser
|
||||
decoder *json.Decoder
|
||||
remaining []byte
|
||||
}
|
||||
|
||||
// NewJSONFramedReader returns an io.Reader that will decode individual JSON objects off
|
||||
// of a wire.
|
||||
//
|
||||
// The boundaries between each frame are valid JSON objects. A JSON parsing error will terminate
|
||||
// the read.
|
||||
func NewJSONFramedReader(r io.ReadCloser) io.ReadCloser {
|
||||
return &jsonFrameReader{
|
||||
r: r,
|
||||
decoder: json.NewDecoder(r),
|
||||
}
|
||||
}
|
||||
|
||||
// ReadFrame decodes the next JSON object in the stream, or returns an error. The returned
|
||||
// byte slice will be modified the next time ReadFrame is invoked and should not be altered.
|
||||
func (r *jsonFrameReader) Read(data []byte) (int, error) {
|
||||
// Return whatever remaining data exists from an in progress frame
|
||||
if n := len(r.remaining); n > 0 {
|
||||
if n <= len(data) {
|
||||
//nolint:staticcheck // SA4006,SA4010 underlying array of data is modified here.
|
||||
data = append(data[0:0], r.remaining...)
|
||||
r.remaining = nil
|
||||
return n, nil
|
||||
}
|
||||
|
||||
n = len(data)
|
||||
//nolint:staticcheck // SA4006,SA4010 underlying array of data is modified here.
|
||||
data = append(data[0:0], r.remaining[:n]...)
|
||||
r.remaining = r.remaining[n:]
|
||||
return n, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
// RawMessage#Unmarshal appends to data - we reset the slice down to 0 and will either see
|
||||
// data written to data, or be larger than data and a different array.
|
||||
m := json.RawMessage(data[:0])
|
||||
if err := r.decoder.Decode(&m); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// If capacity of data is less than length of the message, decoder will allocate a new slice
|
||||
// and set m to it, which means we need to copy the partial result back into data and preserve
|
||||
// the remaining result for subsequent reads.
|
||||
if len(m) > cap(data) {
|
||||
copy(data, m)
|
||||
r.remaining = m[len(data):]
|
||||
return len(data), io.ErrShortBuffer
|
||||
}
|
||||
|
||||
if len(m) > len(data) {
|
||||
// The bytes beyond len(data) were stored in data's underlying array, which we do
|
||||
// not own after this function returns.
|
||||
r.remaining = append([]byte(nil), m[len(data):]...)
|
||||
return len(data), io.ErrShortBuffer
|
||||
}
|
||||
|
||||
return len(m), nil
|
||||
}
|
||||
|
||||
func (r *jsonFrameReader) Close() error {
|
||||
return r.r.Close()
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by protoc-gen-gogo. DO NOT EDIT.
|
||||
// source: k8s.io/apimachinery/pkg/util/intstr/generated.proto
|
||||
|
||||
package intstr
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
|
||||
io "io"
|
||||
math_bits "math/bits"
|
||||
)
|
||||
|
||||
func (m *IntOrString) Reset() { *m = IntOrString{} }
|
||||
|
||||
func (m *IntOrString) Marshal() (dAtA []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalToSizedBuffer(dAtA[:size])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
}
|
||||
|
||||
func (m *IntOrString) MarshalTo(dAtA []byte) (int, error) {
|
||||
size := m.Size()
|
||||
return m.MarshalToSizedBuffer(dAtA[:size])
|
||||
}
|
||||
|
||||
func (m *IntOrString) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
||||
i := len(dAtA)
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
i -= len(m.StrVal)
|
||||
copy(dAtA[i:], m.StrVal)
|
||||
i = encodeVarintGenerated(dAtA, i, uint64(len(m.StrVal)))
|
||||
i--
|
||||
dAtA[i] = 0x1a
|
||||
i = encodeVarintGenerated(dAtA, i, uint64(m.IntVal))
|
||||
i--
|
||||
dAtA[i] = 0x10
|
||||
i = encodeVarintGenerated(dAtA, i, uint64(m.Type))
|
||||
i--
|
||||
dAtA[i] = 0x8
|
||||
return len(dAtA) - i, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
func (m *IntOrString) Size() (n int) {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
var l int
|
||||
_ = l
|
||||
n += 1 + sovGenerated(uint64(m.Type))
|
||||
n += 1 + sovGenerated(uint64(m.IntVal))
|
||||
l = len(m.StrVal)
|
||||
n += 1 + l + sovGenerated(uint64(l))
|
||||
return n
|
||||
}
|
||||
|
||||
func sovGenerated(x uint64) (n int) {
|
||||
return (math_bits.Len64(x|1) + 6) / 7
|
||||
}
|
||||
func sozGenerated(x uint64) (n int) {
|
||||
return sovGenerated(uint64((x << 1) ^ uint64((int64(x) >> 63))))
|
||||
}
|
||||
func (m *IntOrString) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fieldNum := int32(wire >> 3)
|
||||
wireType := int(wire & 0x7)
|
||||
if wireType == 4 {
|
||||
return fmt.Errorf("proto: IntOrString: wiretype end group for non-group")
|
||||
}
|
||||
if fieldNum <= 0 {
|
||||
return fmt.Errorf("proto: IntOrString: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||
}
|
||||
switch fieldNum {
|
||||
case 1:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Type", wireType)
|
||||
}
|
||||
m.Type = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
m.Type |= Type(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field IntVal", wireType)
|
||||
}
|
||||
m.IntVal = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
m.IntVal |= int32(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
case 3:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field StrVal", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.StrVal = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipGenerated(dAtA[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
if (iNdEx + skippy) > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
iNdEx += skippy
|
||||
}
|
||||
}
|
||||
|
||||
if iNdEx > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func skipGenerated(dAtA []byte) (n int, err error) {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
depth := 0
|
||||
for iNdEx < l {
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return 0, ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
wireType := int(wire & 0x7)
|
||||
switch wireType {
|
||||
case 0:
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return 0, ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
iNdEx++
|
||||
if dAtA[iNdEx-1] < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
iNdEx += 8
|
||||
case 2:
|
||||
var length int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return 0, ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
length |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if length < 0 {
|
||||
return 0, ErrInvalidLengthGenerated
|
||||
}
|
||||
iNdEx += length
|
||||
case 3:
|
||||
depth++
|
||||
case 4:
|
||||
if depth == 0 {
|
||||
return 0, ErrUnexpectedEndOfGroupGenerated
|
||||
}
|
||||
depth--
|
||||
case 5:
|
||||
iNdEx += 4
|
||||
default:
|
||||
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
|
||||
}
|
||||
if iNdEx < 0 {
|
||||
return 0, ErrInvalidLengthGenerated
|
||||
}
|
||||
if depth == 0 {
|
||||
return iNdEx, nil
|
||||
}
|
||||
}
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidLengthGenerated = fmt.Errorf("proto: negative length found during unmarshaling")
|
||||
ErrIntOverflowGenerated = fmt.Errorf("proto: integer overflow")
|
||||
ErrUnexpectedEndOfGroupGenerated = fmt.Errorf("proto: unexpected end of group")
|
||||
)
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
// This file was autogenerated by go-to-protobuf. Do not edit it manually!
|
||||
|
||||
syntax = "proto2";
|
||||
|
||||
package k8s.io.apimachinery.pkg.util.intstr;
|
||||
|
||||
// Package-wide variables from generator "generated".
|
||||
option go_package = "k8s.io/apimachinery/pkg/util/intstr";
|
||||
|
||||
// IntOrString is a type that can hold an int32 or a string. When used in
|
||||
// JSON or YAML marshalling and unmarshalling, it produces or consumes the
|
||||
// inner type. This allows you to have, for example, a JSON field that can
|
||||
// accept a name or number.
|
||||
// TODO: Rename to Int32OrString
|
||||
//
|
||||
// +protobuf=true
|
||||
// +protobuf.options.(gogoproto.goproto_stringer)=false
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:openapi-model-package=io.k8s.apimachinery.pkg.util.intstr
|
||||
message IntOrString {
|
||||
optional int64 type = 1;
|
||||
|
||||
optional int32 intVal = 2;
|
||||
|
||||
optional string strVal = 3;
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
//go:build kubernetes_protomessage_one_more_release
|
||||
// +build kubernetes_protomessage_one_more_release
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by go-to-protobuf. DO NOT EDIT.
|
||||
|
||||
package intstr
|
||||
|
||||
func (*IntOrString) ProtoMessage() {}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
//go:build !notest
|
||||
// +build !notest
|
||||
|
||||
/*
|
||||
Copyright 2020 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 intstr
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
// RandFill satisfies randfill.NativeSelfFiller
|
||||
func (intstr *IntOrString) RandFill(c randfill.Continue) {
|
||||
if intstr == nil {
|
||||
return
|
||||
}
|
||||
if c.Bool() {
|
||||
intstr.Type = Int
|
||||
c.Fill(&intstr.IntVal)
|
||||
intstr.StrVal = ""
|
||||
} else {
|
||||
intstr.Type = String
|
||||
intstr.IntVal = 0
|
||||
c.Fill(&intstr.StrVal)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure IntOrString implements fuzz.Interface
|
||||
var _ randfill.NativeSelfFiller = &IntOrString{}
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
Copyright 2014 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 intstr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// IntOrString is a type that can hold an int32 or a string. When used in
|
||||
// JSON or YAML marshalling and unmarshalling, it produces or consumes the
|
||||
// inner type. This allows you to have, for example, a JSON field that can
|
||||
// accept a name or number.
|
||||
// TODO: Rename to Int32OrString
|
||||
//
|
||||
// +protobuf=true
|
||||
// +protobuf.options.(gogoproto.goproto_stringer)=false
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:openapi-model-package=io.k8s.apimachinery.pkg.util.intstr
|
||||
type IntOrString struct {
|
||||
Type Type `protobuf:"varint,1,opt,name=type,casttype=Type"`
|
||||
IntVal int32 `protobuf:"varint,2,opt,name=intVal"`
|
||||
StrVal string `protobuf:"bytes,3,opt,name=strVal"`
|
||||
}
|
||||
|
||||
// Type represents the stored type of IntOrString.
|
||||
type Type int64
|
||||
|
||||
const (
|
||||
Int Type = iota // The IntOrString holds an int.
|
||||
String // The IntOrString holds a string.
|
||||
)
|
||||
|
||||
// FromInt creates an IntOrString object with an int32 value. It is
|
||||
// your responsibility not to call this method with a value greater
|
||||
// than int32.
|
||||
// Deprecated: use FromInt32 instead.
|
||||
func FromInt(val int) IntOrString {
|
||||
if val > math.MaxInt32 || val < math.MinInt32 {
|
||||
klog.Errorf("value: %d overflows int32\n%s\n", val, debug.Stack())
|
||||
}
|
||||
return IntOrString{Type: Int, IntVal: int32(val)}
|
||||
}
|
||||
|
||||
// FromInt32 creates an IntOrString object with an int32 value.
|
||||
func FromInt32(val int32) IntOrString {
|
||||
return IntOrString{Type: Int, IntVal: val}
|
||||
}
|
||||
|
||||
// FromString creates an IntOrString object with a string value.
|
||||
func FromString(val string) IntOrString {
|
||||
return IntOrString{Type: String, StrVal: val}
|
||||
}
|
||||
|
||||
// Parse the given string and try to convert it to an int32 integer before
|
||||
// setting it as a string value.
|
||||
func Parse(val string) IntOrString {
|
||||
i, err := strconv.ParseInt(val, 10, 32)
|
||||
if err != nil {
|
||||
return FromString(val)
|
||||
}
|
||||
return FromInt32(int32(i))
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaller interface.
|
||||
func (intstr *IntOrString) UnmarshalJSON(value []byte) error {
|
||||
if value[0] == '"' {
|
||||
intstr.Type = String
|
||||
return json.Unmarshal(value, &intstr.StrVal)
|
||||
}
|
||||
intstr.Type = Int
|
||||
return json.Unmarshal(value, &intstr.IntVal)
|
||||
}
|
||||
|
||||
func (intstr *IntOrString) UnmarshalCBOR(value []byte) error {
|
||||
if err := cbor.Unmarshal(value, &intstr.StrVal); err == nil {
|
||||
intstr.Type = String
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := cbor.Unmarshal(value, &intstr.IntVal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
intstr.Type = Int
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the string value, or the Itoa of the int value.
|
||||
func (intstr *IntOrString) String() string {
|
||||
if intstr == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
if intstr.Type == String {
|
||||
return intstr.StrVal
|
||||
}
|
||||
return strconv.Itoa(intstr.IntValue())
|
||||
}
|
||||
|
||||
// IntValue returns the IntVal if type Int, or if
|
||||
// it is a String, will attempt a conversion to int,
|
||||
// returning 0 if a parsing error occurs.
|
||||
func (intstr *IntOrString) IntValue() int {
|
||||
if intstr.Type == String {
|
||||
i, _ := strconv.Atoi(intstr.StrVal)
|
||||
return i
|
||||
}
|
||||
return int(intstr.IntVal)
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
func (intstr IntOrString) MarshalJSON() ([]byte, error) {
|
||||
switch intstr.Type {
|
||||
case Int:
|
||||
return json.Marshal(intstr.IntVal)
|
||||
case String:
|
||||
return json.Marshal(intstr.StrVal)
|
||||
default:
|
||||
return []byte{}, fmt.Errorf("impossible IntOrString.Type")
|
||||
}
|
||||
}
|
||||
|
||||
func (intstr IntOrString) MarshalCBOR() ([]byte, error) {
|
||||
switch intstr.Type {
|
||||
case Int:
|
||||
return cbor.Marshal(intstr.IntVal)
|
||||
case String:
|
||||
return cbor.Marshal(intstr.StrVal)
|
||||
default:
|
||||
return nil, fmt.Errorf("impossible IntOrString.Type")
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAPISchemaType is used by the kube-openapi generator when constructing
|
||||
// the OpenAPI spec of this type.
|
||||
//
|
||||
// See: https://github.com/kubernetes/kube-openapi/tree/master/pkg/generators
|
||||
func (IntOrString) OpenAPISchemaType() []string { return []string{"string"} }
|
||||
|
||||
// OpenAPISchemaFormat is used by the kube-openapi generator when constructing
|
||||
// the OpenAPI spec of this type.
|
||||
func (IntOrString) OpenAPISchemaFormat() string { return "int-or-string" }
|
||||
|
||||
// OpenAPIV3OneOfTypes is used by the kube-openapi generator when constructing
|
||||
// the OpenAPI v3 spec of this type.
|
||||
func (IntOrString) OpenAPIV3OneOfTypes() []string { return []string{"integer", "string"} }
|
||||
|
||||
func ValueOrDefault(intOrPercent *IntOrString, defaultValue IntOrString) *IntOrString {
|
||||
if intOrPercent == nil {
|
||||
return &defaultValue
|
||||
}
|
||||
return intOrPercent
|
||||
}
|
||||
|
||||
// GetScaledValueFromIntOrPercent is meant to replace GetValueFromIntOrPercent.
|
||||
// This method returns a scaled value from an IntOrString type. If the IntOrString
|
||||
// is a percentage string value it's treated as a percentage and scaled appropriately
|
||||
// in accordance to the total, if it's an int value it's treated as a simple value and
|
||||
// if it is a string value which is either non-numeric or numeric but lacking a trailing '%' it returns an error.
|
||||
func GetScaledValueFromIntOrPercent(intOrPercent *IntOrString, total int, roundUp bool) (int, error) {
|
||||
if intOrPercent == nil {
|
||||
return 0, errors.New("nil value for IntOrString")
|
||||
}
|
||||
value, isPercent, err := getIntOrPercentValueSafely(intOrPercent)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid value for IntOrString: %v", err)
|
||||
}
|
||||
if isPercent {
|
||||
if roundUp {
|
||||
value = int(math.Ceil(float64(value) * (float64(total)) / 100))
|
||||
} else {
|
||||
value = int(math.Floor(float64(value) * (float64(total)) / 100))
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GetValueFromIntOrPercent was deprecated in favor of
|
||||
// GetScaledValueFromIntOrPercent. This method was treating all int as a numeric value and all
|
||||
// strings with or without a percent symbol as a percentage value.
|
||||
// Deprecated
|
||||
func GetValueFromIntOrPercent(intOrPercent *IntOrString, total int, roundUp bool) (int, error) {
|
||||
if intOrPercent == nil {
|
||||
return 0, errors.New("nil value for IntOrString")
|
||||
}
|
||||
value, isPercent, err := getIntOrPercentValue(intOrPercent)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid value for IntOrString: %v", err)
|
||||
}
|
||||
if isPercent {
|
||||
if roundUp {
|
||||
value = int(math.Ceil(float64(value) * (float64(total)) / 100))
|
||||
} else {
|
||||
value = int(math.Floor(float64(value) * (float64(total)) / 100))
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// getIntOrPercentValue is a legacy function and only meant to be called by GetValueFromIntOrPercent
|
||||
// For a more correct implementation call getIntOrPercentSafely
|
||||
func getIntOrPercentValue(intOrStr *IntOrString) (int, bool, error) {
|
||||
switch intOrStr.Type {
|
||||
case Int:
|
||||
return intOrStr.IntValue(), false, nil
|
||||
case String:
|
||||
s := strings.Replace(intOrStr.StrVal, "%", "", -1)
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, false, fmt.Errorf("invalid value %q: %v", intOrStr.StrVal, err)
|
||||
}
|
||||
return int(v), true, nil
|
||||
}
|
||||
return 0, false, fmt.Errorf("invalid type: neither int nor percentage")
|
||||
}
|
||||
|
||||
func getIntOrPercentValueSafely(intOrStr *IntOrString) (int, bool, error) {
|
||||
switch intOrStr.Type {
|
||||
case Int:
|
||||
return intOrStr.IntValue(), false, nil
|
||||
case String:
|
||||
isPercent := false
|
||||
s := intOrStr.StrVal
|
||||
if strings.HasSuffix(s, "%") {
|
||||
isPercent = true
|
||||
s = strings.TrimSuffix(intOrStr.StrVal, "%")
|
||||
} else {
|
||||
return 0, false, fmt.Errorf("invalid type: string is not a percentage")
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, false, fmt.Errorf("invalid value %q: %v", intOrStr.StrVal, err)
|
||||
}
|
||||
return int(v), isPercent, nil
|
||||
}
|
||||
return 0, false, fmt.Errorf("invalid type: neither int nor percentage")
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by openapi-gen. DO NOT EDIT.
|
||||
|
||||
package intstr
|
||||
|
||||
// OpenAPIModelName returns the OpenAPI model name for this type.
|
||||
func (in IntOrString) OpenAPIModelName() string {
|
||||
return "io.k8s.apimachinery.pkg.util.intstr.IntOrString"
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
Copyright 2015 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"
|
||||
|
||||
kjson "sigs.k8s.io/json"
|
||||
)
|
||||
|
||||
// NewEncoder delegates to json.NewEncoder
|
||||
// It is only here so this package can be a drop-in for common encoding/json uses
|
||||
func NewEncoder(w io.Writer) *json.Encoder {
|
||||
return json.NewEncoder(w)
|
||||
}
|
||||
|
||||
// Marshal delegates to json.Marshal
|
||||
// It is only here so this package can be a drop-in for common encoding/json uses
|
||||
func Marshal(v interface{}) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// limit recursive depth to prevent stack overflow errors
|
||||
const maxDepth = 10000
|
||||
|
||||
// Unmarshal unmarshals the given data.
|
||||
// Object keys are case-sensitive.
|
||||
// Numbers decoded into interface{} fields are converted to int64 or float64.
|
||||
func Unmarshal(data []byte, v interface{}) error {
|
||||
return kjson.UnmarshalCaseSensitivePreserveInts(data, v)
|
||||
}
|
||||
|
||||
// ConvertInterfaceNumbers converts any json.Number values to int64 or float64.
|
||||
// Values which are map[string]interface{} or []interface{} are recursively visited
|
||||
func ConvertInterfaceNumbers(v *interface{}, depth int) error {
|
||||
var err error
|
||||
switch v2 := (*v).(type) {
|
||||
case json.Number:
|
||||
*v, err = convertNumber(v2)
|
||||
case map[string]interface{}:
|
||||
err = ConvertMapNumbers(v2, depth+1)
|
||||
case []interface{}:
|
||||
err = ConvertSliceNumbers(v2, depth+1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ConvertMapNumbers traverses the map, converting any json.Number values to int64 or float64.
|
||||
// values which are map[string]interface{} or []interface{} are recursively visited
|
||||
func ConvertMapNumbers(m map[string]interface{}, depth int) error {
|
||||
if depth > maxDepth {
|
||||
return fmt.Errorf("exceeded max depth of %d", maxDepth)
|
||||
}
|
||||
|
||||
var err error
|
||||
for k, v := range m {
|
||||
switch v := v.(type) {
|
||||
case json.Number:
|
||||
m[k], err = convertNumber(v)
|
||||
case map[string]interface{}:
|
||||
err = ConvertMapNumbers(v, depth+1)
|
||||
case []interface{}:
|
||||
err = ConvertSliceNumbers(v, depth+1)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConvertSliceNumbers traverses the slice, converting any json.Number values to int64 or float64.
|
||||
// values which are map[string]interface{} or []interface{} are recursively visited
|
||||
func ConvertSliceNumbers(s []interface{}, depth int) error {
|
||||
if depth > maxDepth {
|
||||
return fmt.Errorf("exceeded max depth of %d", maxDepth)
|
||||
}
|
||||
|
||||
var err error
|
||||
for i, v := range s {
|
||||
switch v := v.(type) {
|
||||
case json.Number:
|
||||
s[i], err = convertNumber(v)
|
||||
case map[string]interface{}:
|
||||
err = ConvertMapNumbers(v, depth+1)
|
||||
case []interface{}:
|
||||
err = ConvertSliceNumbers(v, depth+1)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertNumber converts a json.Number to an int64 or float64, or returns an error
|
||||
func convertNumber(n json.Number) (interface{}, error) {
|
||||
// Attempt to convert to an int64 first
|
||||
if i, err := n.Int64(); err == nil {
|
||||
return i, nil
|
||||
}
|
||||
// Return a float64 (default json.Decode() behavior)
|
||||
// An overflow will return an error
|
||||
return n.Float64()
|
||||
}
|
||||
+7018
File diff suppressed because it is too large
Load Diff
+108
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright 2021 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 managedfields
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/typed"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// ExtractInto extracts the applied configuration state from object for fieldManager
|
||||
// into applyConfiguration. If no managed fields are found for the given fieldManager,
|
||||
// no error is returned, but applyConfiguration is left unpopulated. It is possible
|
||||
// that no managed fields were found for the fieldManager because other field managers
|
||||
// have taken ownership of all the fields previously owned by the fieldManager. It is
|
||||
// also possible the fieldManager never owned fields.
|
||||
//
|
||||
// The provided object MUST bo a root resource object since subresource objects
|
||||
// do not contain their own managed fields. For example, an autoscaling.Scale
|
||||
// object read from a "scale" subresource does not have any managed fields and so
|
||||
// cannot be used as the object.
|
||||
//
|
||||
// If the fields of a subresource are a subset of the fields of the root object,
|
||||
// and their field paths and types are exactly the same, then ExtractInto can be
|
||||
// called with the root resource as the object and the subresource as the
|
||||
// applyConfiguration. This works for "status", obviously, because status is
|
||||
// represented by the exact same object as the root resource. This does NOT
|
||||
// work, for example, with the "scale" subresources of Deployment, ReplicaSet and
|
||||
// StatefulSet. While the spec.replicas, status.replicas fields are in the same
|
||||
// exact field path locations as they are in autoscaling.Scale, the selector
|
||||
// fields are in different locations, and are a different type.
|
||||
func ExtractInto(object runtime.Object, objectType typed.ParseableType, fieldManager string, applyConfiguration interface{}, subresource string) error {
|
||||
typedObj, err := toTyped(object, objectType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error converting obj to typed: %w", err)
|
||||
}
|
||||
|
||||
accessor, err := meta.Accessor(object)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error accessing metadata: %w", err)
|
||||
}
|
||||
fieldsEntry, ok := findManagedFields(accessor, fieldManager, subresource)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
fieldset := &fieldpath.Set{}
|
||||
err = fieldset.FromJSON(bytes.NewReader(fieldsEntry.FieldsV1.Raw))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshalling FieldsV1 to JSON: %w", err)
|
||||
}
|
||||
|
||||
u := typedObj.ExtractItems(fieldset.Leaves()).AsValue().Unstructured()
|
||||
m, ok := u.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to convert managed fields for %s to unstructured, expected map, got %T", fieldManager, u)
|
||||
}
|
||||
|
||||
// set the type meta manually if it doesn't exist to avoid missing kind errors
|
||||
// when decoding from unstructured JSON
|
||||
if _, ok := m["kind"]; !ok && object.GetObjectKind().GroupVersionKind().Kind != "" {
|
||||
m["kind"] = object.GetObjectKind().GroupVersionKind().Kind
|
||||
m["apiVersion"] = object.GetObjectKind().GroupVersionKind().GroupVersion().String()
|
||||
}
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(m, applyConfiguration); err != nil {
|
||||
return fmt.Errorf("error extracting into obj from unstructured: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findManagedFields(accessor metav1.Object, fieldManager string, subresource string) (metav1.ManagedFieldsEntry, bool) {
|
||||
objManagedFields := accessor.GetManagedFields()
|
||||
for _, mf := range objManagedFields {
|
||||
if mf.Manager == fieldManager && mf.Operation == metav1.ManagedFieldsOperationApply && mf.Subresource == subresource {
|
||||
return mf, true
|
||||
}
|
||||
}
|
||||
return metav1.ManagedFieldsEntry{}, false
|
||||
}
|
||||
|
||||
func toTyped(obj runtime.Object, objectType typed.ParseableType) (*typed.TypedValue, error) {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
return objectType.FromUnstructured(o.Object)
|
||||
default:
|
||||
return objectType.FromStructured(o)
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright 2018 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 managedfields
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields/internal"
|
||||
)
|
||||
|
||||
// FieldManager updates the managed fields and merges applied
|
||||
// configurations.
|
||||
type FieldManager = internal.FieldManager
|
||||
|
||||
// NewDefaultFieldManager creates a new FieldManager that merges apply requests
|
||||
// and update managed fields for other types of requests.
|
||||
func NewDefaultFieldManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, hub schema.GroupVersion, subresource string, resetFields map[fieldpath.APIVersion]fieldpath.Filter) (*FieldManager, error) {
|
||||
f, err := internal.NewStructuredMergeManager(typeConverter, objectConverter, objectDefaulter, kind.GroupVersion(), hub, resetFields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create field manager: %v", err)
|
||||
}
|
||||
return internal.NewDefaultFieldManager(f, typeConverter, objectConverter, objectCreater, kind, subresource), nil
|
||||
}
|
||||
|
||||
// NewDefaultCRDFieldManager creates a new FieldManager specifically for
|
||||
// CRDs. This allows for the possibility of fields which are not defined
|
||||
// in models, as well as having no models defined at all.
|
||||
func NewDefaultCRDFieldManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, hub schema.GroupVersion, subresource string, resetFields map[fieldpath.APIVersion]fieldpath.Filter) (_ *FieldManager, err error) {
|
||||
f, err := internal.NewCRDStructuredMergeManager(typeConverter, objectConverter, objectDefaulter, kind.GroupVersion(), hub, resetFields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create field manager: %v", err)
|
||||
}
|
||||
return internal.NewDefaultFieldManager(f, typeConverter, objectConverter, objectCreater, kind, subresource), nil
|
||||
}
|
||||
|
||||
func ValidateManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) error {
|
||||
_, err := internal.DecodeManagedFields(encodedManagedFields)
|
||||
return err
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
Copyright 2018 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 managedfields
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kube-openapi/pkg/schemaconv"
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
smdschema "sigs.k8s.io/structured-merge-diff/v6/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/typed"
|
||||
)
|
||||
|
||||
// groupVersionKindExtensionKey is the key used to lookup the
|
||||
// GroupVersionKind value for an object definition from the
|
||||
// definition's "extensions" map.
|
||||
const groupVersionKindExtensionKey = "x-kubernetes-group-version-kind"
|
||||
|
||||
// GvkParser contains a Parser that allows introspecting the schema.
|
||||
type GvkParser struct {
|
||||
gvks map[schema.GroupVersionKind]string
|
||||
parser typed.Parser
|
||||
}
|
||||
|
||||
// Type returns a helper which can produce objects of the given type. Any
|
||||
// errors are deferred until a further function is called.
|
||||
func (p *GvkParser) Type(gvk schema.GroupVersionKind) *typed.ParseableType {
|
||||
typeName, ok := p.gvks[gvk]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
t := p.parser.Type(typeName)
|
||||
return &t
|
||||
}
|
||||
|
||||
// NewGVKParser builds a GVKParser from a proto.Models. This
|
||||
// will automatically find the proper version of the object, and the
|
||||
// corresponding schema information.
|
||||
func NewGVKParser(models proto.Models, preserveUnknownFields bool) (*GvkParser, error) {
|
||||
typeSchema, err := schemaconv.ToSchemaWithPreserveUnknownFields(models, preserveUnknownFields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert models to schema: %v", err)
|
||||
}
|
||||
parser := GvkParser{
|
||||
gvks: map[schema.GroupVersionKind]string{},
|
||||
}
|
||||
parser.parser = typed.Parser{Schema: smdschema.Schema{Types: typeSchema.Types}}
|
||||
for _, modelName := range models.ListModels() {
|
||||
model := models.LookupModel(modelName)
|
||||
if model == nil {
|
||||
panic(fmt.Sprintf("ListModels returns a model that can't be looked-up for: %v", modelName))
|
||||
}
|
||||
gvkList := parseGroupVersionKind(model)
|
||||
for _, gvk := range gvkList {
|
||||
if len(gvk.Kind) > 0 {
|
||||
_, ok := parser.gvks[gvk]
|
||||
if ok {
|
||||
return nil, fmt.Errorf("duplicate entry for %v", gvk)
|
||||
}
|
||||
parser.gvks[gvk] = modelName
|
||||
}
|
||||
}
|
||||
}
|
||||
return &parser, nil
|
||||
}
|
||||
|
||||
// Get and parse GroupVersionKind from the extension. Returns empty if it doesn't have one.
|
||||
func parseGroupVersionKind(s proto.Schema) []schema.GroupVersionKind {
|
||||
extensions := s.GetExtensions()
|
||||
|
||||
gvkListResult := []schema.GroupVersionKind{}
|
||||
|
||||
// Get the extensions
|
||||
gvkExtension, ok := extensions[groupVersionKindExtensionKey]
|
||||
if !ok {
|
||||
return []schema.GroupVersionKind{}
|
||||
}
|
||||
|
||||
// gvk extension must be a list of at least 1 element.
|
||||
gvkList, ok := gvkExtension.([]interface{})
|
||||
if !ok {
|
||||
return []schema.GroupVersionKind{}
|
||||
}
|
||||
|
||||
for _, gvk := range gvkList {
|
||||
// gvk extension list must be a map with group, version, and
|
||||
// kind fields
|
||||
gvkMap, ok := gvk.(map[interface{}]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
group, ok := gvkMap["group"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
version, ok := gvkMap["version"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
kind, ok := gvkMap["kind"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
gvkListResult = append(gvkListResult, schema.GroupVersionKind{
|
||||
Group: group,
|
||||
Version: version,
|
||||
Kind: kind,
|
||||
})
|
||||
}
|
||||
|
||||
return gvkListResult
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2020 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 internal
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AtMostEvery will never run the method more than once every specified
|
||||
// duration.
|
||||
type AtMostEvery struct {
|
||||
delay time.Duration
|
||||
lastCall time.Time
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// NewAtMostEvery creates a new AtMostEvery, that will run the method at
|
||||
// most every given duration.
|
||||
func NewAtMostEvery(delay time.Duration) *AtMostEvery {
|
||||
return &AtMostEvery{
|
||||
delay: delay,
|
||||
}
|
||||
}
|
||||
|
||||
// updateLastCall returns true if the lastCall time has been updated,
|
||||
// false if it was too early.
|
||||
func (s *AtMostEvery) updateLastCall() bool {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
if time.Since(s.lastCall) < s.delay {
|
||||
return false
|
||||
}
|
||||
s.lastCall = time.Now()
|
||||
return true
|
||||
}
|
||||
|
||||
// Do will run the method if enough time has passed, and return true.
|
||||
// Otherwise, it does nothing and returns false.
|
||||
func (s *AtMostEvery) Do(fn func()) bool {
|
||||
if !s.updateLastCall() {
|
||||
return false
|
||||
}
|
||||
fn()
|
||||
return true
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright 2019 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type buildManagerInfoManager struct {
|
||||
fieldManager Manager
|
||||
groupVersion schema.GroupVersion
|
||||
subresource string
|
||||
}
|
||||
|
||||
var _ Manager = &buildManagerInfoManager{}
|
||||
|
||||
// NewBuildManagerInfoManager creates a new Manager that converts the manager name into a unique identifier
|
||||
// combining operation and version for update requests, and just operation for apply requests.
|
||||
func NewBuildManagerInfoManager(f Manager, gv schema.GroupVersion, subresource string) Manager {
|
||||
return &buildManagerInfoManager{
|
||||
fieldManager: f,
|
||||
groupVersion: gv,
|
||||
subresource: subresource,
|
||||
}
|
||||
}
|
||||
|
||||
// Update implements Manager.
|
||||
func (f *buildManagerInfoManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
|
||||
manager, err := f.buildManagerInfo(manager, metav1.ManagedFieldsOperationUpdate)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to build manager identifier: %v", err)
|
||||
}
|
||||
return f.fieldManager.Update(liveObj, newObj, managed, manager)
|
||||
}
|
||||
|
||||
// Apply implements Manager.
|
||||
func (f *buildManagerInfoManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
|
||||
manager, err := f.buildManagerInfo(manager, metav1.ManagedFieldsOperationApply)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to build manager identifier: %v", err)
|
||||
}
|
||||
return f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force)
|
||||
}
|
||||
|
||||
func (f *buildManagerInfoManager) buildManagerInfo(prefix string, operation metav1.ManagedFieldsOperationType) (string, error) {
|
||||
managerInfo := metav1.ManagedFieldsEntry{
|
||||
Manager: prefix,
|
||||
Operation: operation,
|
||||
APIVersion: f.groupVersion.String(),
|
||||
Subresource: f.subresource,
|
||||
}
|
||||
if managerInfo.Manager == "" {
|
||||
managerInfo.Manager = "unknown"
|
||||
}
|
||||
return BuildManagerIdentifier(&managerInfo)
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
Copyright 2019 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
type capManagersManager struct {
|
||||
fieldManager Manager
|
||||
maxUpdateManagers int
|
||||
oldUpdatesManagerName string
|
||||
}
|
||||
|
||||
var _ Manager = &capManagersManager{}
|
||||
|
||||
// NewCapManagersManager creates a new wrapped FieldManager which ensures that the number of managers from updates
|
||||
// does not exceed maxUpdateManagers, by merging some of the oldest entries on each update.
|
||||
func NewCapManagersManager(fieldManager Manager, maxUpdateManagers int) Manager {
|
||||
return &capManagersManager{
|
||||
fieldManager: fieldManager,
|
||||
maxUpdateManagers: maxUpdateManagers,
|
||||
oldUpdatesManagerName: "ancient-changes",
|
||||
}
|
||||
}
|
||||
|
||||
// Update implements Manager.
|
||||
func (f *capManagersManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
|
||||
object, managed, err := f.fieldManager.Update(liveObj, newObj, managed, manager)
|
||||
if err != nil {
|
||||
return object, managed, err
|
||||
}
|
||||
if managed, err = f.capUpdateManagers(managed); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to cap update managers: %v", err)
|
||||
}
|
||||
return object, managed, nil
|
||||
}
|
||||
|
||||
// Apply implements Manager.
|
||||
func (f *capManagersManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
|
||||
return f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
|
||||
}
|
||||
|
||||
// capUpdateManagers merges a number of the oldest update entries into versioned buckets,
|
||||
// such that the number of entries from updates does not exceed f.maxUpdateManagers.
|
||||
func (f *capManagersManager) capUpdateManagers(managed Managed) (newManaged Managed, err error) {
|
||||
// Gather all entries from updates
|
||||
updaters := []string{}
|
||||
for manager, fields := range managed.Fields() {
|
||||
if !fields.Applied() {
|
||||
updaters = append(updaters, manager)
|
||||
}
|
||||
}
|
||||
if len(updaters) <= f.maxUpdateManagers {
|
||||
return managed, nil
|
||||
}
|
||||
|
||||
// If we have more than the maximum, sort the update entries by time, oldest first.
|
||||
sort.Slice(updaters, func(i, j int) bool {
|
||||
iTime, jTime, iSeconds, jSeconds := managed.Times()[updaters[i]], managed.Times()[updaters[j]], int64(0), int64(0)
|
||||
if iTime != nil {
|
||||
iSeconds = iTime.Unix()
|
||||
}
|
||||
if jTime != nil {
|
||||
jSeconds = jTime.Unix()
|
||||
}
|
||||
if iSeconds != jSeconds {
|
||||
return iSeconds < jSeconds
|
||||
}
|
||||
return updaters[i] < updaters[j]
|
||||
})
|
||||
|
||||
// Merge the oldest updaters with versioned bucket managers until the number of updaters is under the cap
|
||||
versionToFirstManager := map[string]string{}
|
||||
for i, length := 0, len(updaters); i < len(updaters) && length > f.maxUpdateManagers; i++ {
|
||||
manager := updaters[i]
|
||||
vs := managed.Fields()[manager]
|
||||
time := managed.Times()[manager]
|
||||
version := string(vs.APIVersion())
|
||||
|
||||
// Create a new manager identifier for the versioned bucket entry.
|
||||
// The version for this manager comes from the version of the update being merged into the bucket.
|
||||
bucket, err := BuildManagerIdentifier(&metav1.ManagedFieldsEntry{
|
||||
Manager: f.oldUpdatesManagerName,
|
||||
Operation: metav1.ManagedFieldsOperationUpdate,
|
||||
APIVersion: version,
|
||||
})
|
||||
if err != nil {
|
||||
return managed, fmt.Errorf("failed to create bucket manager for version %v: %v", version, err)
|
||||
}
|
||||
|
||||
// Merge the fieldets if this is not the first time the version was seen.
|
||||
// Otherwise just record the manager name in versionToFirstManager
|
||||
if first, ok := versionToFirstManager[version]; ok {
|
||||
// If the bucket doesn't exists yet, create one.
|
||||
if _, ok := managed.Fields()[bucket]; !ok {
|
||||
s := managed.Fields()[first]
|
||||
delete(managed.Fields(), first)
|
||||
managed.Fields()[bucket] = s
|
||||
}
|
||||
|
||||
managed.Fields()[bucket] = fieldpath.NewVersionedSet(vs.Set().Union(managed.Fields()[bucket].Set()), vs.APIVersion(), vs.Applied())
|
||||
delete(managed.Fields(), manager)
|
||||
length--
|
||||
|
||||
// Use the time from the update being merged into the bucket, since it is more recent.
|
||||
managed.Times()[bucket] = time
|
||||
} else {
|
||||
versionToFirstManager[version] = manager
|
||||
}
|
||||
}
|
||||
|
||||
return managed, nil
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
Copyright 2019 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 internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/merge"
|
||||
)
|
||||
|
||||
// NewConflictError returns an error including details on the requests apply conflicts
|
||||
func NewConflictError(conflicts merge.Conflicts) *errors.StatusError {
|
||||
causes := []metav1.StatusCause{}
|
||||
for _, conflict := range conflicts {
|
||||
causes = append(causes, metav1.StatusCause{
|
||||
Type: metav1.CauseTypeFieldManagerConflict,
|
||||
Message: fmt.Sprintf("conflict with %v", printManager(conflict.Manager)),
|
||||
Field: conflict.Path.String(),
|
||||
})
|
||||
}
|
||||
return errors.NewApplyConflict(causes, getConflictMessage(conflicts))
|
||||
}
|
||||
|
||||
func getConflictMessage(conflicts merge.Conflicts) string {
|
||||
if len(conflicts) == 1 {
|
||||
return fmt.Sprintf("Apply failed with 1 conflict: conflict with %v: %v", printManager(conflicts[0].Manager), conflicts[0].Path)
|
||||
}
|
||||
|
||||
m := map[string][]fieldpath.Path{}
|
||||
for _, conflict := range conflicts {
|
||||
m[conflict.Manager] = append(m[conflict.Manager], conflict.Path)
|
||||
}
|
||||
|
||||
uniqueManagers := []string{}
|
||||
for manager := range m {
|
||||
uniqueManagers = append(uniqueManagers, manager)
|
||||
}
|
||||
|
||||
// Print conflicts by sorted managers.
|
||||
sort.Strings(uniqueManagers)
|
||||
|
||||
messages := []string{}
|
||||
for _, manager := range uniqueManagers {
|
||||
messages = append(messages, fmt.Sprintf("conflicts with %v:", printManager(manager)))
|
||||
for _, path := range m[manager] {
|
||||
messages = append(messages, fmt.Sprintf("- %v", path))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("Apply failed with %d conflicts: %s", len(conflicts), strings.Join(messages, "\n"))
|
||||
}
|
||||
|
||||
func printManager(manager string) string {
|
||||
encodedManager := &metav1.ManagedFieldsEntry{}
|
||||
if err := json.Unmarshal([]byte(manager), encodedManager); err != nil {
|
||||
return fmt.Sprintf("%q", manager)
|
||||
}
|
||||
managerStr := fmt.Sprintf("%q", encodedManager.Manager)
|
||||
if encodedManager.Subresource != "" {
|
||||
managerStr = fmt.Sprintf("%s with subresource %q", managerStr, encodedManager.Subresource)
|
||||
}
|
||||
if encodedManager.Operation == metav1.ManagedFieldsOperationUpdate {
|
||||
if encodedManager.Time == nil {
|
||||
return fmt.Sprintf("%s using %v", managerStr, encodedManager.APIVersion)
|
||||
}
|
||||
return fmt.Sprintf("%s using %v at %v", managerStr, encodedManager.APIVersion, encodedManager.Time.UTC().Format(time.RFC3339))
|
||||
}
|
||||
return managerStr
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
Copyright 2022 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/merge"
|
||||
)
|
||||
|
||||
// DefaultMaxUpdateManagers defines the default maximum retained number of managedFields entries from updates
|
||||
// if the number of update managers exceeds this, the oldest entries will be merged until the number is below the maximum.
|
||||
// TODO(jennybuckley): Determine if this is really the best value. Ideally we wouldn't unnecessarily merge too many entries.
|
||||
const DefaultMaxUpdateManagers int = 10
|
||||
|
||||
// DefaultTrackOnCreateProbability defines the default probability that the field management of an object
|
||||
// starts being tracked from the object's creation, instead of from the first time the object is applied to.
|
||||
const DefaultTrackOnCreateProbability float32 = 1
|
||||
|
||||
var atMostEverySecond = NewAtMostEvery(time.Second)
|
||||
|
||||
// FieldManager updates the managed fields and merges applied
|
||||
// configurations.
|
||||
type FieldManager struct {
|
||||
fieldManager Manager
|
||||
subresource string
|
||||
}
|
||||
|
||||
// NewFieldManager creates a new FieldManager that decodes, manages, then re-encodes managedFields
|
||||
// on update and apply requests.
|
||||
func NewFieldManager(f Manager, subresource string) *FieldManager {
|
||||
return &FieldManager{fieldManager: f, subresource: subresource}
|
||||
}
|
||||
|
||||
// newDefaultFieldManager is a helper function which wraps a Manager with certain default logic.
|
||||
func NewDefaultFieldManager(f Manager, typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, subresource string) *FieldManager {
|
||||
return NewFieldManager(
|
||||
NewVersionCheckManager(
|
||||
NewLastAppliedUpdater(
|
||||
NewLastAppliedManager(
|
||||
NewProbabilisticSkipNonAppliedManager(
|
||||
NewCapManagersManager(
|
||||
NewBuildManagerInfoManager(
|
||||
NewManagedFieldsUpdater(
|
||||
NewStripMetaManager(f),
|
||||
), kind.GroupVersion(), subresource,
|
||||
), DefaultMaxUpdateManagers,
|
||||
), objectCreater, DefaultTrackOnCreateProbability,
|
||||
), typeConverter, objectConverter, kind.GroupVersion(),
|
||||
),
|
||||
), kind,
|
||||
), subresource,
|
||||
)
|
||||
}
|
||||
|
||||
func decodeLiveOrNew(liveObj, newObj runtime.Object, ignoreManagedFieldsFromRequestObject bool) (Managed, error) {
|
||||
liveAccessor, err := meta.Accessor(liveObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We take the managedFields of the live object in case the request tries to
|
||||
// manually set managedFields via a subresource.
|
||||
if ignoreManagedFieldsFromRequestObject {
|
||||
return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields()))
|
||||
}
|
||||
|
||||
// If the object doesn't have metadata, we should just return without trying to
|
||||
// set the managedFields at all, so creates/updates/patches will work normally.
|
||||
newAccessor, err := meta.Accessor(newObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isResetManagedFields(newAccessor.GetManagedFields()) {
|
||||
return NewEmptyManaged(), nil
|
||||
}
|
||||
|
||||
// If the managed field is empty or we failed to decode it,
|
||||
// let's try the live object. This is to prevent clients who
|
||||
// don't understand managedFields from deleting it accidentally.
|
||||
managed, err := DecodeManagedFields(newAccessor.GetManagedFields())
|
||||
if err != nil || len(managed.Fields()) == 0 {
|
||||
return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields()))
|
||||
}
|
||||
return managed, nil
|
||||
}
|
||||
|
||||
func emptyManagedFieldsOnErr(managed Managed, err error) (Managed, error) {
|
||||
if err != nil {
|
||||
return NewEmptyManaged(), nil
|
||||
}
|
||||
return managed, nil
|
||||
}
|
||||
|
||||
// Update is used when the object has already been merged (non-apply
|
||||
// use-case), and simply updates the managed fields in the output
|
||||
// object.
|
||||
func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (object runtime.Object, err error) {
|
||||
// First try to decode the managed fields provided in the update,
|
||||
// This is necessary to allow directly updating managed fields.
|
||||
isSubresource := f.subresource != ""
|
||||
managed, err := decodeLiveOrNew(liveObj, newObj, isSubresource)
|
||||
if err != nil {
|
||||
return newObj, nil
|
||||
}
|
||||
|
||||
RemoveObjectManagedFields(newObj)
|
||||
|
||||
if object, managed, err = f.fieldManager.Update(liveObj, newObj, managed, manager); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = EncodeObjectManagedFields(object, managed); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode managed fields: %v", err)
|
||||
}
|
||||
|
||||
return object, nil
|
||||
}
|
||||
|
||||
// UpdateNoErrors is the same as Update, but it will not return
|
||||
// errors. If an error happens, the object is returned with
|
||||
// managedFields cleared.
|
||||
func (f *FieldManager) UpdateNoErrors(liveObj, newObj runtime.Object, manager string) runtime.Object {
|
||||
obj, err := f.Update(liveObj, newObj, manager)
|
||||
if err != nil {
|
||||
atMostEverySecond.Do(func() {
|
||||
ns, name := "unknown", "unknown"
|
||||
if accessor, err := meta.Accessor(newObj); err == nil {
|
||||
ns = accessor.GetNamespace()
|
||||
name = accessor.GetName()
|
||||
}
|
||||
|
||||
klog.ErrorS(err, "[SHOULD NOT HAPPEN] failed to update managedFields", "versionKind",
|
||||
newObj.GetObjectKind().GroupVersionKind(), "namespace", ns, "name", name)
|
||||
})
|
||||
// Explicitly remove managedFields on failure, so that
|
||||
// we can't have garbage in it.
|
||||
RemoveObjectManagedFields(newObj)
|
||||
return newObj
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// Returns true if the managedFields indicate that the user is trying to
|
||||
// reset the managedFields, i.e. if the list is non-nil but empty, or if
|
||||
// the list has one empty item.
|
||||
func isResetManagedFields(managedFields []metav1.ManagedFieldsEntry) bool {
|
||||
if len(managedFields) == 0 {
|
||||
return managedFields != nil
|
||||
}
|
||||
|
||||
if len(managedFields) == 1 {
|
||||
return reflect.DeepEqual(managedFields[0], metav1.ManagedFieldsEntry{})
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Apply is used when server-side apply is called, as it merges the
|
||||
// object and updates the managed fields.
|
||||
func (f *FieldManager) Apply(liveObj, appliedObj runtime.Object, manager string, force bool) (object runtime.Object, err error) {
|
||||
// If the object doesn't have metadata, apply isn't allowed.
|
||||
accessor, err := meta.Accessor(liveObj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't get accessor: %v", err)
|
||||
}
|
||||
|
||||
// Decode the managed fields in the live object, since it isn't allowed in the patch.
|
||||
managed, err := DecodeManagedFields(accessor.GetManagedFields())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode managed fields: %v", err)
|
||||
}
|
||||
|
||||
object, managed, err = f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force)
|
||||
if err != nil {
|
||||
if conflicts, ok := err.(merge.Conflicts); ok {
|
||||
return nil, NewConflictError(conflicts)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = EncodeObjectManagedFields(object, managed); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode managed fields: %v", err)
|
||||
}
|
||||
|
||||
return object, nil
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2018 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 internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
// EmptyFields represents a set with no paths
|
||||
// It looks like metav1.Fields{Raw: []byte("{}")}
|
||||
var EmptyFields = func() metav1.FieldsV1 {
|
||||
f, err := SetToFields(*fieldpath.NewSet())
|
||||
if err != nil {
|
||||
panic("should never happen")
|
||||
}
|
||||
return f
|
||||
}()
|
||||
|
||||
// FieldsToSet creates a set paths from an input trie of fields
|
||||
func FieldsToSet(f metav1.FieldsV1) (s fieldpath.Set, err error) {
|
||||
err = s.FromJSON(bytes.NewReader(f.Raw))
|
||||
return s, err
|
||||
}
|
||||
|
||||
// SetToFields creates a trie of fields from an input set of paths
|
||||
func SetToFields(s fieldpath.Set) (f metav1.FieldsV1, err error) {
|
||||
f.Raw, err = s.ToJSON()
|
||||
return f, err
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2022 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// LastAppliedConfigAnnotation is the annotation used to store the previous
|
||||
// configuration of a resource for use in a three way diff by UpdateApplyAnnotation.
|
||||
//
|
||||
// This is a copy of the corev1 annotation since we don't want to depend on the whole package.
|
||||
const LastAppliedConfigAnnotation = "kubectl.kubernetes.io/last-applied-configuration"
|
||||
|
||||
// SetLastApplied sets the last-applied annotation the given value in
|
||||
// the object.
|
||||
func SetLastApplied(obj runtime.Object, value string) error {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("couldn't get accessor: %v", err))
|
||||
}
|
||||
var annotations = accessor.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[LastAppliedConfigAnnotation] = value
|
||||
if err := apimachineryvalidation.ValidateAnnotationsSize(annotations); err != nil {
|
||||
delete(annotations, LastAppliedConfigAnnotation)
|
||||
}
|
||||
accessor.SetAnnotations(annotations)
|
||||
return nil
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
Copyright 2020 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 internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/merge"
|
||||
)
|
||||
|
||||
type lastAppliedManager struct {
|
||||
fieldManager Manager
|
||||
typeConverter TypeConverter
|
||||
objectConverter runtime.ObjectConvertor
|
||||
groupVersion schema.GroupVersion
|
||||
}
|
||||
|
||||
var _ Manager = &lastAppliedManager{}
|
||||
|
||||
// NewLastAppliedManager converts the client-side apply annotation to
|
||||
// server-side apply managed fields
|
||||
func NewLastAppliedManager(fieldManager Manager, typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, groupVersion schema.GroupVersion) Manager {
|
||||
return &lastAppliedManager{
|
||||
fieldManager: fieldManager,
|
||||
typeConverter: typeConverter,
|
||||
objectConverter: objectConverter,
|
||||
groupVersion: groupVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// Update implements Manager.
|
||||
func (f *lastAppliedManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
|
||||
return f.fieldManager.Update(liveObj, newObj, managed, manager)
|
||||
}
|
||||
|
||||
// Apply will consider the last-applied annotation
|
||||
// for upgrading an object managed by client-side apply to server-side apply
|
||||
// without conflicts.
|
||||
func (f *lastAppliedManager) Apply(liveObj, newObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
|
||||
newLiveObj, newManaged, newErr := f.fieldManager.Apply(liveObj, newObj, managed, manager, force)
|
||||
// Upgrade the client-side apply annotation only from kubectl server-side-apply.
|
||||
// To opt-out of this behavior, users may specify a different field manager.
|
||||
if manager != "kubectl" {
|
||||
return newLiveObj, newManaged, newErr
|
||||
}
|
||||
|
||||
// Check if we have conflicts
|
||||
if newErr == nil {
|
||||
return newLiveObj, newManaged, newErr
|
||||
}
|
||||
conflicts, ok := newErr.(merge.Conflicts)
|
||||
if !ok {
|
||||
return newLiveObj, newManaged, newErr
|
||||
}
|
||||
conflictSet := conflictsToSet(conflicts)
|
||||
|
||||
// Check if conflicts are allowed due to client-side apply,
|
||||
// and if so, then force apply
|
||||
allowedConflictSet, err := f.allowedConflictsFromLastApplied(liveObj)
|
||||
if err != nil {
|
||||
return newLiveObj, newManaged, newErr
|
||||
}
|
||||
if !conflictSet.Difference(allowedConflictSet).Empty() {
|
||||
newConflicts := conflictsDifference(conflicts, allowedConflictSet)
|
||||
return newLiveObj, newManaged, newConflicts
|
||||
}
|
||||
|
||||
return f.fieldManager.Apply(liveObj, newObj, managed, manager, true)
|
||||
}
|
||||
|
||||
func (f *lastAppliedManager) allowedConflictsFromLastApplied(liveObj runtime.Object) (*fieldpath.Set, error) {
|
||||
var accessor, err = meta.Accessor(liveObj)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("couldn't get accessor: %v", err))
|
||||
}
|
||||
|
||||
// If there is no client-side apply annotation, then there is nothing to do
|
||||
var annotations = accessor.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return nil, fmt.Errorf("no last applied annotation")
|
||||
}
|
||||
var lastApplied, ok = annotations[LastAppliedConfigAnnotation]
|
||||
if !ok || lastApplied == "" {
|
||||
return nil, fmt.Errorf("no last applied annotation")
|
||||
}
|
||||
|
||||
liveObjVersioned, err := f.objectConverter.ConvertToVersion(liveObj, f.groupVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert live obj to versioned: %v", err)
|
||||
}
|
||||
|
||||
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert live obj to typed: %v", err)
|
||||
}
|
||||
|
||||
var lastAppliedObj = &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
err = json.Unmarshal([]byte(lastApplied), lastAppliedObj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode last applied obj: %v in '%s'", err, lastApplied)
|
||||
}
|
||||
|
||||
if lastAppliedObj.GetAPIVersion() != f.groupVersion.String() {
|
||||
return nil, fmt.Errorf("expected version of last applied to match live object '%s', but got '%s': %v", f.groupVersion.String(), lastAppliedObj.GetAPIVersion(), err)
|
||||
}
|
||||
|
||||
lastAppliedObjTyped, err := f.typeConverter.ObjectToTyped(lastAppliedObj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert last applied to typed: %v", err)
|
||||
}
|
||||
|
||||
lastAppliedObjFieldSet, err := lastAppliedObjTyped.ToFieldSet()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create fieldset for last applied object: %v", err)
|
||||
}
|
||||
|
||||
comparison, err := lastAppliedObjTyped.Compare(liveObjTyped)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compare last applied object and live object: %v", err)
|
||||
}
|
||||
|
||||
// Remove fields in last applied that are different, added, or missing in
|
||||
// the live object.
|
||||
// Because last-applied fields don't match the live object fields,
|
||||
// then we don't own these fields.
|
||||
lastAppliedObjFieldSet = lastAppliedObjFieldSet.
|
||||
Difference(comparison.Modified).
|
||||
Difference(comparison.Added).
|
||||
Difference(comparison.Removed)
|
||||
|
||||
return lastAppliedObjFieldSet, nil
|
||||
}
|
||||
|
||||
// TODO: replace with merge.Conflicts.ToSet()
|
||||
func conflictsToSet(conflicts merge.Conflicts) *fieldpath.Set {
|
||||
conflictSet := fieldpath.NewSet()
|
||||
for _, conflict := range []merge.Conflict(conflicts) {
|
||||
conflictSet.Insert(conflict.Path)
|
||||
}
|
||||
return conflictSet
|
||||
}
|
||||
|
||||
func conflictsDifference(conflicts merge.Conflicts, s *fieldpath.Set) merge.Conflicts {
|
||||
newConflicts := []merge.Conflict{}
|
||||
for _, conflict := range []merge.Conflict(conflicts) {
|
||||
if !s.Has(conflict.Path) {
|
||||
newConflicts = append(newConflicts, conflict)
|
||||
}
|
||||
}
|
||||
return newConflicts
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright 2020 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
type lastAppliedUpdater struct {
|
||||
fieldManager Manager
|
||||
}
|
||||
|
||||
var _ Manager = &lastAppliedUpdater{}
|
||||
|
||||
// NewLastAppliedUpdater sets the client-side apply annotation up to date with
|
||||
// server-side apply managed fields
|
||||
func NewLastAppliedUpdater(fieldManager Manager) Manager {
|
||||
return &lastAppliedUpdater{
|
||||
fieldManager: fieldManager,
|
||||
}
|
||||
}
|
||||
|
||||
// Update implements Manager.
|
||||
func (f *lastAppliedUpdater) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
|
||||
return f.fieldManager.Update(liveObj, newObj, managed, manager)
|
||||
}
|
||||
|
||||
// server-side apply managed fields
|
||||
func (f *lastAppliedUpdater) Apply(liveObj, newObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
|
||||
liveObj, managed, err := f.fieldManager.Apply(liveObj, newObj, managed, manager, force)
|
||||
if err != nil {
|
||||
return liveObj, managed, err
|
||||
}
|
||||
|
||||
// Sync the client-side apply annotation only from kubectl server-side apply.
|
||||
// To opt-out of this behavior, users may specify a different field manager.
|
||||
//
|
||||
// If the client-side apply annotation doesn't exist,
|
||||
// then continue because we have no annotation to update
|
||||
if manager == "kubectl" && hasLastApplied(liveObj) {
|
||||
lastAppliedValue, err := buildLastApplied(newObj)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to build last-applied annotation: %v", err)
|
||||
}
|
||||
err = SetLastApplied(liveObj, lastAppliedValue)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to set last-applied annotation: %v", err)
|
||||
}
|
||||
}
|
||||
return liveObj, managed, err
|
||||
}
|
||||
|
||||
func hasLastApplied(obj runtime.Object) bool {
|
||||
var accessor, err = meta.Accessor(obj)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("couldn't get accessor: %v", err))
|
||||
}
|
||||
var annotations = accessor.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
lastApplied, ok := annotations[LastAppliedConfigAnnotation]
|
||||
return ok && len(lastApplied) > 0
|
||||
}
|
||||
|
||||
func buildLastApplied(obj runtime.Object) (string, error) {
|
||||
obj = obj.DeepCopyObject()
|
||||
|
||||
var accessor, err = meta.Accessor(obj)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("couldn't get accessor: %v", err))
|
||||
}
|
||||
|
||||
// Remove the annotation from the object before encoding the object
|
||||
var annotations = accessor.GetAnnotations()
|
||||
delete(annotations, LastAppliedConfigAnnotation)
|
||||
accessor.SetAnnotations(annotations)
|
||||
|
||||
lastApplied, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("couldn't encode object into last applied annotation: %v", err)
|
||||
}
|
||||
return string(lastApplied), nil
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
Copyright 2018 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 internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
// ManagedInterface groups a fieldpath.ManagedFields together with the timestamps associated with each operation.
|
||||
type ManagedInterface interface {
|
||||
// Fields gets the fieldpath.ManagedFields.
|
||||
Fields() fieldpath.ManagedFields
|
||||
|
||||
// Times gets the timestamps associated with each operation.
|
||||
Times() map[string]*metav1.Time
|
||||
}
|
||||
|
||||
type managedStruct struct {
|
||||
fields fieldpath.ManagedFields
|
||||
times map[string]*metav1.Time
|
||||
}
|
||||
|
||||
var _ ManagedInterface = &managedStruct{}
|
||||
|
||||
// Fields implements ManagedInterface.
|
||||
func (m *managedStruct) Fields() fieldpath.ManagedFields {
|
||||
return m.fields
|
||||
}
|
||||
|
||||
// Times implements ManagedInterface.
|
||||
func (m *managedStruct) Times() map[string]*metav1.Time {
|
||||
return m.times
|
||||
}
|
||||
|
||||
// NewEmptyManaged creates an empty ManagedInterface.
|
||||
func NewEmptyManaged() ManagedInterface {
|
||||
return NewManaged(fieldpath.ManagedFields{}, map[string]*metav1.Time{})
|
||||
}
|
||||
|
||||
// NewManaged creates a ManagedInterface from a fieldpath.ManagedFields and the timestamps associated with each operation.
|
||||
func NewManaged(f fieldpath.ManagedFields, t map[string]*metav1.Time) ManagedInterface {
|
||||
return &managedStruct{
|
||||
fields: f,
|
||||
times: t,
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveObjectManagedFields removes the ManagedFields from the object
|
||||
// before we merge so that it doesn't appear in the ManagedFields
|
||||
// recursively.
|
||||
func RemoveObjectManagedFields(obj runtime.Object) {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("couldn't get accessor: %v", err))
|
||||
}
|
||||
accessor.SetManagedFields(nil)
|
||||
}
|
||||
|
||||
// EncodeObjectManagedFields converts and stores the fieldpathManagedFields into the objects ManagedFields
|
||||
func EncodeObjectManagedFields(obj runtime.Object, managed ManagedInterface) error {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("couldn't get accessor: %v", err))
|
||||
}
|
||||
|
||||
encodedManagedFields, err := encodeManagedFields(managed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert back managed fields to API: %v", err)
|
||||
}
|
||||
accessor.SetManagedFields(encodedManagedFields)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeManagedFields converts ManagedFields from the wire format (api format)
|
||||
// to the format used by sigs.k8s.io/structured-merge-diff
|
||||
func DecodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (ManagedInterface, error) {
|
||||
managed := managedStruct{}
|
||||
managed.fields = make(fieldpath.ManagedFields, len(encodedManagedFields))
|
||||
managed.times = make(map[string]*metav1.Time, len(encodedManagedFields))
|
||||
|
||||
for i, encodedVersionedSet := range encodedManagedFields {
|
||||
switch encodedVersionedSet.Operation {
|
||||
case metav1.ManagedFieldsOperationApply, metav1.ManagedFieldsOperationUpdate:
|
||||
default:
|
||||
return nil, fmt.Errorf("operation must be `Apply` or `Update`")
|
||||
}
|
||||
if len(encodedVersionedSet.APIVersion) < 1 {
|
||||
return nil, fmt.Errorf("apiVersion must not be empty")
|
||||
}
|
||||
switch encodedVersionedSet.FieldsType {
|
||||
case "FieldsV1":
|
||||
// Valid case.
|
||||
case "":
|
||||
return nil, fmt.Errorf("missing fieldsType in managed fields entry %d", i)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid fieldsType %q in managed fields entry %d", encodedVersionedSet.FieldsType, i)
|
||||
}
|
||||
manager, err := BuildManagerIdentifier(&encodedVersionedSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding manager from %v: %v", encodedVersionedSet, err)
|
||||
}
|
||||
managed.fields[manager], err = decodeVersionedSet(&encodedVersionedSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding versioned set from %v: %v", encodedVersionedSet, err)
|
||||
}
|
||||
managed.times[manager] = encodedVersionedSet.Time
|
||||
}
|
||||
return &managed, nil
|
||||
}
|
||||
|
||||
// BuildManagerIdentifier creates a manager identifier string from a ManagedFieldsEntry
|
||||
func BuildManagerIdentifier(encodedManager *metav1.ManagedFieldsEntry) (manager string, err error) {
|
||||
encodedManagerCopy := *encodedManager
|
||||
|
||||
// Never include fields type in the manager identifier
|
||||
encodedManagerCopy.FieldsType = ""
|
||||
|
||||
// Never include the fields in the manager identifier
|
||||
encodedManagerCopy.FieldsV1 = nil
|
||||
|
||||
// Never include the time in the manager identifier
|
||||
encodedManagerCopy.Time = nil
|
||||
|
||||
// For appliers, don't include the APIVersion in the manager identifier,
|
||||
// so it will always have the same manager identifier each time it applied.
|
||||
if encodedManager.Operation == metav1.ManagedFieldsOperationApply {
|
||||
encodedManagerCopy.APIVersion = ""
|
||||
}
|
||||
|
||||
// Use the remaining fields to build the manager identifier
|
||||
b, err := json.Marshal(&encodedManagerCopy)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error marshalling manager identifier: %v", err)
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func decodeVersionedSet(encodedVersionedSet *metav1.ManagedFieldsEntry) (versionedSet fieldpath.VersionedSet, err error) {
|
||||
fields := EmptyFields
|
||||
if encodedVersionedSet.FieldsV1 != nil {
|
||||
fields = *encodedVersionedSet.FieldsV1
|
||||
}
|
||||
set, err := FieldsToSet(fields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding set: %v", err)
|
||||
}
|
||||
return fieldpath.NewVersionedSet(&set, fieldpath.APIVersion(encodedVersionedSet.APIVersion), encodedVersionedSet.Operation == metav1.ManagedFieldsOperationApply), nil
|
||||
}
|
||||
|
||||
// encodeManagedFields converts ManagedFields from the format used by
|
||||
// sigs.k8s.io/structured-merge-diff to the wire format (api format)
|
||||
func encodeManagedFields(managed ManagedInterface) (encodedManagedFields []metav1.ManagedFieldsEntry, err error) {
|
||||
if len(managed.Fields()) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
encodedManagedFields = []metav1.ManagedFieldsEntry{}
|
||||
for manager := range managed.Fields() {
|
||||
versionedSet := managed.Fields()[manager]
|
||||
v, err := encodeManagerVersionedSet(manager, versionedSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error encoding versioned set for %v: %v", manager, err)
|
||||
}
|
||||
if t, ok := managed.Times()[manager]; ok {
|
||||
v.Time = t
|
||||
}
|
||||
encodedManagedFields = append(encodedManagedFields, *v)
|
||||
}
|
||||
return sortEncodedManagedFields(encodedManagedFields)
|
||||
}
|
||||
|
||||
func sortEncodedManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (sortedManagedFields []metav1.ManagedFieldsEntry, err error) {
|
||||
sort.Slice(encodedManagedFields, func(i, j int) bool {
|
||||
p, q := encodedManagedFields[i], encodedManagedFields[j]
|
||||
|
||||
if p.Operation != q.Operation {
|
||||
return p.Operation < q.Operation
|
||||
}
|
||||
|
||||
pSeconds, qSeconds := int64(0), int64(0)
|
||||
if p.Time != nil {
|
||||
pSeconds = p.Time.Unix()
|
||||
}
|
||||
if q.Time != nil {
|
||||
qSeconds = q.Time.Unix()
|
||||
}
|
||||
if pSeconds != qSeconds {
|
||||
return pSeconds < qSeconds
|
||||
}
|
||||
|
||||
if p.Manager != q.Manager {
|
||||
return p.Manager < q.Manager
|
||||
}
|
||||
|
||||
if p.APIVersion != q.APIVersion {
|
||||
return p.APIVersion < q.APIVersion
|
||||
}
|
||||
return p.Subresource < q.Subresource
|
||||
})
|
||||
|
||||
return encodedManagedFields, nil
|
||||
}
|
||||
|
||||
func encodeManagerVersionedSet(manager string, versionedSet fieldpath.VersionedSet) (encodedVersionedSet *metav1.ManagedFieldsEntry, err error) {
|
||||
encodedVersionedSet = &metav1.ManagedFieldsEntry{}
|
||||
|
||||
// Get as many fields as we can from the manager identifier
|
||||
err = json.Unmarshal([]byte(manager), encodedVersionedSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling manager identifier %v: %v", manager, err)
|
||||
}
|
||||
|
||||
// Get the APIVersion, Operation, and Fields from the VersionedSet
|
||||
encodedVersionedSet.APIVersion = string(versionedSet.APIVersion())
|
||||
if versionedSet.Applied() {
|
||||
encodedVersionedSet.Operation = metav1.ManagedFieldsOperationApply
|
||||
}
|
||||
encodedVersionedSet.FieldsType = "FieldsV1"
|
||||
fields, err := SetToFields(*versionedSet.Set())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error encoding set: %v", err)
|
||||
}
|
||||
encodedVersionedSet.FieldsV1 = &fields
|
||||
|
||||
return encodedVersionedSet, nil
|
||||
}
|
||||
Generated
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2020 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 internal
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
type managedFieldsUpdater struct {
|
||||
fieldManager Manager
|
||||
}
|
||||
|
||||
var _ Manager = &managedFieldsUpdater{}
|
||||
|
||||
// NewManagedFieldsUpdater is responsible for updating the managedfields
|
||||
// in the object, updating the time of the operation as necessary. For
|
||||
// updates, it uses a hard-coded manager to detect if things have
|
||||
// changed, and swaps back the correct manager after the operation is
|
||||
// done.
|
||||
func NewManagedFieldsUpdater(fieldManager Manager) Manager {
|
||||
return &managedFieldsUpdater{
|
||||
fieldManager: fieldManager,
|
||||
}
|
||||
}
|
||||
|
||||
// Update implements Manager.
|
||||
func (f *managedFieldsUpdater) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
|
||||
self := "current-operation"
|
||||
object, managed, err := f.fieldManager.Update(liveObj, newObj, managed, self)
|
||||
if err != nil {
|
||||
return object, managed, err
|
||||
}
|
||||
|
||||
// If the current operation took any fields from anything, it means the object changed,
|
||||
// so update the timestamp of the managedFieldsEntry and merge with any previous updates from the same manager
|
||||
if vs, ok := managed.Fields()[self]; ok {
|
||||
delete(managed.Fields(), self)
|
||||
|
||||
if previous, ok := managed.Fields()[manager]; ok {
|
||||
managed.Fields()[manager] = fieldpath.NewVersionedSet(vs.Set().Union(previous.Set()), vs.APIVersion(), vs.Applied())
|
||||
} else {
|
||||
managed.Fields()[manager] = vs
|
||||
}
|
||||
|
||||
managed.Times()[manager] = &metav1.Time{Time: time.Now().UTC()}
|
||||
}
|
||||
|
||||
return object, managed, nil
|
||||
}
|
||||
|
||||
// Apply implements Manager.
|
||||
func (f *managedFieldsUpdater) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
|
||||
object, managed, err := f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
|
||||
if err != nil {
|
||||
return object, managed, err
|
||||
}
|
||||
if object != nil {
|
||||
managed.Times()[fieldManager] = &metav1.Time{Time: time.Now().UTC()}
|
||||
} else {
|
||||
object = liveObj.DeepCopyObject()
|
||||
RemoveObjectManagedFields(object)
|
||||
}
|
||||
return object, managed, nil
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright 2022 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 internal
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
// Managed groups a fieldpath.ManagedFields together with the timestamps associated with each operation.
|
||||
type Managed interface {
|
||||
// Fields gets the fieldpath.ManagedFields.
|
||||
Fields() fieldpath.ManagedFields
|
||||
|
||||
// Times gets the timestamps associated with each operation.
|
||||
Times() map[string]*metav1.Time
|
||||
}
|
||||
|
||||
// Manager updates the managed fields and merges applied configurations.
|
||||
type Manager interface {
|
||||
// Update is used when the object has already been merged (non-apply
|
||||
// use-case), and simply updates the managed fields in the output
|
||||
// object.
|
||||
// * `liveObj` is not mutated by this function
|
||||
// * `newObj` may be mutated by this function
|
||||
// Returns the new object with managedFields removed, and the object's new
|
||||
// proposed managedFields separately.
|
||||
Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error)
|
||||
|
||||
// Apply is used when server-side apply is called, as it merges the
|
||||
// object and updates the managed fields.
|
||||
// * `liveObj` is not mutated by this function
|
||||
// * `newObj` may be mutated by this function
|
||||
// Returns the new object with managedFields removed, and the object's new
|
||||
// proposed managedFields separately.
|
||||
Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error)
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
Copyright 2018 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 internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/value"
|
||||
)
|
||||
|
||||
const (
|
||||
// Field indicates that the content of this path element is a field's name
|
||||
Field = "f"
|
||||
|
||||
// Value indicates that the content of this path element is a field's value
|
||||
Value = "v"
|
||||
|
||||
// Index indicates that the content of this path element is an index in an array
|
||||
Index = "i"
|
||||
|
||||
// Key indicates that the content of this path element is a key value map
|
||||
Key = "k"
|
||||
|
||||
// Separator separates the type of a path element from the contents
|
||||
Separator = ":"
|
||||
)
|
||||
|
||||
// NewPathElement parses a serialized path element
|
||||
func NewPathElement(s string) (fieldpath.PathElement, error) {
|
||||
split := strings.SplitN(s, Separator, 2)
|
||||
if len(split) < 2 {
|
||||
return fieldpath.PathElement{}, fmt.Errorf("missing colon: %v", s)
|
||||
}
|
||||
switch split[0] {
|
||||
case Field:
|
||||
return fieldpath.PathElement{
|
||||
FieldName: &split[1],
|
||||
}, nil
|
||||
case Value:
|
||||
val, err := value.FromJSON([]byte(split[1]))
|
||||
if err != nil {
|
||||
return fieldpath.PathElement{}, err
|
||||
}
|
||||
return fieldpath.PathElement{
|
||||
Value: &val,
|
||||
}, nil
|
||||
case Index:
|
||||
i, err := strconv.Atoi(split[1])
|
||||
if err != nil {
|
||||
return fieldpath.PathElement{}, err
|
||||
}
|
||||
return fieldpath.PathElement{
|
||||
Index: &i,
|
||||
}, nil
|
||||
case Key:
|
||||
kv := map[string]json.RawMessage{}
|
||||
err := json.Unmarshal([]byte(split[1]), &kv)
|
||||
if err != nil {
|
||||
return fieldpath.PathElement{}, err
|
||||
}
|
||||
fields := value.FieldList{}
|
||||
for k, v := range kv {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fieldpath.PathElement{}, err
|
||||
}
|
||||
val, err := value.FromJSON(b)
|
||||
if err != nil {
|
||||
return fieldpath.PathElement{}, err
|
||||
}
|
||||
|
||||
fields = append(fields, value.Field{
|
||||
Name: k,
|
||||
Value: val,
|
||||
})
|
||||
}
|
||||
return fieldpath.PathElement{
|
||||
Key: &fields,
|
||||
}, nil
|
||||
default:
|
||||
// Ignore unknown key types
|
||||
return fieldpath.PathElement{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// PathElementString serializes a path element
|
||||
func PathElementString(pe fieldpath.PathElement) (string, error) {
|
||||
switch {
|
||||
case pe.FieldName != nil:
|
||||
return Field + Separator + *pe.FieldName, nil
|
||||
case pe.Key != nil:
|
||||
kv := map[string]json.RawMessage{}
|
||||
for _, k := range *pe.Key {
|
||||
b, err := value.ToJSON(k.Value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m := json.RawMessage{}
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
kv[k.Name] = m
|
||||
}
|
||||
b, err := json.Marshal(kv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return Key + ":" + string(b), nil
|
||||
case pe.Value != nil:
|
||||
b, err := value.ToJSON(*pe.Value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return Value + ":" + string(b), nil
|
||||
case pe.Index != nil:
|
||||
return Index + ":" + strconv.Itoa(*pe.Index), nil
|
||||
default:
|
||||
return "", errors.New("Invalid type of path element")
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/typed"
|
||||
)
|
||||
|
||||
type schemeTypeConverter struct {
|
||||
scheme *runtime.Scheme
|
||||
parser *typed.Parser
|
||||
}
|
||||
|
||||
var _ TypeConverter = &schemeTypeConverter{}
|
||||
|
||||
// NewSchemeTypeConverter creates a TypeConverter that uses the provided scheme to
|
||||
// convert between runtime.Objects and TypedValues.
|
||||
func NewSchemeTypeConverter(scheme *runtime.Scheme, parser *typed.Parser) TypeConverter {
|
||||
return &schemeTypeConverter{scheme: scheme, parser: parser}
|
||||
}
|
||||
|
||||
func (tc schemeTypeConverter) ObjectToTyped(obj runtime.Object, opts ...typed.ValidationOptions) (*typed.TypedValue, error) {
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
name, err := tc.scheme.ToOpenAPIDefinitionName(gvk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := tc.parser.Type(name)
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
return t.FromUnstructured(o.UnstructuredContent(), opts...)
|
||||
default:
|
||||
return t.FromStructured(obj, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
func (tc schemeTypeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, error) {
|
||||
vu := value.AsValue().Unstructured()
|
||||
switch o := vu.(type) {
|
||||
case map[string]interface{}:
|
||||
return &unstructured.Unstructured{Object: o}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("failed to convert value to unstructured for type %T", vu)
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2019 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
type skipNonAppliedManager struct {
|
||||
fieldManager Manager
|
||||
objectCreater runtime.ObjectCreater
|
||||
beforeApplyManagerName string
|
||||
probability float32
|
||||
}
|
||||
|
||||
var _ Manager = &skipNonAppliedManager{}
|
||||
|
||||
// NewSkipNonAppliedManager creates a new wrapped FieldManager that only starts tracking managers after the first apply.
|
||||
func NewSkipNonAppliedManager(fieldManager Manager, objectCreater runtime.ObjectCreater) Manager {
|
||||
return NewProbabilisticSkipNonAppliedManager(fieldManager, objectCreater, 0.0)
|
||||
}
|
||||
|
||||
// NewProbabilisticSkipNonAppliedManager creates a new wrapped FieldManager that starts tracking managers after the first apply,
|
||||
// or starts tracking on create with p probability.
|
||||
func NewProbabilisticSkipNonAppliedManager(fieldManager Manager, objectCreater runtime.ObjectCreater, p float32) Manager {
|
||||
return &skipNonAppliedManager{
|
||||
fieldManager: fieldManager,
|
||||
objectCreater: objectCreater,
|
||||
beforeApplyManagerName: "before-first-apply",
|
||||
probability: p,
|
||||
}
|
||||
}
|
||||
|
||||
// Update implements Manager.
|
||||
func (f *skipNonAppliedManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
|
||||
accessor, err := meta.Accessor(liveObj)
|
||||
if err != nil {
|
||||
return newObj, managed, nil
|
||||
}
|
||||
|
||||
// If managed fields is empty, we need to determine whether to skip tracking managed fields.
|
||||
if len(managed.Fields()) == 0 {
|
||||
// Check if the operation is a create, by checking whether lastObj's UID is empty.
|
||||
// If the operation is create, P(tracking managed fields) = f.probability
|
||||
// If the operation is update, skip tracking managed fields, since we already know managed fields is empty.
|
||||
if len(accessor.GetUID()) == 0 {
|
||||
if f.probability <= rand.Float32() {
|
||||
return newObj, managed, nil
|
||||
}
|
||||
} else {
|
||||
return newObj, managed, nil
|
||||
}
|
||||
}
|
||||
return f.fieldManager.Update(liveObj, newObj, managed, manager)
|
||||
}
|
||||
|
||||
// Apply implements Manager.
|
||||
func (f *skipNonAppliedManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
|
||||
if len(managed.Fields()) == 0 {
|
||||
gvk := appliedObj.GetObjectKind().GroupVersionKind()
|
||||
emptyObj, err := f.objectCreater.New(gvk)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create empty object of type %v: %v", gvk, err)
|
||||
}
|
||||
if unstructured, isUnstructured := emptyObj.(runtime.Unstructured); isUnstructured {
|
||||
unstructured.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
}
|
||||
liveObj, managed, err = f.fieldManager.Update(emptyObj, liveObj, managed, f.beforeApplyManagerName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create manager for existing fields: %v", err)
|
||||
}
|
||||
}
|
||||
return f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Copyright 2019 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
type stripMetaManager struct {
|
||||
fieldManager Manager
|
||||
|
||||
// stripSet is the list of fields that should never be part of a mangedFields.
|
||||
stripSet *fieldpath.Set
|
||||
}
|
||||
|
||||
var _ Manager = &stripMetaManager{}
|
||||
|
||||
// NewStripMetaManager creates a new Manager that strips metadata and typemeta fields from the manager's fieldset.
|
||||
func NewStripMetaManager(fieldManager Manager) Manager {
|
||||
return &stripMetaManager{
|
||||
fieldManager: fieldManager,
|
||||
stripSet: fieldpath.NewSet(
|
||||
fieldpath.MakePathOrDie("apiVersion"),
|
||||
fieldpath.MakePathOrDie("kind"),
|
||||
fieldpath.MakePathOrDie("metadata"),
|
||||
fieldpath.MakePathOrDie("metadata", "name"),
|
||||
fieldpath.MakePathOrDie("metadata", "namespace"),
|
||||
fieldpath.MakePathOrDie("metadata", "creationTimestamp"),
|
||||
fieldpath.MakePathOrDie("metadata", "selfLink"),
|
||||
fieldpath.MakePathOrDie("metadata", "uid"),
|
||||
fieldpath.MakePathOrDie("metadata", "clusterName"),
|
||||
fieldpath.MakePathOrDie("metadata", "generation"),
|
||||
fieldpath.MakePathOrDie("metadata", "managedFields"),
|
||||
fieldpath.MakePathOrDie("metadata", "resourceVersion"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Update implements Manager.
|
||||
func (f *stripMetaManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
|
||||
newObj, managed, err := f.fieldManager.Update(liveObj, newObj, managed, manager)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
f.stripFields(managed.Fields(), manager)
|
||||
return newObj, managed, nil
|
||||
}
|
||||
|
||||
// Apply implements Manager.
|
||||
func (f *stripMetaManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
|
||||
newObj, managed, err := f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
f.stripFields(managed.Fields(), manager)
|
||||
return newObj, managed, nil
|
||||
}
|
||||
|
||||
// stripFields removes a predefined set of paths found in typed from managed
|
||||
func (f *stripMetaManager) stripFields(managed fieldpath.ManagedFields, manager string) {
|
||||
vs, ok := managed[manager]
|
||||
if ok {
|
||||
if vs == nil {
|
||||
panic(fmt.Sprintf("Found unexpected nil manager which should never happen: %s", manager))
|
||||
}
|
||||
newSet := vs.Set().Difference(f.stripSet)
|
||||
if newSet.Empty() {
|
||||
delete(managed, manager)
|
||||
} else {
|
||||
managed[manager] = fieldpath.NewVersionedSet(newSet, vs.APIVersion(), vs.Applied())
|
||||
}
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
Copyright 2019 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/merge"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/typed"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type structuredMergeManager struct {
|
||||
typeConverter TypeConverter
|
||||
objectConverter runtime.ObjectConvertor
|
||||
objectDefaulter runtime.ObjectDefaulter
|
||||
groupVersion schema.GroupVersion
|
||||
hubVersion schema.GroupVersion
|
||||
updater merge.Updater
|
||||
}
|
||||
|
||||
var _ Manager = &structuredMergeManager{}
|
||||
|
||||
// NewStructuredMergeManager creates a new Manager that merges apply requests
|
||||
// and update managed fields for other types of requests.
|
||||
func NewStructuredMergeManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion, resetFields map[fieldpath.APIVersion]fieldpath.Filter) (Manager, error) {
|
||||
if typeConverter == nil {
|
||||
return nil, fmt.Errorf("typeconverter must not be nil")
|
||||
}
|
||||
return &structuredMergeManager{
|
||||
typeConverter: typeConverter,
|
||||
objectConverter: objectConverter,
|
||||
objectDefaulter: objectDefaulter,
|
||||
groupVersion: gv,
|
||||
hubVersion: hub,
|
||||
updater: merge.Updater{
|
||||
Converter: newVersionConverter(typeConverter, objectConverter, hub), // This is the converter provided to SMD from k8s
|
||||
IgnoreFilter: resetFields,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCRDStructuredMergeManager creates a new Manager specifically for
|
||||
// CRDs. This allows for the possibility of fields which are not defined
|
||||
// in models, as well as having no models defined at all.
|
||||
func NewCRDStructuredMergeManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion, resetFields map[fieldpath.APIVersion]fieldpath.Filter) (_ Manager, err error) {
|
||||
return &structuredMergeManager{
|
||||
typeConverter: typeConverter,
|
||||
objectConverter: objectConverter,
|
||||
objectDefaulter: objectDefaulter,
|
||||
groupVersion: gv,
|
||||
hubVersion: hub,
|
||||
updater: merge.Updater{
|
||||
Converter: newCRDVersionConverter(typeConverter, objectConverter, hub),
|
||||
IgnoreFilter: resetFields,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func objectGVKNN(obj runtime.Object) string {
|
||||
name := "<unknown>"
|
||||
namespace := "<unknown>"
|
||||
if accessor, err := meta.Accessor(obj); err == nil {
|
||||
name = accessor.GetName()
|
||||
namespace = accessor.GetNamespace()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v/%v; %v", namespace, name, obj.GetObjectKind().GroupVersionKind())
|
||||
}
|
||||
|
||||
// Update implements Manager.
|
||||
func (f *structuredMergeManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
|
||||
newObjVersioned, err := f.toVersioned(newObj)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to convert new object (%v) to proper version (%v): %v", objectGVKNN(newObj), f.groupVersion, err)
|
||||
}
|
||||
liveObjVersioned, err := f.toVersioned(liveObj)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to convert live object (%v) to proper version: %v", objectGVKNN(liveObj), err)
|
||||
}
|
||||
newObjTyped, err := f.typeConverter.ObjectToTyped(newObjVersioned, typed.AllowDuplicates)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to convert new object (%v) to smd typed: %v", objectGVKNN(newObjVersioned), err)
|
||||
}
|
||||
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned, typed.AllowDuplicates)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to convert live object (%v) to smd typed: %v", objectGVKNN(liveObjVersioned), err)
|
||||
}
|
||||
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
|
||||
|
||||
// TODO(apelisse) use the first return value when unions are implemented
|
||||
_, managedFields, err := f.updater.Update(liveObjTyped, newObjTyped, apiVersion, managed.Fields(), manager)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to update ManagedFields (%v): %v", objectGVKNN(newObjVersioned), err)
|
||||
}
|
||||
managed = NewManaged(managedFields, managed.Times())
|
||||
|
||||
return newObj, managed, nil
|
||||
}
|
||||
|
||||
// Apply implements Manager.
|
||||
func (f *structuredMergeManager) Apply(liveObj, patchObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
|
||||
// Check that the patch object has the same version as the live object
|
||||
if patchVersion := patchObj.GetObjectKind().GroupVersionKind().GroupVersion(); patchVersion != f.groupVersion {
|
||||
return nil, nil,
|
||||
errors.NewBadRequest(
|
||||
fmt.Sprintf("Incorrect version specified in apply patch. "+
|
||||
"Specified patch version: %s, expected: %s",
|
||||
patchVersion, f.groupVersion))
|
||||
}
|
||||
|
||||
patchObjMeta, err := meta.Accessor(patchObj)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("couldn't get accessor: %v", err)
|
||||
}
|
||||
if patchObjMeta.GetManagedFields() != nil {
|
||||
return nil, nil, errors.NewBadRequest("metadata.managedFields must be nil")
|
||||
}
|
||||
|
||||
liveObjVersioned, err := f.toVersioned(liveObj)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to convert live object (%v) to proper version: %v", objectGVKNN(liveObj), err)
|
||||
}
|
||||
|
||||
// Don't allow duplicates in the applied object.
|
||||
patchObjTyped, err := f.typeConverter.ObjectToTyped(patchObj)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create typed patch object (%v): %v", objectGVKNN(patchObj), err)
|
||||
}
|
||||
|
||||
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned, typed.AllowDuplicates)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create typed live object (%v): %v", objectGVKNN(liveObjVersioned), err)
|
||||
}
|
||||
|
||||
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
|
||||
newObjTyped, managedFields, err := f.updater.Apply(liveObjTyped, patchObjTyped, apiVersion, managed.Fields(), manager, force)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
managed = NewManaged(managedFields, managed.Times())
|
||||
|
||||
if newObjTyped == nil {
|
||||
return nil, managed, nil
|
||||
}
|
||||
|
||||
newObj, err := f.typeConverter.TypedToObject(newObjTyped)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to convert new typed object (%v) to object: %v", objectGVKNN(patchObj), err)
|
||||
}
|
||||
|
||||
newObjVersioned, err := f.toVersioned(newObj)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to convert new object (%v) to proper version: %v", objectGVKNN(patchObj), err)
|
||||
}
|
||||
f.objectDefaulter.Default(newObjVersioned)
|
||||
|
||||
newObjUnversioned, err := f.toUnversioned(newObjVersioned)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to convert to unversioned (%v): %v", objectGVKNN(patchObj), err)
|
||||
}
|
||||
return newObjUnversioned, managed, nil
|
||||
}
|
||||
|
||||
func (f *structuredMergeManager) toVersioned(obj runtime.Object) (runtime.Object, error) {
|
||||
return f.objectConverter.ConvertToVersion(obj, f.groupVersion)
|
||||
}
|
||||
|
||||
func (f *structuredMergeManager) toUnversioned(obj runtime.Object) (runtime.Object, error) {
|
||||
return f.objectConverter.ConvertToVersion(obj, f.hubVersion)
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
Copyright 2022 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kube-openapi/pkg/schemaconv"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
smdschema "sigs.k8s.io/structured-merge-diff/v6/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/typed"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/value"
|
||||
)
|
||||
|
||||
// TypeConverter allows you to convert from runtime.Object to
|
||||
// typed.TypedValue and the other way around.
|
||||
type TypeConverter interface {
|
||||
ObjectToTyped(runtime.Object, ...typed.ValidationOptions) (*typed.TypedValue, error)
|
||||
TypedToObject(*typed.TypedValue) (runtime.Object, error)
|
||||
}
|
||||
|
||||
type typeConverter struct {
|
||||
parser map[schema.GroupVersionKind]*typed.ParseableType
|
||||
}
|
||||
|
||||
var _ TypeConverter = &typeConverter{}
|
||||
|
||||
func NewTypeConverter(openapiSpec map[string]*spec.Schema, preserveUnknownFields bool) (TypeConverter, error) {
|
||||
typeSchema, err := schemaconv.ToSchemaFromOpenAPI(openapiSpec, preserveUnknownFields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert models to schema: %v", err)
|
||||
}
|
||||
|
||||
typeParser := typed.Parser{Schema: smdschema.Schema{Types: typeSchema.Types}}
|
||||
tr := indexModels(&typeParser, openapiSpec)
|
||||
|
||||
return &typeConverter{parser: tr}, nil
|
||||
}
|
||||
|
||||
func (c *typeConverter) ObjectToTyped(obj runtime.Object, opts ...typed.ValidationOptions) (*typed.TypedValue, error) {
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
t := c.parser[gvk]
|
||||
if t == nil {
|
||||
return nil, NewNoCorrespondingTypeError(gvk)
|
||||
}
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
return t.FromUnstructured(o.UnstructuredContent(), opts...)
|
||||
default:
|
||||
return t.FromStructured(obj, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *typeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, error) {
|
||||
return valueToObject(value.AsValue())
|
||||
}
|
||||
|
||||
type deducedTypeConverter struct{}
|
||||
|
||||
// DeducedTypeConverter is a TypeConverter for CRDs that don't have a
|
||||
// schema. It does implement the same interface though (and create the
|
||||
// same types of objects), so that everything can still work the same.
|
||||
// CRDs are merged with all their fields being "atomic" (lists
|
||||
// included).
|
||||
func NewDeducedTypeConverter() TypeConverter {
|
||||
return deducedTypeConverter{}
|
||||
}
|
||||
|
||||
// ObjectToTyped converts an object into a TypedValue with a "deduced type".
|
||||
func (deducedTypeConverter) ObjectToTyped(obj runtime.Object, opts ...typed.ValidationOptions) (*typed.TypedValue, error) {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
return typed.DeducedParseableType.FromUnstructured(o.UnstructuredContent(), opts...)
|
||||
default:
|
||||
return typed.DeducedParseableType.FromStructured(obj, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// TypedToObject transforms the typed value into a runtime.Object. That
|
||||
// is not specific to deduced type.
|
||||
func (deducedTypeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, error) {
|
||||
return valueToObject(value.AsValue())
|
||||
}
|
||||
|
||||
func valueToObject(val value.Value) (runtime.Object, error) {
|
||||
vu := val.Unstructured()
|
||||
switch o := vu.(type) {
|
||||
case map[string]interface{}:
|
||||
return &unstructured.Unstructured{Object: o}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("failed to convert value to unstructured for type %T", vu)
|
||||
}
|
||||
}
|
||||
|
||||
func indexModels(
|
||||
typeParser *typed.Parser,
|
||||
openAPISchemas map[string]*spec.Schema,
|
||||
) map[schema.GroupVersionKind]*typed.ParseableType {
|
||||
tr := map[schema.GroupVersionKind]*typed.ParseableType{}
|
||||
for modelName, model := range openAPISchemas {
|
||||
gvkList := parseGroupVersionKind(model.Extensions)
|
||||
if len(gvkList) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
parsedType := typeParser.Type(modelName)
|
||||
for _, gvk := range gvkList {
|
||||
if len(gvk.Kind) > 0 {
|
||||
tr[schema.GroupVersionKind(gvk)] = &parsedType
|
||||
}
|
||||
}
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
// Get and parse GroupVersionKind from the extension. Returns empty if it doesn't have one.
|
||||
func parseGroupVersionKind(extensions map[string]interface{}) []schema.GroupVersionKind {
|
||||
gvkListResult := []schema.GroupVersionKind{}
|
||||
|
||||
// Get the extensions
|
||||
gvkExtension, ok := extensions["x-kubernetes-group-version-kind"]
|
||||
if !ok {
|
||||
return []schema.GroupVersionKind{}
|
||||
}
|
||||
|
||||
// gvk extension must be a list of at least 1 element.
|
||||
gvkList, ok := gvkExtension.([]interface{})
|
||||
if !ok {
|
||||
return []schema.GroupVersionKind{}
|
||||
}
|
||||
|
||||
for _, gvk := range gvkList {
|
||||
var group, version, kind string
|
||||
|
||||
// gvk extension list must be a map with group, version, and
|
||||
// kind fields
|
||||
if gvkMap, ok := gvk.(map[interface{}]interface{}); ok {
|
||||
group, ok = gvkMap["group"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
version, ok = gvkMap["version"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
kind, ok = gvkMap["kind"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
} else if gvkMap, ok := gvk.(map[string]interface{}); ok {
|
||||
group, ok = gvkMap["group"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
version, ok = gvkMap["version"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
kind, ok = gvkMap["kind"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
gvkListResult = append(gvkListResult, schema.GroupVersionKind{
|
||||
Group: group,
|
||||
Version: version,
|
||||
Kind: kind,
|
||||
})
|
||||
}
|
||||
|
||||
return gvkListResult
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type versionCheckManager struct {
|
||||
fieldManager Manager
|
||||
gvk schema.GroupVersionKind
|
||||
}
|
||||
|
||||
var _ Manager = &versionCheckManager{}
|
||||
|
||||
// NewVersionCheckManager creates a manager that makes sure that the
|
||||
// applied object is in the proper version.
|
||||
func NewVersionCheckManager(fieldManager Manager, gvk schema.GroupVersionKind) Manager {
|
||||
return &versionCheckManager{fieldManager: fieldManager, gvk: gvk}
|
||||
}
|
||||
|
||||
// Update implements Manager.
|
||||
func (f *versionCheckManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
|
||||
// Nothing to do for updates, this is checked in many other places.
|
||||
return f.fieldManager.Update(liveObj, newObj, managed, manager)
|
||||
}
|
||||
|
||||
// Apply implements Manager.
|
||||
func (f *versionCheckManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
|
||||
if gvk := appliedObj.GetObjectKind().GroupVersionKind(); gvk != f.gvk {
|
||||
return nil, nil, errors.NewBadRequest(fmt.Sprintf("invalid object type: %v", gvk))
|
||||
}
|
||||
return f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
Copyright 2018 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/merge"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/typed"
|
||||
)
|
||||
|
||||
// versionConverter is an implementation of
|
||||
// sigs.k8s.io/structured-merge-diff/merge.Converter
|
||||
type versionConverter struct {
|
||||
typeConverter TypeConverter
|
||||
objectConvertor runtime.ObjectConvertor
|
||||
hubGetter func(from schema.GroupVersion) schema.GroupVersion
|
||||
}
|
||||
|
||||
var _ merge.Converter = &versionConverter{}
|
||||
|
||||
// NewVersionConverter builds a VersionConverter from a TypeConverter and an ObjectConvertor.
|
||||
func newVersionConverter(t TypeConverter, o runtime.ObjectConvertor, h schema.GroupVersion) merge.Converter {
|
||||
return &versionConverter{
|
||||
typeConverter: t,
|
||||
objectConvertor: o,
|
||||
hubGetter: func(from schema.GroupVersion) schema.GroupVersion {
|
||||
return schema.GroupVersion{
|
||||
Group: from.Group,
|
||||
Version: h.Version,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewCRDVersionConverter builds a VersionConverter for CRDs from a TypeConverter and an ObjectConvertor.
|
||||
func newCRDVersionConverter(t TypeConverter, o runtime.ObjectConvertor, h schema.GroupVersion) merge.Converter {
|
||||
return &versionConverter{
|
||||
typeConverter: t,
|
||||
objectConvertor: o,
|
||||
hubGetter: func(from schema.GroupVersion) schema.GroupVersion {
|
||||
return h
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Convert implements sigs.k8s.io/structured-merge-diff/merge.Converter
|
||||
func (v *versionConverter) Convert(object *typed.TypedValue, version fieldpath.APIVersion) (*typed.TypedValue, error) {
|
||||
// Convert the smd typed value to a kubernetes object.
|
||||
objectToConvert, err := v.typeConverter.TypedToObject(object)
|
||||
if err != nil {
|
||||
return object, err
|
||||
}
|
||||
|
||||
// Parse the target groupVersion.
|
||||
groupVersion, err := schema.ParseGroupVersion(string(version))
|
||||
if err != nil {
|
||||
return object, err
|
||||
}
|
||||
|
||||
// If attempting to convert to the same version as we already have, just return it.
|
||||
fromVersion := objectToConvert.GetObjectKind().GroupVersionKind().GroupVersion()
|
||||
if fromVersion == groupVersion {
|
||||
return object, nil
|
||||
}
|
||||
|
||||
// Convert to internal
|
||||
internalObject, err := v.objectConvertor.ConvertToVersion(objectToConvert, v.hubGetter(fromVersion))
|
||||
if err != nil {
|
||||
return object, err
|
||||
}
|
||||
|
||||
// Convert the object into the target version
|
||||
convertedObject, err := v.objectConvertor.ConvertToVersion(internalObject, groupVersion)
|
||||
if err != nil {
|
||||
return object, err
|
||||
}
|
||||
|
||||
// Convert the object back to a smd typed value and return it.
|
||||
return v.typeConverter.ObjectToTyped(convertedObject)
|
||||
}
|
||||
|
||||
// IsMissingVersionError
|
||||
func (v *versionConverter) IsMissingVersionError(err error) bool {
|
||||
return runtime.IsNotRegisteredError(err) || isNoCorrespondingTypeError(err)
|
||||
}
|
||||
|
||||
type noCorrespondingTypeErr struct {
|
||||
gvk schema.GroupVersionKind
|
||||
}
|
||||
|
||||
func NewNoCorrespondingTypeError(gvk schema.GroupVersionKind) error {
|
||||
return &noCorrespondingTypeErr{gvk: gvk}
|
||||
}
|
||||
|
||||
func (k *noCorrespondingTypeErr) Error() string {
|
||||
return fmt.Sprintf("no corresponding type for %v", k.gvk)
|
||||
}
|
||||
|
||||
func isNoCorrespondingTypeError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := err.(*noCorrespondingTypeErr)
|
||||
return ok
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
apiVersion: v1
|
||||
kind: Node
|
||||
metadata:
|
||||
annotations:
|
||||
container.googleapis.com/instance_id: "123456789321654789"
|
||||
node.alpha.kubernetes.io/ttl: "0"
|
||||
volumes.kubernetes.io/controller-managed-attach-detach: "true"
|
||||
creationTimestamp: "2019-07-09T16:17:29Z"
|
||||
labels:
|
||||
kubernetes.io/arch: amd64
|
||||
beta.kubernetes.io/fluentd-ds-ready: "true"
|
||||
beta.kubernetes.io/instance-type: n1-standard-4
|
||||
kubernetes.io/os: linux
|
||||
cloud.google.com/gke-nodepool: default-pool
|
||||
cloud.google.com/gke-os-distribution: cos
|
||||
failure-domain.beta.kubernetes.io/region: us-central1
|
||||
failure-domain.beta.kubernetes.io/zone: us-central1-b
|
||||
topology.kubernetes.io/region: us-central1
|
||||
topology.kubernetes.io/zone: us-central1-b
|
||||
kubernetes.io/hostname: node-default-pool-something
|
||||
name: node-default-pool-something
|
||||
resourceVersion: "211582541"
|
||||
selfLink: /api/v1/nodes/node-default-pool-something
|
||||
uid: 0c24d0e1-a265-11e9-abe4-42010a80026b
|
||||
spec:
|
||||
podCIDR: 10.0.0.1/24
|
||||
providerID: some-provider-id-of-some-sort
|
||||
status:
|
||||
addresses:
|
||||
- address: 10.0.0.1
|
||||
type: InternalIP
|
||||
- address: 192.168.0.1
|
||||
type: ExternalIP
|
||||
- address: node-default-pool-something
|
||||
type: Hostname
|
||||
allocatable:
|
||||
cpu: 3920m
|
||||
ephemeral-storage: "104638878617"
|
||||
hugepages-2Mi: "0"
|
||||
memory: 12700100Ki
|
||||
pods: "110"
|
||||
capacity:
|
||||
cpu: "4"
|
||||
ephemeral-storage: 202086868Ki
|
||||
hugepages-2Mi: "0"
|
||||
memory: 15399364Ki
|
||||
pods: "110"
|
||||
conditions:
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:08Z"
|
||||
lastTransitionTime: "2019-07-09T16:22:08Z"
|
||||
message: containerd is functioning properly
|
||||
reason: FrequentContainerdRestart
|
||||
status: "False"
|
||||
type: FrequentContainerdRestart
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:08Z"
|
||||
lastTransitionTime: "2019-07-09T16:22:06Z"
|
||||
message: docker overlay2 is functioning properly
|
||||
reason: CorruptDockerOverlay2
|
||||
status: "False"
|
||||
type: CorruptDockerOverlay2
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:08Z"
|
||||
lastTransitionTime: "2019-07-09T16:22:06Z"
|
||||
message: node is functioning properly
|
||||
reason: UnregisterNetDevice
|
||||
status: "False"
|
||||
type: FrequentUnregisterNetDevice
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:08Z"
|
||||
lastTransitionTime: "2019-07-09T16:17:04Z"
|
||||
message: kernel has no deadlock
|
||||
reason: KernelHasNoDeadlock
|
||||
status: "False"
|
||||
type: KernelDeadlock
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:08Z"
|
||||
lastTransitionTime: "2019-07-09T16:17:04Z"
|
||||
message: Filesystem is not read-only
|
||||
reason: FilesystemIsNotReadOnly
|
||||
status: "False"
|
||||
type: ReadonlyFilesystem
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:08Z"
|
||||
lastTransitionTime: "2019-07-09T16:22:05Z"
|
||||
message: kubelet is functioning properly
|
||||
reason: FrequentKubeletRestart
|
||||
status: "False"
|
||||
type: FrequentKubeletRestart
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:08Z"
|
||||
lastTransitionTime: "2019-07-09T16:22:06Z"
|
||||
message: docker is functioning properly
|
||||
reason: FrequentDockerRestart
|
||||
status: "False"
|
||||
type: FrequentDockerRestart
|
||||
- lastHeartbeatTime: "2019-07-09T16:17:47Z"
|
||||
lastTransitionTime: "2019-07-09T16:17:47Z"
|
||||
message: RouteController created a route
|
||||
reason: RouteCreated
|
||||
status: "False"
|
||||
type: NetworkUnavailable
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:50Z"
|
||||
lastTransitionTime: "2019-07-09T16:17:29Z"
|
||||
message: kubelet has sufficient disk space available
|
||||
reason: KubeletHasSufficientDisk
|
||||
status: "False"
|
||||
type: OutOfDisk
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:50Z"
|
||||
lastTransitionTime: "2019-07-09T16:17:29Z"
|
||||
message: kubelet has sufficient memory available
|
||||
reason: KubeletHasSufficientMemory
|
||||
status: "False"
|
||||
type: MemoryPressure
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:50Z"
|
||||
lastTransitionTime: "2019-07-09T16:17:29Z"
|
||||
message: kubelet has no disk pressure
|
||||
reason: KubeletHasNoDiskPressure
|
||||
status: "False"
|
||||
type: DiskPressure
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:50Z"
|
||||
lastTransitionTime: "2019-07-09T16:17:29Z"
|
||||
message: kubelet has sufficient PID available
|
||||
reason: KubeletHasSufficientPID
|
||||
status: "False"
|
||||
type: PIDPressure
|
||||
- lastHeartbeatTime: "2019-09-20T19:32:50Z"
|
||||
lastTransitionTime: "2019-07-09T16:17:49Z"
|
||||
message: kubelet is posting ready status
|
||||
reason: KubeletReady
|
||||
status: "True"
|
||||
type: Ready
|
||||
daemonEndpoints:
|
||||
kubeletEndpoint:
|
||||
Port: 10250
|
||||
images:
|
||||
- names:
|
||||
- grafana/grafana@sha256:80e5e113a984d74836aa16f5b4524012099436b1a50df293f00ac6377fb512c8
|
||||
- grafana/grafana:4.4.2
|
||||
sizeBytes: 287008013
|
||||
- names:
|
||||
- registry.k8s.io/node-problem-detector@sha256:f95cab985c26b2f46e9bd43283e0bfa88860c14e0fb0649266babe8b65e9eb2b
|
||||
- registry.k8s.io/node-problem-detector:v0.4.1
|
||||
sizeBytes: 286572743
|
||||
- names:
|
||||
- grafana/grafana@sha256:7ff7f9b2501a5d55b55ce3f58d21771b1c5af1f2a4ab7dbf11bef7142aae7033
|
||||
- grafana/grafana:4.2.0
|
||||
sizeBytes: 277940263
|
||||
- names:
|
||||
- influxdb@sha256:7dddf03376348876ed4bdf33d6dfa3326f45a2bae0930dbd80781a374eb519bc
|
||||
- influxdb:1.2.2
|
||||
sizeBytes: 223948571
|
||||
- names:
|
||||
- gcr.io/stackdriver-agents/stackdriver-logging-agent@sha256:f8d5231b67b9c53f60068b535a11811d29d1b3efd53d2b79f2a2591ea338e4f2
|
||||
- gcr.io/stackdriver-agents/stackdriver-logging-agent:0.6-1.6.0-1
|
||||
sizeBytes: 223242132
|
||||
- names:
|
||||
- nginx@sha256:35779791c05d119df4fe476db8f47c0bee5943c83eba5656a15fc046db48178b
|
||||
- nginx:1.10.1
|
||||
sizeBytes: 180708613
|
||||
- names:
|
||||
- registry.k8s.io/fluentd-elasticsearch@sha256:b8c94527b489fb61d3d81ce5ad7f3ddbb7be71e9620a3a36e2bede2f2e487d73
|
||||
- registry.k8s.io/fluentd-elasticsearch:v2.0.4
|
||||
sizeBytes: 135716379
|
||||
- names:
|
||||
- nginx@sha256:00be67d6ba53d5318cd91c57771530f5251cfbe028b7be2c4b70526f988cfc9f
|
||||
- nginx:latest
|
||||
sizeBytes: 109357355
|
||||
- names:
|
||||
- registry.k8s.io/kubernetes-dashboard-amd64@sha256:dc4026c1b595435ef5527ca598e1e9c4343076926d7d62b365c44831395adbd0
|
||||
- registry.k8s.io/kubernetes-dashboard-amd64:v1.8.3
|
||||
sizeBytes: 102319441
|
||||
- names:
|
||||
- gcr.io/google_containers/kube-proxy:v1.11.10-gke.5
|
||||
- registry.k8s.io/kube-proxy:v1.11.10-gke.5
|
||||
sizeBytes: 102279340
|
||||
- names:
|
||||
- registry.k8s.io/event-exporter@sha256:7f9cd7cb04d6959b0aa960727d04fa86759008048c785397b7b0d9dff0007516
|
||||
- registry.k8s.io/event-exporter:v0.2.3
|
||||
sizeBytes: 94171943
|
||||
- names:
|
||||
- registry.k8s.io/prometheus-to-sd@sha256:6c0c742475363d537ff059136e5d5e4ab1f512ee0fd9b7ca42ea48bc309d1662
|
||||
- registry.k8s.io/prometheus-to-sd:v0.3.1
|
||||
sizeBytes: 88077694
|
||||
- names:
|
||||
- registry.k8s.io/fluentd-gcp-scaler@sha256:a5ace7506d393c4ed65eb2cbb6312c64ab357fcea16dff76b9055bc6e498e5ff
|
||||
- registry.k8s.io/fluentd-gcp-scaler:0.5.1
|
||||
sizeBytes: 86637208
|
||||
- names:
|
||||
- registry.k8s.io/heapster-amd64@sha256:9fae0af136ce0cf4f88393b3670f7139ffc464692060c374d2ae748e13144521
|
||||
- registry.k8s.io/heapster-amd64:v1.6.0-beta.1
|
||||
sizeBytes: 76016169
|
||||
- names:
|
||||
- registry.k8s.io/ingress-glbc-amd64@sha256:31d36bbd9c44caffa135fc78cf0737266fcf25e3cf0cd1c2fcbfbc4f7309cc52
|
||||
- registry.k8s.io/ingress-glbc-amd64:v1.1.1
|
||||
sizeBytes: 67801919
|
||||
- names:
|
||||
- registry.k8s.io/kube-addon-manager@sha256:d53486c3a0b49ebee019932878dc44232735d5622a51dbbdcec7124199020d09
|
||||
- registry.k8s.io/kube-addon-manager:v8.7
|
||||
sizeBytes: 63322109
|
||||
- names:
|
||||
- nginx@sha256:4aacdcf186934dcb02f642579314075910f1855590fd3039d8fa4c9f96e48315
|
||||
- nginx:1.10-alpine
|
||||
sizeBytes: 54042627
|
||||
- names:
|
||||
- registry.k8s.io/cpvpa-amd64@sha256:cfe7b0a11c9c8e18c87b1eb34fef9a7cbb8480a8da11fc2657f78dbf4739f869
|
||||
- registry.k8s.io/cpvpa-amd64:v0.6.0
|
||||
sizeBytes: 51785854
|
||||
- names:
|
||||
- registry.k8s.io/cluster-proportional-autoscaler-amd64@sha256:003f98d9f411ddfa6ff6d539196355e03ddd69fa4ed38c7ffb8fec6f729afe2d
|
||||
- registry.k8s.io/cluster-proportional-autoscaler-amd64:1.1.2-r2
|
||||
sizeBytes: 49648481
|
||||
- names:
|
||||
- registry.k8s.io/ip-masq-agent-amd64@sha256:1ffda57d87901bc01324c82ceb2145fe6a0448d3f0dd9cb65aa76a867cd62103
|
||||
- registry.k8s.io/ip-masq-agent-amd64:v2.1.1
|
||||
sizeBytes: 49612505
|
||||
- names:
|
||||
- registry.k8s.io/k8s-dns-kube-dns-amd64@sha256:b99fc3eee2a9f052f7eb4cc00f15eb12fc405fa41019baa2d6b79847ae7284a8
|
||||
- registry.k8s.io/k8s-dns-kube-dns-amd64:1.14.10
|
||||
sizeBytes: 49549457
|
||||
- names:
|
||||
- registry.k8s.io/rescheduler@sha256:156cfbfd05a5a815206fd2eeb6cbdaf1596d71ea4b415d3a6c43071dd7b99450
|
||||
- registry.k8s.io/rescheduler:v0.4.0
|
||||
sizeBytes: 48973149
|
||||
- names:
|
||||
- registry.k8s.io/event-exporter@sha256:16ca66e2b5dc7a1ce6a5aafcb21d0885828b75cdfc08135430480f7ad2364adc
|
||||
- registry.k8s.io/event-exporter:v0.2.4
|
||||
sizeBytes: 47261019
|
||||
- names:
|
||||
- registry.k8s.io/coredns@sha256:db2bf53126ed1c761d5a41f24a1b82a461c85f736ff6e90542e9522be4757848
|
||||
- registry.k8s.io/coredns:1.1.3
|
||||
sizeBytes: 45587362
|
||||
- names:
|
||||
- prom/prometheus@sha256:483f4c9d7733699ba79facca9f8bcce1cef1af43dfc3e7c5a1882aa85f53cb74
|
||||
- prom/prometheus:v1.1.3
|
||||
sizeBytes: 45493941
|
||||
nodeInfo:
|
||||
architecture: amd64
|
||||
bootID: a32eca78-4ad4-4b76-9252-f143d6c2ae61
|
||||
containerRuntimeVersion: docker://17.3.2
|
||||
kernelVersion: 4.14.127+
|
||||
kubeProxyVersion: v1.11.10-gke.5
|
||||
kubeletVersion: v1.11.10-gke.5
|
||||
machineID: 1739555e5b231057f0f9a0b5fa29511b
|
||||
operatingSystem: linux
|
||||
osImage: Container-Optimized OS from Google
|
||||
systemUUID: 1739555E-5B23-1057-F0F9-A0B5FA29511B
|
||||
volumesAttached:
|
||||
- devicePath: /dev/disk/by-id/b9772-pvc-c787c67d-14d7-11e7-9baf-42010a800049
|
||||
name: kubernetes.io/pd/some-random-clusterb9772-pvc-c787c67d-14d7-11e7-9baf-42010a800049
|
||||
- devicePath: /dev/disk/by-id/b9772-pvc-8895a852-fd42-11e6-94d4-42010a800049
|
||||
name: kubernetes.io/pd/some-random-clusterb9772-pvc-8895a852-fd42-11e6-94d4-42010a800049
|
||||
- devicePath: /dev/disk/by-id/some-random-clusterb9772-pvc-72e1c7f1-fd41-11e6-94d4-42010a800049
|
||||
name: kubernetes.io/pd/some-random-clusterb9772-pvc-72e1c7f1-fd41-11e6-94d4-42010a800049
|
||||
- devicePath: /dev/disk/by-id/some-random-clusterb9772-pvc-c2435a06-14d7-11e7-9baf-42010a800049
|
||||
name: kubernetes.io/pd/some-random-clusterb9772-pvc-c2435a06-14d7-11e7-9baf-42010a800049
|
||||
- devicePath: /dev/disk/by-id/some-random-clusterb9772-pvc-8bf50554-fd42-11e6-94d4-42010a800049
|
||||
name: kubernetes.io/pd/some-random-clusterb9772-pvc-8bf50554-fd42-11e6-94d4-42010a800049
|
||||
- devicePath: /dev/disk/by-id/some-random-clusterb9772-pvc-8fb5e386-4641-11e7-a490-42010a800283
|
||||
name: kubernetes.io/pd/some-random-clusterb9772-pvc-8fb5e386-4641-11e7-a490-42010a800283
|
||||
volumesInUse:
|
||||
- kubernetes.io/pd/some-random-clusterb9772-pvc-72e1c7f1-fd41-11e6-94d4-42010a800049
|
||||
- kubernetes.io/pd/some-random-clusterb9772-pvc-8895a852-fd42-11e6-94d4-42010a800049
|
||||
- kubernetes.io/pd/some-random-clusterb9772-pvc-8bf50554-fd42-11e6-94d4-42010a800049
|
||||
- kubernetes.io/pd/some-random-clusterb9772-pvc-8fb5e386-4641-11e7-a490-42010a800283
|
||||
- kubernetes.io/pd/some-random-clusterb9772-pvc-c2435a06-14d7-11e7-9baf-42010a800049
|
||||
- kubernetes.io/pd/some-random-clusterb9772-pvc-c787c67d-14d7-11e7-9baf-42010a800049
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
labels:
|
||||
app: some-app
|
||||
plugin1: some-value
|
||||
plugin2: some-value
|
||||
plugin3: some-value
|
||||
plugin4: some-value
|
||||
name: some-name
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: apps/v1
|
||||
blockOwnerDeletion: true
|
||||
controller: true
|
||||
kind: ReplicaSet
|
||||
name: some-name
|
||||
uid: 0a9d2b9e-779e-11e7-b422-42010a8001be
|
||||
spec:
|
||||
containers:
|
||||
- args:
|
||||
- one
|
||||
- two
|
||||
- three
|
||||
- four
|
||||
- five
|
||||
- six
|
||||
- seven
|
||||
- eight
|
||||
- nine
|
||||
env:
|
||||
- name: VAR_3
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: some-other-key
|
||||
name: some-oher-name
|
||||
- name: VAR_2
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: other-key
|
||||
name: other-name
|
||||
- name: VAR_1
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: some-key
|
||||
name: some-name
|
||||
image: some-image-name
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: some-name
|
||||
resources:
|
||||
requests:
|
||||
cpu: '0'
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
|
||||
name: default-token-hu5jz
|
||||
readOnly: true
|
||||
dnsPolicy: ClusterFirst
|
||||
nodeName: node-name
|
||||
priority: 0
|
||||
restartPolicy: Always
|
||||
schedulerName: default-scheduler
|
||||
securityContext: {}
|
||||
serviceAccount: default
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 30
|
||||
tolerations:
|
||||
- effect: NoExecute
|
||||
key: node.kubernetes.io/not-ready
|
||||
operator: Exists
|
||||
tolerationSeconds: 300
|
||||
- effect: NoExecute
|
||||
key: node.kubernetes.io/unreachable
|
||||
operator: Exists
|
||||
tolerationSeconds: 300
|
||||
volumes:
|
||||
- name: default-token-hu5jz
|
||||
secret:
|
||||
defaultMode: 420
|
||||
secretName: default-token-hu5jz
|
||||
status:
|
||||
conditions:
|
||||
- lastProbeTime: null
|
||||
lastTransitionTime: '2019-07-08T09:31:18Z'
|
||||
status: 'True'
|
||||
type: Initialized
|
||||
- lastProbeTime: null
|
||||
lastTransitionTime: '2019-07-08T09:41:59Z'
|
||||
status: 'True'
|
||||
type: Ready
|
||||
- lastProbeTime: null
|
||||
lastTransitionTime: null
|
||||
status: 'True'
|
||||
type: ContainersReady
|
||||
- lastProbeTime: null
|
||||
lastTransitionTime: '2019-07-08T09:31:18Z'
|
||||
status: 'True'
|
||||
type: PodScheduled
|
||||
containerStatuses:
|
||||
- containerID: docker://885e82a1ed0b7356541bb410a0126921ac42439607c09875cd8097dd5d7b5376
|
||||
image: some-image-name
|
||||
imageID: docker-pullable://some-image-id
|
||||
lastState:
|
||||
terminated:
|
||||
containerID: docker://d57290f9e00fad626b20d2dd87a3cf69bbc22edae07985374f86a8b2b4e39565
|
||||
exitCode: 255
|
||||
finishedAt: '2019-07-08T09:39:09Z'
|
||||
reason: Error
|
||||
startedAt: '2019-07-08T09:38:54Z'
|
||||
name: name
|
||||
ready: true
|
||||
restartCount: 6
|
||||
state:
|
||||
running:
|
||||
startedAt: '2019-07-08T09:41:59Z'
|
||||
hostIP: 10.0.0.1
|
||||
phase: Running
|
||||
podIP: 10.0.0.1
|
||||
qosClass: BestEffort
|
||||
startTime: '2019-07-08T09:31:18Z'
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright 2021 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 managedfields
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields/internal"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
var (
|
||||
scaleGroupVersion = schema.GroupVersion{Group: "autoscaling", Version: "v1"}
|
||||
replicasPathInScale = fieldpath.MakePathOrDie("spec", "replicas")
|
||||
)
|
||||
|
||||
// ResourcePathMappings maps a group/version to its replicas path. The
|
||||
// assumption is that all the paths correspond to leaf fields.
|
||||
type ResourcePathMappings map[string]fieldpath.Path
|
||||
|
||||
// ScaleHandler manages the conversion of managed fields between a main
|
||||
// resource and the scale subresource
|
||||
type ScaleHandler struct {
|
||||
parentEntries []metav1.ManagedFieldsEntry
|
||||
groupVersion schema.GroupVersion
|
||||
mappings ResourcePathMappings
|
||||
}
|
||||
|
||||
// NewScaleHandler creates a new ScaleHandler
|
||||
func NewScaleHandler(parentEntries []metav1.ManagedFieldsEntry, groupVersion schema.GroupVersion, mappings ResourcePathMappings) *ScaleHandler {
|
||||
return &ScaleHandler{
|
||||
parentEntries: parentEntries,
|
||||
groupVersion: groupVersion,
|
||||
mappings: mappings,
|
||||
}
|
||||
}
|
||||
|
||||
// ToSubresource filter the managed fields of the main resource and convert
|
||||
// them so that they can be handled by scale.
|
||||
// For the managed fields that have a replicas path it performs two changes:
|
||||
// 1. APIVersion is changed to the APIVersion of the scale subresource
|
||||
// 2. Replicas path of the main resource is transformed to the replicas path of
|
||||
// the scale subresource
|
||||
func (h *ScaleHandler) ToSubresource() ([]metav1.ManagedFieldsEntry, error) {
|
||||
managed, err := internal.DecodeManagedFields(h.parentEntries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := fieldpath.ManagedFields{}
|
||||
t := map[string]*metav1.Time{}
|
||||
for manager, versionedSet := range managed.Fields() {
|
||||
path, ok := h.mappings[string(versionedSet.APIVersion())]
|
||||
// Skip the entry if the APIVersion is unknown
|
||||
if !ok || path == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if versionedSet.Set().Has(path) {
|
||||
newVersionedSet := fieldpath.NewVersionedSet(
|
||||
fieldpath.NewSet(replicasPathInScale),
|
||||
fieldpath.APIVersion(scaleGroupVersion.String()),
|
||||
versionedSet.Applied(),
|
||||
)
|
||||
|
||||
f[manager] = newVersionedSet
|
||||
t[manager] = managed.Times()[manager]
|
||||
}
|
||||
}
|
||||
|
||||
return managedFieldsEntries(internal.NewManaged(f, t))
|
||||
}
|
||||
|
||||
// ToParent merges `scaleEntries` with the entries of the main resource and
|
||||
// transforms them accordingly
|
||||
func (h *ScaleHandler) ToParent(scaleEntries []metav1.ManagedFieldsEntry) ([]metav1.ManagedFieldsEntry, error) {
|
||||
decodedParentEntries, err := internal.DecodeManagedFields(h.parentEntries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentFields := decodedParentEntries.Fields()
|
||||
|
||||
decodedScaleEntries, err := internal.DecodeManagedFields(scaleEntries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scaleFields := decodedScaleEntries.Fields()
|
||||
|
||||
f := fieldpath.ManagedFields{}
|
||||
t := map[string]*metav1.Time{}
|
||||
|
||||
for manager, versionedSet := range parentFields {
|
||||
// Get the main resource "replicas" path
|
||||
path, ok := h.mappings[string(versionedSet.APIVersion())]
|
||||
// Drop the entry if the APIVersion is unknown.
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the parent entry does not have the replicas path or it is nil, just
|
||||
// keep it as it is. The path is nil for Custom Resources without scale
|
||||
// subresource.
|
||||
if path == nil || !versionedSet.Set().Has(path) {
|
||||
f[manager] = versionedSet
|
||||
t[manager] = decodedParentEntries.Times()[manager]
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := scaleFields[manager]; !ok {
|
||||
// "Steal" the replicas path from the main resource entry
|
||||
newSet := versionedSet.Set().Difference(fieldpath.NewSet(path))
|
||||
|
||||
if !newSet.Empty() {
|
||||
newVersionedSet := fieldpath.NewVersionedSet(
|
||||
newSet,
|
||||
versionedSet.APIVersion(),
|
||||
versionedSet.Applied(),
|
||||
)
|
||||
f[manager] = newVersionedSet
|
||||
t[manager] = decodedParentEntries.Times()[manager]
|
||||
}
|
||||
} else {
|
||||
// Field wasn't stolen, let's keep the entry as it is.
|
||||
f[manager] = versionedSet
|
||||
t[manager] = decodedParentEntries.Times()[manager]
|
||||
delete(scaleFields, manager)
|
||||
}
|
||||
}
|
||||
|
||||
for manager, versionedSet := range scaleFields {
|
||||
if !versionedSet.Set().Has(replicasPathInScale) {
|
||||
continue
|
||||
}
|
||||
newVersionedSet := fieldpath.NewVersionedSet(
|
||||
fieldpath.NewSet(h.mappings[h.groupVersion.String()]),
|
||||
fieldpath.APIVersion(h.groupVersion.String()),
|
||||
versionedSet.Applied(),
|
||||
)
|
||||
f[manager] = newVersionedSet
|
||||
t[manager] = decodedParentEntries.Times()[manager]
|
||||
}
|
||||
|
||||
return managedFieldsEntries(internal.NewManaged(f, t))
|
||||
}
|
||||
|
||||
func managedFieldsEntries(entries internal.ManagedInterface) ([]metav1.ManagedFieldsEntry, error) {
|
||||
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
if err := internal.EncodeObjectManagedFields(obj, entries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("couldn't get accessor: %v", err))
|
||||
}
|
||||
return accessor.GetManagedFields(), nil
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2018 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 managedfields
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/structured-merge-diff/v6/typed"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields/internal"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
||||
// TypeConverter allows you to convert from runtime.Object to
|
||||
// typed.TypedValue and the other way around.
|
||||
type TypeConverter = internal.TypeConverter
|
||||
|
||||
// NewDeducedTypeConverter creates a TypeConverter for CRDs that don't
|
||||
// have a schema. It does implement the same interface though (and
|
||||
// create the same types of objects), so that everything can still work
|
||||
// the same. CRDs are merged with all their fields being "atomic" (lists
|
||||
// included).
|
||||
func NewDeducedTypeConverter() TypeConverter {
|
||||
return internal.NewDeducedTypeConverter()
|
||||
}
|
||||
|
||||
// NewTypeConverter builds a TypeConverter from a map of OpenAPIV3 schemas.
|
||||
// This will automatically find the proper version of the object, and the
|
||||
// corresponding schema information.
|
||||
// The keys to the map must be consistent with the names
|
||||
// used by Refs within the schemas.
|
||||
// The schemas should conform to the Kubernetes Structural Schema OpenAPI
|
||||
// restrictions found in docs:
|
||||
// https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema
|
||||
func NewTypeConverter(openapiSpec map[string]*spec.Schema, preserveUnknownFields bool) (TypeConverter, error) {
|
||||
return internal.NewTypeConverter(openapiSpec, preserveUnknownFields)
|
||||
}
|
||||
|
||||
// NewSchemeTypeConverter creates a TypeConverter that uses the provided scheme to
|
||||
// convert between runtime.Objects and TypedValues.
|
||||
func NewSchemeTypeConverter(scheme *runtime.Scheme, parser *typed.Parser) TypeConverter {
|
||||
return internal.NewSchemeTypeConverter(scheme, parser)
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- pwittrock
|
||||
reviewers:
|
||||
- apelisse
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright 2017 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 mergepatch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadJSONDoc = errors.New("invalid JSON document")
|
||||
ErrNoListOfLists = errors.New("lists of lists are not supported")
|
||||
ErrBadPatchFormatForPrimitiveList = errors.New("invalid patch format of primitive list")
|
||||
ErrBadPatchFormatForRetainKeys = errors.New("invalid patch format of retainKeys")
|
||||
ErrBadPatchFormatForSetElementOrderList = errors.New("invalid patch format of setElementOrder list")
|
||||
ErrPatchContentNotMatchRetainKeys = errors.New("patch content doesn't match retainKeys list")
|
||||
ErrUnsupportedStrategicMergePatchFormat = errors.New("strategic merge patch format is not supported")
|
||||
)
|
||||
|
||||
func ErrNoMergeKey(m map[string]interface{}, k string) error {
|
||||
return fmt.Errorf("map: %v does not contain declared merge key: %s", m, k)
|
||||
}
|
||||
|
||||
func ErrBadArgType(expected, actual interface{}) error {
|
||||
return fmt.Errorf("expected a %s, but received a %s",
|
||||
reflect.TypeOf(expected),
|
||||
reflect.TypeOf(actual))
|
||||
}
|
||||
|
||||
func ErrBadArgKind(expected, actual interface{}) error {
|
||||
var expectedKindString, actualKindString string
|
||||
if expected == nil {
|
||||
expectedKindString = "nil"
|
||||
} else {
|
||||
expectedKindString = reflect.TypeOf(expected).Kind().String()
|
||||
}
|
||||
if actual == nil {
|
||||
actualKindString = "nil"
|
||||
} else {
|
||||
actualKindString = reflect.TypeOf(actual).Kind().String()
|
||||
}
|
||||
return fmt.Errorf("expected a %s, but received a %s", expectedKindString, actualKindString)
|
||||
}
|
||||
|
||||
func ErrBadPatchType(t interface{}, m map[string]interface{}) error {
|
||||
return fmt.Errorf("unknown patch type: %s in map: %v", t, m)
|
||||
}
|
||||
|
||||
// IsPreconditionFailed returns true if the provided error indicates
|
||||
// a precondition failed.
|
||||
func IsPreconditionFailed(err error) bool {
|
||||
_, ok := err.(ErrPreconditionFailed)
|
||||
return ok
|
||||
}
|
||||
|
||||
type ErrPreconditionFailed struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func NewErrPreconditionFailed(target map[string]interface{}) ErrPreconditionFailed {
|
||||
s := fmt.Sprintf("precondition failed for: %v", target)
|
||||
return ErrPreconditionFailed{s}
|
||||
}
|
||||
|
||||
func (err ErrPreconditionFailed) Error() string {
|
||||
return err.message
|
||||
}
|
||||
|
||||
type ErrConflict struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func NewErrConflict(patch, current string) ErrConflict {
|
||||
s := fmt.Sprintf("patch:\n%s\nconflicts with changes made from original to current:\n%s\n", patch, current)
|
||||
return ErrConflict{s}
|
||||
}
|
||||
|
||||
func (err ErrConflict) Error() string {
|
||||
return err.message
|
||||
}
|
||||
|
||||
// IsConflict returns true if the provided error indicates
|
||||
// a conflict between the patch and the current configuration.
|
||||
func IsConflict(err error) bool {
|
||||
_, ok := err.(ErrConflict)
|
||||
return ok
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
Copyright 2017 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 mergepatch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/dump"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// PreconditionFunc asserts that an incompatible change is not present within a patch.
|
||||
type PreconditionFunc func(interface{}) bool
|
||||
|
||||
// RequireKeyUnchanged returns a precondition function that fails if the provided key
|
||||
// is present in the patch (indicating that its value has changed).
|
||||
func RequireKeyUnchanged(key string) PreconditionFunc {
|
||||
return func(patch interface{}) bool {
|
||||
patchMap, ok := patch.(map[string]interface{})
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// The presence of key means that its value has been changed, so the test fails.
|
||||
_, ok = patchMap[key]
|
||||
return !ok
|
||||
}
|
||||
}
|
||||
|
||||
// RequireMetadataKeyUnchanged creates a precondition function that fails
|
||||
// if the metadata.key is present in the patch (indicating its value
|
||||
// has changed).
|
||||
func RequireMetadataKeyUnchanged(key string) PreconditionFunc {
|
||||
return func(patch interface{}) bool {
|
||||
patchMap, ok := patch.(map[string]interface{})
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
patchMap1, ok := patchMap["metadata"]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
patchMap2, ok := patchMap1.(map[string]interface{})
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
_, ok = patchMap2[key]
|
||||
return !ok
|
||||
}
|
||||
}
|
||||
|
||||
func ToYAMLOrError(v interface{}) string {
|
||||
y, err := toYAML(v)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return y
|
||||
}
|
||||
|
||||
func toYAML(v interface{}) (string, error) {
|
||||
y, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("yaml marshal failed:%v\n%v\n", err, dump.Pretty(v))
|
||||
}
|
||||
|
||||
return string(y), nil
|
||||
}
|
||||
|
||||
// HasConflicts returns true if the left and right JSON interface objects overlap with
|
||||
// different values in any key. All keys are required to be strings. Since patches of the
|
||||
// same Type have congruent keys, this is valid for multiple patch types. This method
|
||||
// supports JSON merge patch semantics.
|
||||
//
|
||||
// NOTE: Numbers with different types (e.g. int(0) vs int64(0)) will be detected as conflicts.
|
||||
// Make sure the unmarshaling of left and right are consistent (e.g. use the same library).
|
||||
func HasConflicts(left, right interface{}) (bool, error) {
|
||||
switch typedLeft := left.(type) {
|
||||
case map[string]interface{}:
|
||||
switch typedRight := right.(type) {
|
||||
case map[string]interface{}:
|
||||
for key, leftValue := range typedLeft {
|
||||
rightValue, ok := typedRight[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if conflict, err := HasConflicts(leftValue, rightValue); err != nil || conflict {
|
||||
return conflict, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
case []interface{}:
|
||||
switch typedRight := right.(type) {
|
||||
case []interface{}:
|
||||
if len(typedLeft) != len(typedRight) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
for i := range typedLeft {
|
||||
if conflict, err := HasConflicts(typedLeft[i], typedRight[i]); err != nil || conflict {
|
||||
return conflict, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
case string, float64, bool, int64, nil:
|
||||
return !reflect.DeepEqual(left, right), nil
|
||||
default:
|
||||
return true, fmt.Errorf("unknown type: %v", reflect.TypeOf(left))
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2018 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 naming
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
goruntime "runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetNameFromCallsite walks back through the call stack until we find a caller from outside of the ignoredPackages
|
||||
// it returns back a shortpath/filename:line to aid in identification of this reflector when it starts logging
|
||||
func GetNameFromCallsite(ignoredPackages ...string) string {
|
||||
name := "????"
|
||||
const maxStack = 10
|
||||
for i := 1; i < maxStack; i++ {
|
||||
_, file, line, ok := goruntime.Caller(i)
|
||||
if !ok {
|
||||
file, line, ok = extractStackCreator()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
i += maxStack
|
||||
}
|
||||
if hasPackage(file, append(ignoredPackages, "/runtime/asm_")) {
|
||||
continue
|
||||
}
|
||||
|
||||
file = trimPackagePrefix(file)
|
||||
name = fmt.Sprintf("%s:%d", file, line)
|
||||
break
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// hasPackage returns true if the file is in one of the ignored packages.
|
||||
func hasPackage(file string, ignoredPackages []string) bool {
|
||||
for _, ignoredPackage := range ignoredPackages {
|
||||
if strings.Contains(file, ignoredPackage) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// trimPackagePrefix reduces duplicate values off the front of a package name.
|
||||
func trimPackagePrefix(file string) string {
|
||||
if l := strings.LastIndex(file, "/vendor/"); l >= 0 {
|
||||
return file[l+len("/vendor/"):]
|
||||
}
|
||||
if l := strings.LastIndex(file, "/src/"); l >= 0 {
|
||||
return file[l+5:]
|
||||
}
|
||||
if l := strings.LastIndex(file, "/pkg/"); l >= 0 {
|
||||
return file[l+1:]
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
var stackCreator = regexp.MustCompile(`(?m)^created by (.*)\n\s+(.*):(\d+) \+0x[[:xdigit:]]+$`)
|
||||
|
||||
// extractStackCreator retrieves the goroutine file and line that launched this stack. Returns false
|
||||
// if the creator cannot be located.
|
||||
// TODO: Go does not expose this via runtime https://github.com/golang/go/issues/11440
|
||||
func extractStackCreator() (string, int, bool) {
|
||||
stack := debug.Stack()
|
||||
matches := stackCreator.FindStringSubmatch(string(stack))
|
||||
if len(matches) != 4 {
|
||||
return "", 0, false
|
||||
}
|
||||
line, err := strconv.Atoi(matches[3])
|
||||
if err != nil {
|
||||
return "", 0, false
|
||||
}
|
||||
return matches[2], line, true
|
||||
}
|
||||
+699
@@ -0,0 +1,699 @@
|
||||
/*
|
||||
Copyright 2016 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 net
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"k8s.io/klog/v2"
|
||||
netutils "k8s.io/utils/net"
|
||||
)
|
||||
|
||||
// JoinPreservingTrailingSlash does a path.Join of the specified elements,
|
||||
// preserving any trailing slash on the last non-empty segment
|
||||
func JoinPreservingTrailingSlash(elem ...string) string {
|
||||
// do the basic path join
|
||||
result := path.Join(elem...)
|
||||
|
||||
// find the last non-empty segment
|
||||
for i := len(elem) - 1; i >= 0; i-- {
|
||||
if len(elem[i]) > 0 {
|
||||
// if the last segment ended in a slash, ensure our result does as well
|
||||
if strings.HasSuffix(elem[i], "/") && !strings.HasSuffix(result, "/") {
|
||||
result += "/"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// IsTimeout returns true if the given error is a network timeout error
|
||||
func IsTimeout(err error) bool {
|
||||
var neterr net.Error
|
||||
if errors.As(err, &neterr) {
|
||||
return neterr != nil && neterr.Timeout()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsProbableEOF returns true if the given error resembles a connection termination
|
||||
// scenario that would justify assuming that the watch is empty.
|
||||
// These errors are what the Go http stack returns back to us which are general
|
||||
// connection closure errors (strongly correlated) and callers that need to
|
||||
// differentiate probable errors in connection behavior between normal "this is
|
||||
// disconnected" should use the method.
|
||||
func IsProbableEOF(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var uerr *url.Error
|
||||
if errors.As(err, &uerr) {
|
||||
err = uerr.Err
|
||||
}
|
||||
msg := err.Error()
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
return true
|
||||
case err == io.ErrUnexpectedEOF:
|
||||
return true
|
||||
case msg == "http: can't write HTTP request on broken connection":
|
||||
return true
|
||||
case strings.Contains(msg, "http2: server sent GOAWAY and closed the connection"):
|
||||
return true
|
||||
case strings.Contains(msg, "connection reset by peer"):
|
||||
return true
|
||||
case strings.Contains(strings.ToLower(msg), "use of closed network connection"):
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var defaultTransport = http.DefaultTransport.(*http.Transport)
|
||||
|
||||
// SetOldTransportDefaults applies the defaults from http.DefaultTransport
|
||||
// for the Proxy, Dial, and TLSHandshakeTimeout fields if unset
|
||||
func SetOldTransportDefaults(t *http.Transport) *http.Transport {
|
||||
if t.Proxy == nil || isDefault(t.Proxy) {
|
||||
// http.ProxyFromEnvironment doesn't respect CIDRs and that makes it impossible to exclude things like pod and service IPs from proxy settings
|
||||
// ProxierWithNoProxyCIDR allows CIDR rules in NO_PROXY
|
||||
t.Proxy = NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment)
|
||||
}
|
||||
// If no custom dialer is set, use the default context dialer
|
||||
//lint:file-ignore SA1019 Keep supporting deprecated Dial method of custom transports
|
||||
if t.DialContext == nil && t.Dial == nil {
|
||||
t.DialContext = defaultTransport.DialContext
|
||||
}
|
||||
if t.TLSHandshakeTimeout == 0 {
|
||||
t.TLSHandshakeTimeout = defaultTransport.TLSHandshakeTimeout
|
||||
}
|
||||
if t.IdleConnTimeout == 0 {
|
||||
t.IdleConnTimeout = defaultTransport.IdleConnTimeout
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// SetTransportDefaults applies the defaults from http.DefaultTransport
|
||||
// for the Proxy, Dial, and TLSHandshakeTimeout fields if unset
|
||||
func SetTransportDefaults(t *http.Transport) *http.Transport {
|
||||
t = SetOldTransportDefaults(t)
|
||||
// Allow clients to disable http2 if needed.
|
||||
if s := os.Getenv("DISABLE_HTTP2"); len(s) > 0 {
|
||||
klog.Info("HTTP2 has been explicitly disabled")
|
||||
} else if allowsHTTP2(t) {
|
||||
if err := configureHTTP2Transport(t); err != nil {
|
||||
klog.Warningf("Transport failed http2 configuration: %v", err)
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func readIdleTimeoutSeconds() int {
|
||||
ret := 30
|
||||
// User can set the readIdleTimeout to 0 to disable the HTTP/2
|
||||
// connection health check.
|
||||
if s := os.Getenv("HTTP2_READ_IDLE_TIMEOUT_SECONDS"); len(s) > 0 {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
klog.Warningf("Illegal HTTP2_READ_IDLE_TIMEOUT_SECONDS(%q): %v."+
|
||||
" Default value %d is used", s, err, ret)
|
||||
return ret
|
||||
}
|
||||
ret = i
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func pingTimeoutSeconds() int {
|
||||
ret := 15
|
||||
if s := os.Getenv("HTTP2_PING_TIMEOUT_SECONDS"); len(s) > 0 {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
klog.Warningf("Illegal HTTP2_PING_TIMEOUT_SECONDS(%q): %v."+
|
||||
" Default value %d is used", s, err, ret)
|
||||
return ret
|
||||
}
|
||||
ret = i
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func configureHTTP2Transport(t *http.Transport) error {
|
||||
t2, err := http2.ConfigureTransports(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The following enables the HTTP/2 connection health check added in
|
||||
// https://github.com/golang/net/pull/55. The health check detects and
|
||||
// closes broken transport layer connections. Without the health check,
|
||||
// a broken connection can linger too long, e.g., a broken TCP
|
||||
// connection will be closed by the Linux kernel after 13 to 30 minutes
|
||||
// by default, which caused
|
||||
// https://github.com/kubernetes/client-go/issues/374 and
|
||||
// https://github.com/kubernetes/kubernetes/issues/87615.
|
||||
t2.ReadIdleTimeout = time.Duration(readIdleTimeoutSeconds()) * time.Second
|
||||
t2.PingTimeout = time.Duration(pingTimeoutSeconds()) * time.Second
|
||||
return nil
|
||||
}
|
||||
|
||||
func allowsHTTP2(t *http.Transport) bool {
|
||||
if t.TLSClientConfig == nil || len(t.TLSClientConfig.NextProtos) == 0 {
|
||||
// the transport expressed no NextProto preference, allow
|
||||
return true
|
||||
}
|
||||
for _, p := range t.TLSClientConfig.NextProtos {
|
||||
if p == http2.NextProtoTLS {
|
||||
// the transport explicitly allowed http/2
|
||||
return true
|
||||
}
|
||||
}
|
||||
// the transport explicitly set NextProtos and excluded http/2
|
||||
return false
|
||||
}
|
||||
|
||||
type RoundTripperWrapper interface {
|
||||
http.RoundTripper
|
||||
WrappedRoundTripper() http.RoundTripper
|
||||
}
|
||||
|
||||
type DialFunc func(ctx context.Context, net, addr string) (net.Conn, error)
|
||||
|
||||
func DialerFor(transport http.RoundTripper) (DialFunc, error) {
|
||||
if transport == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch transport := transport.(type) {
|
||||
case *http.Transport:
|
||||
// transport.DialContext takes precedence over transport.Dial
|
||||
if transport.DialContext != nil {
|
||||
return transport.DialContext, nil
|
||||
}
|
||||
// adapt transport.Dial to the DialWithContext signature
|
||||
if transport.Dial != nil {
|
||||
return func(ctx context.Context, net, addr string) (net.Conn, error) {
|
||||
return transport.Dial(net, addr)
|
||||
}, nil
|
||||
}
|
||||
// otherwise return nil
|
||||
return nil, nil
|
||||
case RoundTripperWrapper:
|
||||
return DialerFor(transport.WrappedRoundTripper())
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown transport type: %T", transport)
|
||||
}
|
||||
}
|
||||
|
||||
// CloseIdleConnectionsFor close idles connections for the Transport.
|
||||
// If the Transport is wrapped it iterates over the wrapped round trippers
|
||||
// until it finds one that implements the CloseIdleConnections method.
|
||||
// If the Transport does not have a CloseIdleConnections method
|
||||
// then this function does nothing.
|
||||
func CloseIdleConnectionsFor(transport http.RoundTripper) {
|
||||
if transport == nil {
|
||||
return
|
||||
}
|
||||
type closeIdler interface {
|
||||
CloseIdleConnections()
|
||||
}
|
||||
|
||||
switch transport := transport.(type) {
|
||||
case closeIdler:
|
||||
transport.CloseIdleConnections()
|
||||
case RoundTripperWrapper:
|
||||
CloseIdleConnectionsFor(transport.WrappedRoundTripper())
|
||||
default:
|
||||
klog.Warningf("unknown transport type: %T", transport)
|
||||
}
|
||||
}
|
||||
|
||||
type TLSClientConfigHolder interface {
|
||||
TLSClientConfig() *tls.Config
|
||||
}
|
||||
|
||||
func TLSClientConfig(transport http.RoundTripper) (*tls.Config, error) {
|
||||
if transport == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch transport := transport.(type) {
|
||||
case *http.Transport:
|
||||
return transport.TLSClientConfig, nil
|
||||
case TLSClientConfigHolder:
|
||||
return transport.TLSClientConfig(), nil
|
||||
case RoundTripperWrapper:
|
||||
return TLSClientConfig(transport.WrappedRoundTripper())
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown transport type: %T", transport)
|
||||
}
|
||||
}
|
||||
|
||||
func FormatURL(scheme string, host string, port int, path string) *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: net.JoinHostPort(host, strconv.Itoa(port)),
|
||||
Path: path,
|
||||
}
|
||||
}
|
||||
|
||||
func GetHTTPClient(req *http.Request) string {
|
||||
if ua := req.UserAgent(); len(ua) != 0 {
|
||||
return ua
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// SourceIPs splits the comma separated X-Forwarded-For header and joins it with
|
||||
// the X-Real-Ip header and/or req.RemoteAddr, ignoring invalid IPs.
|
||||
// The X-Real-Ip is omitted if it's already present in the X-Forwarded-For chain.
|
||||
// The req.RemoteAddr is always the last IP in the returned list.
|
||||
// It returns nil if all of these are empty or invalid.
|
||||
func SourceIPs(req *http.Request) []net.IP {
|
||||
var srcIPs []net.IP
|
||||
|
||||
hdr := req.Header
|
||||
// First check the X-Forwarded-For header for requests via proxy.
|
||||
hdrForwardedFor := hdr.Get("X-Forwarded-For")
|
||||
if hdrForwardedFor != "" {
|
||||
// X-Forwarded-For can be a csv of IPs in case of multiple proxies.
|
||||
// Use the first valid one.
|
||||
parts := strings.Split(hdrForwardedFor, ",")
|
||||
for _, part := range parts {
|
||||
ip := netutils.ParseIPSloppy(strings.TrimSpace(part))
|
||||
if ip != nil {
|
||||
srcIPs = append(srcIPs, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try the X-Real-Ip header.
|
||||
hdrRealIp := hdr.Get("X-Real-Ip")
|
||||
if hdrRealIp != "" {
|
||||
ip := netutils.ParseIPSloppy(hdrRealIp)
|
||||
// Only append the X-Real-Ip if it's not already contained in the X-Forwarded-For chain.
|
||||
if ip != nil && !containsIP(srcIPs, ip) {
|
||||
srcIPs = append(srcIPs, ip)
|
||||
}
|
||||
}
|
||||
|
||||
// Always include the request Remote Address as it cannot be easily spoofed.
|
||||
var remoteIP net.IP
|
||||
// Remote Address in Go's HTTP server is in the form host:port so we need to split that first.
|
||||
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err == nil {
|
||||
remoteIP = netutils.ParseIPSloppy(host)
|
||||
}
|
||||
// Fallback if Remote Address was just IP.
|
||||
if remoteIP == nil {
|
||||
remoteIP = netutils.ParseIPSloppy(req.RemoteAddr)
|
||||
}
|
||||
|
||||
// Don't duplicate remote IP if it's already the last address in the chain.
|
||||
if remoteIP != nil && (len(srcIPs) == 0 || !remoteIP.Equal(srcIPs[len(srcIPs)-1])) {
|
||||
srcIPs = append(srcIPs, remoteIP)
|
||||
}
|
||||
|
||||
return srcIPs
|
||||
}
|
||||
|
||||
// Checks whether the given IP address is contained in the list of IPs.
|
||||
func containsIP(ips []net.IP, ip net.IP) bool {
|
||||
for _, v := range ips {
|
||||
if v.Equal(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Extracts and returns the clients IP from the given request.
|
||||
// Looks at X-Forwarded-For header, X-Real-Ip header and request.RemoteAddr in that order.
|
||||
// Returns nil if none of them are set or is set to an invalid value.
|
||||
func GetClientIP(req *http.Request) net.IP {
|
||||
ips := SourceIPs(req)
|
||||
if len(ips) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ips[0]
|
||||
}
|
||||
|
||||
// Prepares the X-Forwarded-For header for another forwarding hop by appending the previous sender's
|
||||
// IP address to the X-Forwarded-For chain.
|
||||
func AppendForwardedForHeader(req *http.Request) {
|
||||
// Copied from net/http/httputil/reverseproxy.go:
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
// If we aren't the first proxy retain prior
|
||||
// X-Forwarded-For information as a comma+space
|
||||
// separated list and fold multiple headers into one.
|
||||
if prior, ok := req.Header["X-Forwarded-For"]; ok {
|
||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||
}
|
||||
req.Header.Set("X-Forwarded-For", clientIP)
|
||||
}
|
||||
}
|
||||
|
||||
var defaultProxyFuncPointer = fmt.Sprintf("%p", http.ProxyFromEnvironment)
|
||||
|
||||
// isDefault checks to see if the transportProxierFunc is pointing to the default one
|
||||
func isDefault(transportProxier func(*http.Request) (*url.URL, error)) bool {
|
||||
transportProxierPointer := fmt.Sprintf("%p", transportProxier)
|
||||
return transportProxierPointer == defaultProxyFuncPointer
|
||||
}
|
||||
|
||||
// NewProxierWithNoProxyCIDR constructs a Proxier function that respects CIDRs in NO_PROXY and delegates if
|
||||
// no matching CIDRs are found
|
||||
func NewProxierWithNoProxyCIDR(delegate func(req *http.Request) (*url.URL, error)) func(req *http.Request) (*url.URL, error) {
|
||||
// we wrap the default method, so we only need to perform our check if the NO_PROXY (or no_proxy) envvar has a CIDR in it
|
||||
noProxyEnv := os.Getenv("NO_PROXY")
|
||||
if noProxyEnv == "" {
|
||||
noProxyEnv = os.Getenv("no_proxy")
|
||||
}
|
||||
noProxyRules := strings.Split(noProxyEnv, ",")
|
||||
|
||||
cidrs := []*net.IPNet{}
|
||||
for _, noProxyRule := range noProxyRules {
|
||||
_, cidr, _ := netutils.ParseCIDRSloppy(noProxyRule)
|
||||
if cidr != nil {
|
||||
cidrs = append(cidrs, cidr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cidrs) == 0 {
|
||||
return delegate
|
||||
}
|
||||
|
||||
return func(req *http.Request) (*url.URL, error) {
|
||||
ip := netutils.ParseIPSloppy(req.URL.Hostname())
|
||||
if ip == nil {
|
||||
return delegate(req)
|
||||
}
|
||||
|
||||
for _, cidr := range cidrs {
|
||||
if cidr.Contains(ip) {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
return delegate(req)
|
||||
}
|
||||
}
|
||||
|
||||
// DialerFunc implements Dialer for the provided function.
|
||||
type DialerFunc func(req *http.Request) (net.Conn, error)
|
||||
|
||||
func (fn DialerFunc) Dial(req *http.Request) (net.Conn, error) {
|
||||
return fn(req)
|
||||
}
|
||||
|
||||
// Dialer dials a host and writes a request to it.
|
||||
type Dialer interface {
|
||||
// Dial connects to the host specified by req's URL, writes the request to the connection, and
|
||||
// returns the opened net.Conn.
|
||||
Dial(req *http.Request) (net.Conn, error)
|
||||
}
|
||||
|
||||
// CloneRequest creates a shallow copy of the request along with a deep copy of the Headers.
|
||||
func CloneRequest(req *http.Request) *http.Request {
|
||||
r := new(http.Request)
|
||||
|
||||
// shallow clone
|
||||
*r = *req
|
||||
|
||||
// deep copy headers
|
||||
r.Header = CloneHeader(req.Header)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// CloneHeader creates a deep copy of an http.Header.
|
||||
func CloneHeader(in http.Header) http.Header {
|
||||
out := make(http.Header, len(in))
|
||||
for key, values := range in {
|
||||
newValues := make([]string, len(values))
|
||||
copy(newValues, values)
|
||||
out[key] = newValues
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// WarningHeader contains a single RFC2616 14.46 warnings header
|
||||
type WarningHeader struct {
|
||||
// Codeindicates the type of warning. 299 is a miscellaneous persistent warning
|
||||
Code int
|
||||
// Agent contains the name or pseudonym of the server adding the Warning header.
|
||||
// A single "-" is recommended when agent is unknown.
|
||||
Agent string
|
||||
// Warning text
|
||||
Text string
|
||||
}
|
||||
|
||||
// ParseWarningHeaders extract RFC2616 14.46 warnings headers from the specified set of header values.
|
||||
// Multiple comma-separated warnings per header are supported.
|
||||
// If errors are encountered on a header, the remainder of that header are skipped and subsequent headers are parsed.
|
||||
// Returns successfully parsed warnings and any errors encountered.
|
||||
func ParseWarningHeaders(headers []string) ([]WarningHeader, []error) {
|
||||
var (
|
||||
results []WarningHeader
|
||||
errs []error
|
||||
)
|
||||
for _, header := range headers {
|
||||
for len(header) > 0 {
|
||||
result, remainder, err := ParseWarningHeader(header)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
break
|
||||
}
|
||||
results = append(results, result)
|
||||
header = remainder
|
||||
}
|
||||
}
|
||||
return results, errs
|
||||
}
|
||||
|
||||
var (
|
||||
codeMatcher = regexp.MustCompile(`^[0-9]{3}$`)
|
||||
wordDecoder = &mime.WordDecoder{}
|
||||
)
|
||||
|
||||
// ParseWarningHeader extracts one RFC2616 14.46 warning from the specified header,
|
||||
// returning an error if the header does not contain a correctly formatted warning.
|
||||
// Any remaining content in the header is returned.
|
||||
func ParseWarningHeader(header string) (result WarningHeader, remainder string, err error) {
|
||||
// https://tools.ietf.org/html/rfc2616#section-14.46
|
||||
// updated by
|
||||
// https://tools.ietf.org/html/rfc7234#section-5.5
|
||||
// https://tools.ietf.org/html/rfc7234#appendix-A
|
||||
// Some requirements regarding production and processing of the Warning
|
||||
// header fields have been relaxed, as it is not widely implemented.
|
||||
// Furthermore, the Warning header field no longer uses RFC 2047
|
||||
// encoding, nor does it allow multiple languages, as these aspects were
|
||||
// not implemented.
|
||||
//
|
||||
// Format is one of:
|
||||
// warn-code warn-agent "warn-text"
|
||||
// warn-code warn-agent "warn-text" "warn-date"
|
||||
//
|
||||
// warn-code is a three digit number
|
||||
// warn-agent is unquoted and contains no spaces
|
||||
// warn-text is quoted with backslash escaping (RFC2047-encoded according to RFC2616, not encoded according to RFC7234)
|
||||
// warn-date is optional, quoted, and in HTTP-date format (no embedded or escaped quotes)
|
||||
//
|
||||
// additional warnings can optionally be included in the same header by comma-separating them:
|
||||
// warn-code warn-agent "warn-text" "warn-date"[, warn-code warn-agent "warn-text" "warn-date", ...]
|
||||
|
||||
// tolerate leading whitespace
|
||||
header = strings.TrimSpace(header)
|
||||
|
||||
parts := strings.SplitN(header, " ", 3)
|
||||
if len(parts) != 3 {
|
||||
return WarningHeader{}, "", errors.New("invalid warning header: fewer than 3 segments")
|
||||
}
|
||||
code, agent, textDateRemainder := parts[0], parts[1], parts[2]
|
||||
|
||||
// verify code format
|
||||
if !codeMatcher.Match([]byte(code)) {
|
||||
return WarningHeader{}, "", errors.New("invalid warning header: code segment is not 3 digits between 100-299")
|
||||
}
|
||||
codeInt, _ := strconv.ParseInt(code, 10, 64)
|
||||
|
||||
// verify agent presence
|
||||
if len(agent) == 0 {
|
||||
return WarningHeader{}, "", errors.New("invalid warning header: empty agent segment")
|
||||
}
|
||||
if !utf8.ValidString(agent) || hasAnyRunes(agent, unicode.IsControl) {
|
||||
return WarningHeader{}, "", errors.New("invalid warning header: invalid agent")
|
||||
}
|
||||
|
||||
// verify textDateRemainder presence
|
||||
if len(textDateRemainder) == 0 {
|
||||
return WarningHeader{}, "", errors.New("invalid warning header: empty text segment")
|
||||
}
|
||||
|
||||
// extract text
|
||||
text, dateAndRemainder, err := parseQuotedString(textDateRemainder)
|
||||
if err != nil {
|
||||
return WarningHeader{}, "", fmt.Errorf("invalid warning header: %v", err)
|
||||
}
|
||||
// tolerate RFC2047-encoded text from warnings produced according to RFC2616
|
||||
if decodedText, err := wordDecoder.DecodeHeader(text); err == nil {
|
||||
text = decodedText
|
||||
}
|
||||
if !utf8.ValidString(text) || hasAnyRunes(text, unicode.IsControl) {
|
||||
return WarningHeader{}, "", errors.New("invalid warning header: invalid text")
|
||||
}
|
||||
result = WarningHeader{Code: int(codeInt), Agent: agent, Text: text}
|
||||
|
||||
if len(dateAndRemainder) > 0 {
|
||||
if dateAndRemainder[0] == '"' {
|
||||
// consume date
|
||||
foundEndQuote := false
|
||||
for i := 1; i < len(dateAndRemainder); i++ {
|
||||
if dateAndRemainder[i] == '"' {
|
||||
foundEndQuote = true
|
||||
remainder = strings.TrimSpace(dateAndRemainder[i+1:])
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundEndQuote {
|
||||
return WarningHeader{}, "", errors.New("invalid warning header: unterminated date segment")
|
||||
}
|
||||
} else {
|
||||
remainder = dateAndRemainder
|
||||
}
|
||||
}
|
||||
if len(remainder) > 0 {
|
||||
if remainder[0] == ',' {
|
||||
// consume comma if present
|
||||
remainder = strings.TrimSpace(remainder[1:])
|
||||
} else {
|
||||
return WarningHeader{}, "", errors.New("invalid warning header: unexpected token after warn-date")
|
||||
}
|
||||
}
|
||||
|
||||
return result, remainder, nil
|
||||
}
|
||||
|
||||
func parseQuotedString(quotedString string) (string, string, error) {
|
||||
if len(quotedString) == 0 {
|
||||
return "", "", errors.New("invalid quoted string: 0-length")
|
||||
}
|
||||
|
||||
if quotedString[0] != '"' {
|
||||
return "", "", errors.New("invalid quoted string: missing initial quote")
|
||||
}
|
||||
|
||||
quotedString = quotedString[1:]
|
||||
var remainder string
|
||||
escaping := false
|
||||
closedQuote := false
|
||||
result := &strings.Builder{}
|
||||
loop:
|
||||
for i := 0; i < len(quotedString); i++ {
|
||||
b := quotedString[i]
|
||||
switch b {
|
||||
case '"':
|
||||
if escaping {
|
||||
result.WriteByte(b)
|
||||
escaping = false
|
||||
} else {
|
||||
closedQuote = true
|
||||
remainder = strings.TrimSpace(quotedString[i+1:])
|
||||
break loop
|
||||
}
|
||||
case '\\':
|
||||
if escaping {
|
||||
result.WriteByte(b)
|
||||
escaping = false
|
||||
} else {
|
||||
escaping = true
|
||||
}
|
||||
default:
|
||||
result.WriteByte(b)
|
||||
escaping = false
|
||||
}
|
||||
}
|
||||
|
||||
if !closedQuote {
|
||||
return "", "", errors.New("invalid quoted string: missing closing quote")
|
||||
}
|
||||
return result.String(), remainder, nil
|
||||
}
|
||||
|
||||
func NewWarningHeader(code int, agent, text string) (string, error) {
|
||||
if code < 0 || code > 999 {
|
||||
return "", errors.New("code must be between 0 and 999")
|
||||
}
|
||||
if len(agent) == 0 {
|
||||
agent = "-"
|
||||
} else if !utf8.ValidString(agent) || strings.ContainsAny(agent, `\"`) || hasAnyRunes(agent, unicode.IsSpace, unicode.IsControl) {
|
||||
return "", errors.New("agent must be valid UTF-8 and must not contain spaces, quotes, backslashes, or control characters")
|
||||
}
|
||||
if !utf8.ValidString(text) || hasAnyRunes(text, unicode.IsControl) {
|
||||
return "", errors.New("text must be valid UTF-8 and must not contain control characters")
|
||||
}
|
||||
return fmt.Sprintf("%03d %s %s", code, agent, makeQuotedString(text)), nil
|
||||
}
|
||||
|
||||
func hasAnyRunes(s string, runeCheckers ...func(rune) bool) bool {
|
||||
for _, r := range s {
|
||||
for _, checker := range runeCheckers {
|
||||
if checker(r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func makeQuotedString(s string) string {
|
||||
result := &bytes.Buffer{}
|
||||
// opening quote
|
||||
result.WriteRune('"')
|
||||
for _, c := range s {
|
||||
switch c {
|
||||
case '"', '\\':
|
||||
// escape " and \
|
||||
result.WriteRune('\\')
|
||||
result.WriteRune(c)
|
||||
default:
|
||||
// write everything else as-is
|
||||
result.WriteRune(c)
|
||||
}
|
||||
}
|
||||
// closing quote
|
||||
result.WriteRune('"')
|
||||
return result.String()
|
||||
}
|
||||
+500
@@ -0,0 +1,500 @@
|
||||
/*
|
||||
Copyright 2016 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 net
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"strings"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
netutils "k8s.io/utils/net"
|
||||
)
|
||||
|
||||
type AddressFamily uint
|
||||
|
||||
const (
|
||||
familyIPv4 AddressFamily = 4
|
||||
familyIPv6 AddressFamily = 6
|
||||
)
|
||||
|
||||
type AddressFamilyPreference []AddressFamily
|
||||
|
||||
var (
|
||||
preferIPv4 = AddressFamilyPreference{familyIPv4, familyIPv6}
|
||||
preferIPv6 = AddressFamilyPreference{familyIPv6, familyIPv4}
|
||||
)
|
||||
|
||||
const (
|
||||
// LoopbackInterfaceName is the default name of the loopback interface
|
||||
LoopbackInterfaceName = "lo"
|
||||
)
|
||||
|
||||
const (
|
||||
ipv4RouteFile = "/proc/net/route"
|
||||
ipv6RouteFile = "/proc/net/ipv6_route"
|
||||
)
|
||||
|
||||
type Route struct {
|
||||
Interface string
|
||||
Destination net.IP
|
||||
Gateway net.IP
|
||||
Family AddressFamily
|
||||
}
|
||||
|
||||
type RouteFile struct {
|
||||
name string
|
||||
parse func(input io.Reader) ([]Route, error)
|
||||
}
|
||||
|
||||
// noRoutesError can be returned in case of no routes
|
||||
type noRoutesError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e noRoutesError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// IsNoRoutesError checks if an error is of type noRoutesError
|
||||
func IsNoRoutesError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
switch err.(type) {
|
||||
case noRoutesError:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
v4File = RouteFile{name: ipv4RouteFile, parse: getIPv4DefaultRoutes}
|
||||
v6File = RouteFile{name: ipv6RouteFile, parse: getIPv6DefaultRoutes}
|
||||
)
|
||||
|
||||
func (rf RouteFile) extract() ([]Route, error) {
|
||||
file, err := os.Open(rf.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
return rf.parse(file)
|
||||
}
|
||||
|
||||
// getIPv4DefaultRoutes obtains the IPv4 routes, and filters out non-default routes.
|
||||
func getIPv4DefaultRoutes(input io.Reader) ([]Route, error) {
|
||||
routes := []Route{}
|
||||
scanner := bufio.NewReader(input)
|
||||
for {
|
||||
line, err := scanner.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
//ignore the headers in the route info
|
||||
if strings.HasPrefix(line, "Iface") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
// Interested in fields:
|
||||
// 0 - interface name
|
||||
// 1 - destination address
|
||||
// 2 - gateway
|
||||
dest, err := parseIP(fields[1], familyIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gw, err := parseIP(fields[2], familyIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !dest.Equal(net.IPv4zero) {
|
||||
continue
|
||||
}
|
||||
routes = append(routes, Route{
|
||||
Interface: fields[0],
|
||||
Destination: dest,
|
||||
Gateway: gw,
|
||||
Family: familyIPv4,
|
||||
})
|
||||
}
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func getIPv6DefaultRoutes(input io.Reader) ([]Route, error) {
|
||||
routes := []Route{}
|
||||
scanner := bufio.NewReader(input)
|
||||
for {
|
||||
line, err := scanner.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
// Interested in fields:
|
||||
// 0 - destination address
|
||||
// 4 - gateway
|
||||
// 9 - interface name
|
||||
dest, err := parseIP(fields[0], familyIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gw, err := parseIP(fields[4], familyIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !dest.Equal(net.IPv6zero) {
|
||||
continue
|
||||
}
|
||||
if gw.Equal(net.IPv6zero) {
|
||||
continue // loopback
|
||||
}
|
||||
routes = append(routes, Route{
|
||||
Interface: fields[9],
|
||||
Destination: dest,
|
||||
Gateway: gw,
|
||||
Family: familyIPv6,
|
||||
})
|
||||
}
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// parseIP takes the hex IP address string from route file and converts it
|
||||
// to a net.IP address. For IPv4, the value must be converted to big endian.
|
||||
func parseIP(str string, family AddressFamily) (net.IP, error) {
|
||||
if str == "" {
|
||||
return nil, fmt.Errorf("input is nil")
|
||||
}
|
||||
bytes, err := hex.DecodeString(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if family == familyIPv4 {
|
||||
if len(bytes) != net.IPv4len {
|
||||
return nil, fmt.Errorf("invalid IPv4 address in route")
|
||||
}
|
||||
return net.IP([]byte{bytes[3], bytes[2], bytes[1], bytes[0]}), nil
|
||||
}
|
||||
// Must be IPv6
|
||||
if len(bytes) != net.IPv6len {
|
||||
return nil, fmt.Errorf("invalid IPv6 address in route")
|
||||
}
|
||||
return net.IP(bytes), nil
|
||||
}
|
||||
|
||||
func isInterfaceUp(intf *net.Interface) bool {
|
||||
if intf == nil {
|
||||
return false
|
||||
}
|
||||
if intf.Flags&net.FlagUp != 0 {
|
||||
klog.V(4).Infof("Interface %v is up", intf.Name)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isLoopbackOrPointToPoint(intf *net.Interface) bool {
|
||||
return intf.Flags&(net.FlagLoopback|net.FlagPointToPoint) != 0
|
||||
}
|
||||
|
||||
// getMatchingGlobalIP returns the first valid global unicast address of the given
|
||||
// 'family' from the list of 'addrs'.
|
||||
func getMatchingGlobalIP(addrs []net.Addr, family AddressFamily) (net.IP, error) {
|
||||
if len(addrs) > 0 {
|
||||
for i := range addrs {
|
||||
klog.V(4).Infof("Checking addr %s.", addrs[i].String())
|
||||
ip, _, err := netutils.ParseCIDRSloppy(addrs[i].String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if memberOf(ip, family) {
|
||||
if ip.IsGlobalUnicast() {
|
||||
klog.V(4).Infof("IP found %v", ip)
|
||||
return ip, nil
|
||||
} else {
|
||||
klog.V(4).Infof("Non-global unicast address found %v", ip)
|
||||
}
|
||||
} else {
|
||||
klog.V(4).Infof("%v is not an IPv%d address", ip, int(family))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getIPFromInterface gets the IPs on an interface and returns a global unicast address, if any. The
|
||||
// interface must be up, the IP must in the family requested, and the IP must be a global unicast address.
|
||||
func getIPFromInterface(intfName string, forFamily AddressFamily, nw networkInterfacer) (net.IP, error) {
|
||||
intf, err := nw.InterfaceByName(intfName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isInterfaceUp(intf) {
|
||||
addrs, err := nw.Addrs(intf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
klog.V(4).Infof("Interface %q has %d addresses :%v.", intfName, len(addrs), addrs)
|
||||
matchingIP, err := getMatchingGlobalIP(addrs, forFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matchingIP != nil {
|
||||
klog.V(4).Infof("Found valid IPv%d address %v for interface %q.", int(forFamily), matchingIP, intfName)
|
||||
return matchingIP, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getIPFromLoopbackInterface gets the IPs on a loopback interface and returns a global unicast address, if any.
|
||||
// The loopback interface must be up, the IP must in the family requested, and the IP must be a global unicast address.
|
||||
func getIPFromLoopbackInterface(forFamily AddressFamily, nw networkInterfacer) (net.IP, error) {
|
||||
intfs, err := nw.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, intf := range intfs {
|
||||
if !isInterfaceUp(&intf) {
|
||||
continue
|
||||
}
|
||||
if intf.Flags&(net.FlagLoopback) != 0 {
|
||||
addrs, err := nw.Addrs(&intf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
klog.V(4).Infof("Interface %q has %d addresses :%v.", intf.Name, len(addrs), addrs)
|
||||
matchingIP, err := getMatchingGlobalIP(addrs, forFamily)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matchingIP != nil {
|
||||
klog.V(4).Infof("Found valid IPv%d address %v for interface %q.", int(forFamily), matchingIP, intf.Name)
|
||||
return matchingIP, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// memberOf tells if the IP is of the desired family. Used for checking interface addresses.
|
||||
func memberOf(ip net.IP, family AddressFamily) bool {
|
||||
if ip.To4() != nil {
|
||||
return family == familyIPv4
|
||||
} else {
|
||||
return family == familyIPv6
|
||||
}
|
||||
}
|
||||
|
||||
// chooseIPFromHostInterfaces looks at all system interfaces, trying to find one that is up that
|
||||
// has a global unicast address (non-loopback, non-link local, non-point2point), and returns the IP.
|
||||
// addressFamilies determines whether it prefers IPv4 or IPv6
|
||||
func chooseIPFromHostInterfaces(nw networkInterfacer, addressFamilies AddressFamilyPreference) (net.IP, error) {
|
||||
intfs, err := nw.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(intfs) == 0 {
|
||||
return nil, fmt.Errorf("no interfaces found on host.")
|
||||
}
|
||||
for _, family := range addressFamilies {
|
||||
klog.V(4).Infof("Looking for system interface with a global IPv%d address", uint(family))
|
||||
for _, intf := range intfs {
|
||||
if !isInterfaceUp(&intf) {
|
||||
klog.V(4).Infof("Skipping: down interface %q", intf.Name)
|
||||
continue
|
||||
}
|
||||
if isLoopbackOrPointToPoint(&intf) {
|
||||
klog.V(4).Infof("Skipping: LB or P2P interface %q", intf.Name)
|
||||
continue
|
||||
}
|
||||
addrs, err := nw.Addrs(&intf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
klog.V(4).Infof("Skipping: no addresses on interface %q", intf.Name)
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ip, _, err := netutils.ParseCIDRSloppy(addr.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse CIDR for interface %q: %s", intf.Name, err)
|
||||
}
|
||||
if !memberOf(ip, family) {
|
||||
klog.V(4).Infof("Skipping: no address family match for %q on interface %q.", ip, intf.Name)
|
||||
continue
|
||||
}
|
||||
// TODO: Decide if should open up to allow IPv6 LLAs in future.
|
||||
if !ip.IsGlobalUnicast() {
|
||||
klog.V(4).Infof("Skipping: non-global address %q on interface %q.", ip, intf.Name)
|
||||
continue
|
||||
}
|
||||
klog.V(4).Infof("Found global unicast address %q on interface %q.", ip, intf.Name)
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no acceptable interface with global unicast address found on host")
|
||||
}
|
||||
|
||||
// ChooseHostInterface is a method used fetch an IP for a daemon.
|
||||
// If there is no routing info file, it will choose a global IP from the system
|
||||
// interfaces. Otherwise, it will use IPv4 and IPv6 route information to return the
|
||||
// IP of the interface with a gateway on it (with priority given to IPv4). For a node
|
||||
// with no internet connection, it returns error.
|
||||
func ChooseHostInterface() (net.IP, error) {
|
||||
return chooseHostInterface(preferIPv4)
|
||||
}
|
||||
|
||||
func chooseHostInterface(addressFamilies AddressFamilyPreference) (net.IP, error) {
|
||||
var nw networkInterfacer = networkInterface{}
|
||||
if _, err := os.Stat(ipv4RouteFile); os.IsNotExist(err) {
|
||||
return chooseIPFromHostInterfaces(nw, addressFamilies)
|
||||
}
|
||||
routes, err := getAllDefaultRoutes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return chooseHostInterfaceFromRoute(routes, nw, addressFamilies)
|
||||
}
|
||||
|
||||
// networkInterfacer defines an interface for several net library functions. Production
|
||||
// code will forward to net library functions, and unit tests will override the methods
|
||||
// for testing purposes.
|
||||
type networkInterfacer interface {
|
||||
InterfaceByName(intfName string) (*net.Interface, error)
|
||||
Addrs(intf *net.Interface) ([]net.Addr, error)
|
||||
Interfaces() ([]net.Interface, error)
|
||||
}
|
||||
|
||||
// networkInterface implements the networkInterfacer interface for production code, just
|
||||
// wrapping the underlying net library function calls.
|
||||
type networkInterface struct{}
|
||||
|
||||
func (_ networkInterface) InterfaceByName(intfName string) (*net.Interface, error) {
|
||||
return net.InterfaceByName(intfName)
|
||||
}
|
||||
|
||||
func (_ networkInterface) Addrs(intf *net.Interface) ([]net.Addr, error) {
|
||||
return intf.Addrs()
|
||||
}
|
||||
|
||||
func (_ networkInterface) Interfaces() ([]net.Interface, error) {
|
||||
return net.Interfaces()
|
||||
}
|
||||
|
||||
// getAllDefaultRoutes obtains IPv4 and IPv6 default routes on the node. If unable
|
||||
// to read the IPv4 routing info file, we return an error. If unable to read the IPv6
|
||||
// routing info file (which is optional), we'll just use the IPv4 route information.
|
||||
// Using all the routing info, if no default routes are found, an error is returned.
|
||||
func getAllDefaultRoutes() ([]Route, error) {
|
||||
routes, err := v4File.extract()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v6Routes, _ := v6File.extract()
|
||||
routes = append(routes, v6Routes...)
|
||||
if len(routes) == 0 {
|
||||
return nil, noRoutesError{
|
||||
message: fmt.Sprintf("no default routes found in %q or %q", v4File.name, v6File.name),
|
||||
}
|
||||
}
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// chooseHostInterfaceFromRoute cycles through each default route provided, looking for a
|
||||
// global IP address from the interface for the route. If there are routes but no global
|
||||
// address is obtained from the interfaces, it checks if the loopback interface has a global address.
|
||||
// addressFamilies determines whether it prefers IPv4 or IPv6
|
||||
func chooseHostInterfaceFromRoute(routes []Route, nw networkInterfacer, addressFamilies AddressFamilyPreference) (net.IP, error) {
|
||||
for _, family := range addressFamilies {
|
||||
klog.V(4).Infof("Looking for default routes with IPv%d addresses", uint(family))
|
||||
for _, route := range routes {
|
||||
if route.Family != family {
|
||||
continue
|
||||
}
|
||||
klog.V(4).Infof("Default route transits interface %q", route.Interface)
|
||||
finalIP, err := getIPFromInterface(route.Interface, family, nw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if finalIP != nil {
|
||||
klog.V(4).Infof("Found active IP %v ", finalIP)
|
||||
return finalIP, nil
|
||||
}
|
||||
// In case of network setups where default routes are present, but network
|
||||
// interfaces use only link-local addresses (e.g. as described in RFC5549).
|
||||
// the global IP is assigned to the loopback interface, and we should use it
|
||||
loopbackIP, err := getIPFromLoopbackInterface(family, nw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if loopbackIP != nil {
|
||||
klog.V(4).Infof("Found active IP %v on Loopback interface", loopbackIP)
|
||||
return loopbackIP, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
klog.V(4).Infof("No active IP found by looking at default routes")
|
||||
return nil, fmt.Errorf("unable to select an IP from default routes.")
|
||||
}
|
||||
|
||||
// ResolveBindAddress returns the IP address of a daemon, based on the given bindAddress:
|
||||
// If bindAddress is unset, it returns the host's default IP, as with ChooseHostInterface().
|
||||
// If bindAddress is unspecified or loopback, it returns the default IP of the same
|
||||
// address family as bindAddress.
|
||||
// Otherwise, it just returns bindAddress.
|
||||
func ResolveBindAddress(bindAddress net.IP) (net.IP, error) {
|
||||
addressFamilies := preferIPv4
|
||||
if bindAddress != nil && memberOf(bindAddress, familyIPv6) {
|
||||
addressFamilies = preferIPv6
|
||||
}
|
||||
|
||||
if bindAddress == nil || bindAddress.IsUnspecified() || bindAddress.IsLoopback() {
|
||||
hostIP, err := chooseHostInterface(addressFamilies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bindAddress = hostIP
|
||||
}
|
||||
return bindAddress, nil
|
||||
}
|
||||
|
||||
// ChooseBindAddressForInterface choose a global IP for a specific interface, with priority given to IPv4.
|
||||
// This is required in case of network setups where default routes are present, but network
|
||||
// interfaces use only link-local addresses (e.g. as described in RFC5549).
|
||||
// e.g when using BGP to announce a host IP over link-local ip addresses and this ip address is attached to the lo interface.
|
||||
func ChooseBindAddressForInterface(intfName string) (net.IP, error) {
|
||||
var nw networkInterfacer = networkInterface{}
|
||||
for _, family := range preferIPv4 {
|
||||
ip, err := getIPFromInterface(intfName, family, nw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unable to select an IP from %s network interface", intfName)
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
Copyright 2015 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 net
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PortRange represents a range of TCP/UDP ports. To represent a single port,
|
||||
// set Size to 1.
|
||||
type PortRange struct {
|
||||
Base int
|
||||
Size int
|
||||
}
|
||||
|
||||
// Contains tests whether a given port falls within the PortRange.
|
||||
func (pr *PortRange) Contains(p int) bool {
|
||||
return (p >= pr.Base) && ((p - pr.Base) < pr.Size)
|
||||
}
|
||||
|
||||
// String converts the PortRange to a string representation, which can be
|
||||
// parsed by PortRange.Set or ParsePortRange.
|
||||
func (pr PortRange) String() string {
|
||||
if pr.Size == 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%d-%d", pr.Base, pr.Base+pr.Size-1)
|
||||
}
|
||||
|
||||
// Set parses a string of the form "value", "min-max", or "min+offset", inclusive at both ends, and
|
||||
// sets the PortRange from it. This is part of the flag.Value and pflag.Value
|
||||
// interfaces.
|
||||
func (pr *PortRange) Set(value string) error {
|
||||
const (
|
||||
SinglePortNotation = 1 << iota
|
||||
HyphenNotation
|
||||
PlusNotation
|
||||
)
|
||||
|
||||
value = strings.TrimSpace(value)
|
||||
hyphenIndex := strings.Index(value, "-")
|
||||
plusIndex := strings.Index(value, "+")
|
||||
|
||||
if value == "" {
|
||||
pr.Base = 0
|
||||
pr.Size = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
var low, high int
|
||||
var notation int
|
||||
|
||||
if plusIndex == -1 && hyphenIndex == -1 {
|
||||
notation |= SinglePortNotation
|
||||
}
|
||||
if hyphenIndex != -1 {
|
||||
notation |= HyphenNotation
|
||||
}
|
||||
if plusIndex != -1 {
|
||||
notation |= PlusNotation
|
||||
}
|
||||
|
||||
switch notation {
|
||||
case SinglePortNotation:
|
||||
var port int
|
||||
port, err = strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
low = port
|
||||
high = port
|
||||
case HyphenNotation:
|
||||
low, err = strconv.Atoi(value[:hyphenIndex])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
high, err = strconv.Atoi(value[hyphenIndex+1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case PlusNotation:
|
||||
var offset int
|
||||
low, err = strconv.Atoi(value[:plusIndex])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
offset, err = strconv.Atoi(value[plusIndex+1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
high = low + offset
|
||||
default:
|
||||
return fmt.Errorf("unable to parse port range: %s", value)
|
||||
}
|
||||
|
||||
if low > 65535 || high > 65535 {
|
||||
return fmt.Errorf("the port range cannot be greater than 65535: %s", value)
|
||||
}
|
||||
|
||||
if high < low {
|
||||
return fmt.Errorf("end port cannot be less than start port: %s", value)
|
||||
}
|
||||
|
||||
pr.Base = low
|
||||
pr.Size = 1 + high - low
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns a descriptive string about this type. This is part of the
|
||||
// pflag.Value interface.
|
||||
func (*PortRange) Type() string {
|
||||
return "portRange"
|
||||
}
|
||||
|
||||
// ParsePortRange parses a string of the form "min-max", inclusive at both
|
||||
// ends, and initializes a new PortRange from it.
|
||||
func ParsePortRange(value string) (*PortRange, error) {
|
||||
pr := &PortRange{}
|
||||
err := pr.Set(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
func ParsePortRangeOrDie(value string) *PortRange {
|
||||
pr, err := ParsePortRange(value)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("couldn't parse port range %q: %v", value, err))
|
||||
}
|
||||
return pr
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright 2015 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 net
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
var validSchemes = sets.NewString("http", "https", "")
|
||||
|
||||
// SplitSchemeNamePort takes a string of the following forms:
|
||||
// - "<name>", returns "", "<name>","", true
|
||||
// - "<name>:<port>", returns "", "<name>","<port>",true
|
||||
// - "<scheme>:<name>:<port>", returns "<scheme>","<name>","<port>",true
|
||||
//
|
||||
// Name must be non-empty or valid will be returned false.
|
||||
// Scheme must be "http" or "https" if specified
|
||||
// Port is returned as a string, and it is not required to be numeric (could be
|
||||
// used for a named port, for example).
|
||||
func SplitSchemeNamePort(id string) (scheme, name, port string, valid bool) {
|
||||
parts := strings.Split(id, ":")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
name = parts[0]
|
||||
case 2:
|
||||
name = parts[0]
|
||||
port = parts[1]
|
||||
case 3:
|
||||
scheme = parts[0]
|
||||
name = parts[1]
|
||||
port = parts[2]
|
||||
default:
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
if len(name) > 0 && validSchemes.Has(scheme) {
|
||||
return scheme, name, port, true
|
||||
} else {
|
||||
return "", "", "", false
|
||||
}
|
||||
}
|
||||
|
||||
// JoinSchemeNamePort returns a string that specifies the scheme, name, and port:
|
||||
// - "<name>"
|
||||
// - "<name>:<port>"
|
||||
// - "<scheme>:<name>:<port>"
|
||||
//
|
||||
// None of the parameters may contain a ':' character
|
||||
// Name is required
|
||||
// Scheme must be "", "http", or "https"
|
||||
func JoinSchemeNamePort(scheme, name, port string) string {
|
||||
if len(scheme) > 0 {
|
||||
// Must include three segments to specify scheme
|
||||
return scheme + ":" + name + ":" + port
|
||||
}
|
||||
if len(port) > 0 {
|
||||
// Must include two segments to specify port
|
||||
return name + ":" + port
|
||||
}
|
||||
// Return name alone
|
||||
return name
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2016 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 net
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// IPNetEqual checks if the two input IPNets are representing the same subnet.
|
||||
// For example,
|
||||
//
|
||||
// 10.0.0.1/24 and 10.0.0.0/24 are the same subnet.
|
||||
// 10.0.0.1/24 and 10.0.0.0/25 are not the same subnet.
|
||||
func IPNetEqual(ipnet1, ipnet2 *net.IPNet) bool {
|
||||
if ipnet1 == nil || ipnet2 == nil {
|
||||
return false
|
||||
}
|
||||
if reflect.DeepEqual(ipnet1.Mask, ipnet2.Mask) && ipnet1.Contains(ipnet2.IP) && ipnet2.Contains(ipnet1.IP) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Returns if the given err is "connection reset by peer" error.
|
||||
func IsConnectionReset(err error) bool {
|
||||
var errno syscall.Errno
|
||||
if errors.As(err, &errno) {
|
||||
return errno == syscall.ECONNRESET
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Returns if the given err is "http2: client connection lost" error.
|
||||
func IsHTTP2ConnectionLost(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "http2: client connection lost")
|
||||
}
|
||||
|
||||
// Returns if the given err is "connection refused" error
|
||||
func IsConnectionRefused(err error) bool {
|
||||
var errno syscall.Errno
|
||||
if errors.As(err, &errno) {
|
||||
return errno == syscall.ECONNREFUSED
|
||||
}
|
||||
return false
|
||||
}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
Copyright 2014 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 runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// ReallyCrash controls the behavior of HandleCrash and defaults to
|
||||
// true. It's exposed so components can optionally set to false
|
||||
// to restore prior behavior. This flag is mostly used for tests to validate
|
||||
// crash conditions.
|
||||
ReallyCrash = true
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// 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))
|
||||
for i, handler := range additionalHandlers {
|
||||
handler := handler // capture loop variable
|
||||
additionalHandlersWithContext[i] = func(_ context.Context, r interface{}) {
|
||||
handler(r)
|
||||
}
|
||||
}
|
||||
|
||||
handleCrash(context.Background(), r, additionalHandlersWithContext...)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCrashWithContext 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.
|
||||
//
|
||||
// The context is used to determine how to log.
|
||||
func HandleCrashWithContext(ctx context.Context, additionalHandlers ...func(context.Context, interface{})) {
|
||||
if r := recover(); r != nil {
|
||||
handleCrash(ctx, r, additionalHandlers...)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
for _, fn := range additionalHandlers {
|
||||
fn(ctx, r)
|
||||
}
|
||||
if ReallyCrash {
|
||||
// Actually proceed to panic.
|
||||
panic(r)
|
||||
}
|
||||
}
|
||||
|
||||
// logPanic logs the caller tree when a panic occurs (except in the special case of http.ErrAbortHandler).
|
||||
func logPanic(ctx context.Context, r interface{}) {
|
||||
if r == http.ErrAbortHandler {
|
||||
// honor the http.ErrAbortHandler sentinel panic value:
|
||||
// ErrAbortHandler is a sentinel panic value to abort a handler.
|
||||
// While any panic from ServeHTTP aborts the response to the client,
|
||||
// panicking with ErrAbortHandler also suppresses logging of a stack trace to the server's error log.
|
||||
return
|
||||
}
|
||||
|
||||
// Same as stdlib http server code. Manually allocate stack trace buffer size
|
||||
// to prevent excessively large logs
|
||||
const size = 64 << 10
|
||||
stacktrace := make([]byte, size)
|
||||
stacktrace = stacktrace[:runtime.Stack(stacktrace, false)]
|
||||
|
||||
logger := klog.FromContext(ctx)
|
||||
|
||||
// For backwards compatibility, conversion to string
|
||||
// is handled here instead of defering to the logging
|
||||
// backend.
|
||||
if _, ok := r.(string); ok {
|
||||
logger.Error(nil, "Observed a panic", "panic", r, "stacktrace", string(stacktrace))
|
||||
} else {
|
||||
logger.Error(nil, "Observed a panic", "panic", fmt.Sprintf("%v", r), "panicGoValue", fmt.Sprintf("%#v", r), "stacktrace", string(stacktrace))
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorHandlers is a list of functions which will be invoked when a nonreturnable
|
||||
// error occurs.
|
||||
// TODO(lavalamp): for testability, this and the below HandleError function
|
||||
// should be packaged up into a testable and reusable object.
|
||||
var ErrorHandlers = []ErrorHandler{
|
||||
logError,
|
||||
// 1ms was the number folks were able to stomach as a global rate limit.
|
||||
// If you need to log errors more than 1000 times a second, you
|
||||
// should probably consider fixing your code instead. :)
|
||||
backoffError(1 * time.Millisecond),
|
||||
}
|
||||
|
||||
type ErrorHandler func(ctx context.Context, err error, msg string, keysAndValues ...interface{})
|
||||
|
||||
// HandlerError is a method to invoke when a non-user facing piece of code cannot
|
||||
// return an error and needs to indicate it has been ignored. Invoking this method
|
||||
// is preferable to logging the error - the default behavior is to log but the
|
||||
// errors may be sent to a remote server for analysis.
|
||||
//
|
||||
// Contextual logging: HandleErrorWithContext should be used instead of HandleError in code which supports contextual logging.
|
||||
func HandleError(err error) {
|
||||
// this is sometimes called with a nil error. We probably shouldn't fail and should do nothing instead
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
handleError(context.Background(), err, "Unhandled Error")
|
||||
}
|
||||
|
||||
// HandlerErrorWithContext is a method to invoke when a non-user facing piece of code cannot
|
||||
// return an error and needs to indicate it has been ignored. Invoking this method
|
||||
// is preferable to logging the error - the default behavior is to log but the
|
||||
// errors may be sent to a remote server for analysis. The context is used to
|
||||
// determine how to log the error.
|
||||
//
|
||||
// If contextual logging is enabled, the default log output is equivalent to
|
||||
//
|
||||
// logr.FromContext(ctx).WithName("UnhandledError").Error(err, msg, keysAndValues...)
|
||||
//
|
||||
// Without contextual logging, it is equivalent to:
|
||||
//
|
||||
// klog.ErrorS(err, msg, keysAndValues...)
|
||||
//
|
||||
// In contrast to HandleError, passing nil for the error is still going to
|
||||
// trigger a log entry. Don't construct a new error or wrap an error
|
||||
// with fmt.Errorf. Instead, add additional information via the mssage
|
||||
// and key/value pairs.
|
||||
//
|
||||
// This variant should be used instead of HandleError because it supports
|
||||
// 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...)
|
||||
}
|
||||
|
||||
// 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{}) {
|
||||
for _, fn := range ErrorHandlers {
|
||||
fn(ctx, err, msg, keysAndValues...)
|
||||
}
|
||||
}
|
||||
|
||||
// logError prints an error with the call stack of the location it was reported.
|
||||
// It expects to be called as <caller> -> HandleError[WithContext] -> handleError -> logError.
|
||||
func logError(ctx context.Context, err error, msg string, keysAndValues ...interface{}) {
|
||||
logger := klog.FromContext(ctx).WithCallDepth(3)
|
||||
logger = klog.LoggerWithName(logger, "UnhandledError")
|
||||
logger.Error(err, msg, keysAndValues...) //nolint:logcheck // logcheck complains about unknown key/value pairs.
|
||||
}
|
||||
|
||||
// backoffError blocks if it is called more often than the minPeriod.
|
||||
func backoffError(minPeriod time.Duration) ErrorHandler {
|
||||
r := &rudimentaryErrorBackoff{
|
||||
lastErrorTime: time.Now(),
|
||||
minPeriod: minPeriod,
|
||||
}
|
||||
|
||||
return func(ctx context.Context, err error, msg string, keysAndValues ...interface{}) {
|
||||
r.OnError()
|
||||
}
|
||||
}
|
||||
|
||||
type rudimentaryErrorBackoff struct {
|
||||
minPeriod time.Duration // immutable
|
||||
// TODO(lavalamp): use the clock for testability. Need to move that
|
||||
// package for that to be accessible here.
|
||||
lastErrorTimeLock sync.Mutex
|
||||
lastErrorTime time.Time
|
||||
}
|
||||
|
||||
// OnError will block if it is called more often than the embedded period time.
|
||||
// This will prevent overly tight hot error loops.
|
||||
func (r *rudimentaryErrorBackoff) OnError() {
|
||||
now := time.Now() // start the timer before acquiring the lock
|
||||
r.lastErrorTimeLock.Lock()
|
||||
d := now.Sub(r.lastErrorTime)
|
||||
r.lastErrorTime = time.Now()
|
||||
r.lastErrorTimeLock.Unlock()
|
||||
|
||||
// Do not sleep with the lock held because that causes all callers of HandleError to block.
|
||||
// We only want the current goroutine to block.
|
||||
// A negative or zero duration causes time.Sleep to return immediately.
|
||||
// If the time moves backwards for any reason, do nothing.
|
||||
time.Sleep(r.minPeriod - d)
|
||||
}
|
||||
|
||||
// GetCaller returns the caller of the function that calls it.
|
||||
func GetCaller() string {
|
||||
var pc [1]uintptr
|
||||
runtime.Callers(3, pc[:])
|
||||
f := runtime.FuncForPC(pc[0])
|
||||
if f == nil {
|
||||
return "Unable to find caller"
|
||||
}
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
// RecoverFromPanic replaces the specified error with an error containing the
|
||||
// original error, and the call tree when a panic occurs. This enables error
|
||||
// handlers to handle errors and panics the same way.
|
||||
func RecoverFromPanic(err *error) {
|
||||
if r := recover(); r != nil {
|
||||
// Same as stdlib http server code. Manually allocate stack trace buffer size
|
||||
// to prevent excessively large logs
|
||||
const size = 64 << 10
|
||||
stacktrace := make([]byte, size)
|
||||
stacktrace = stacktrace[:runtime.Stack(stacktrace, false)]
|
||||
|
||||
*err = fmt.Errorf(
|
||||
"recovered from panic %q. (err=%v) Call stack:\n%s",
|
||||
r,
|
||||
*err,
|
||||
stacktrace)
|
||||
}
|
||||
}
|
||||
|
||||
// Must panics on non-nil errors. Useful to handling programmer level errors.
|
||||
func Must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright 2022 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 sets
|
||||
|
||||
// Byte is a set of bytes, implemented via map[byte]struct{} for minimal memory consumption.
|
||||
//
|
||||
// Deprecated: use generic Set instead.
|
||||
// new ways:
|
||||
// s1 := Set[byte]{}
|
||||
// s2 := New[byte]()
|
||||
type Byte map[byte]Empty
|
||||
|
||||
// NewByte creates a Byte from a list of values.
|
||||
func NewByte(items ...byte) Byte {
|
||||
return Byte(New[byte](items...))
|
||||
}
|
||||
|
||||
// ByteKeySet creates a Byte from a keys of a map[byte](? extends interface{}).
|
||||
// If the value passed in is not actually a map, this will panic.
|
||||
func ByteKeySet[T any](theMap map[byte]T) Byte {
|
||||
return Byte(KeySet(theMap))
|
||||
}
|
||||
|
||||
// Insert adds items to the set.
|
||||
func (s Byte) Insert(items ...byte) Byte {
|
||||
return Byte(cast(s).Insert(items...))
|
||||
}
|
||||
|
||||
// Delete removes all items from the set.
|
||||
func (s Byte) Delete(items ...byte) Byte {
|
||||
return Byte(cast(s).Delete(items...))
|
||||
}
|
||||
|
||||
// Has returns true if and only if item is contained in the set.
|
||||
func (s Byte) Has(item byte) bool {
|
||||
return cast(s).Has(item)
|
||||
}
|
||||
|
||||
// HasAll returns true if and only if all items are contained in the set.
|
||||
func (s Byte) HasAll(items ...byte) bool {
|
||||
return cast(s).HasAll(items...)
|
||||
}
|
||||
|
||||
// HasAny returns true if any items are contained in the set.
|
||||
func (s Byte) HasAny(items ...byte) bool {
|
||||
return cast(s).HasAny(items...)
|
||||
}
|
||||
|
||||
// Clone returns a new set which is a copy of the current set.
|
||||
func (s Byte) Clone() Byte {
|
||||
return Byte(cast(s).Clone())
|
||||
}
|
||||
|
||||
// Difference returns a set of objects that are not in s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.Difference(s2) = {a3}
|
||||
// s2.Difference(s1) = {a4, a5}
|
||||
func (s1 Byte) Difference(s2 Byte) Byte {
|
||||
return Byte(cast(s1).Difference(cast(s2)))
|
||||
}
|
||||
|
||||
// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.SymmetricDifference(s2) = {a3, a4, a5}
|
||||
// s2.SymmetricDifference(s1) = {a3, a4, a5}
|
||||
func (s1 Byte) SymmetricDifference(s2 Byte) Byte {
|
||||
return Byte(cast(s1).SymmetricDifference(cast(s2)))
|
||||
}
|
||||
|
||||
// Union returns a new set which includes items in either s1 or s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a3, a4}
|
||||
// s1.Union(s2) = {a1, a2, a3, a4}
|
||||
// s2.Union(s1) = {a1, a2, a3, a4}
|
||||
func (s1 Byte) Union(s2 Byte) Byte {
|
||||
return Byte(cast(s1).Union(cast(s2)))
|
||||
}
|
||||
|
||||
// Intersection returns a new set which includes the item in BOTH s1 and s2
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a2, a3}
|
||||
// s1.Intersection(s2) = {a2}
|
||||
func (s1 Byte) Intersection(s2 Byte) Byte {
|
||||
return Byte(cast(s1).Intersection(cast(s2)))
|
||||
}
|
||||
|
||||
// IsSuperset returns true if and only if s1 is a superset of s2.
|
||||
func (s1 Byte) IsSuperset(s2 Byte) bool {
|
||||
return cast(s1).IsSuperset(cast(s2))
|
||||
}
|
||||
|
||||
// Equal returns true if and only if s1 is equal (as a set) to s2.
|
||||
// Two sets are equal if their membership is identical.
|
||||
// (In practice, this means same elements, order doesn't matter)
|
||||
func (s1 Byte) Equal(s2 Byte) bool {
|
||||
return cast(s1).Equal(cast(s2))
|
||||
}
|
||||
|
||||
// List returns the contents as a sorted byte slice.
|
||||
func (s Byte) List() []byte {
|
||||
return List(cast(s))
|
||||
}
|
||||
|
||||
// UnsortedList returns the slice with contents in random order.
|
||||
func (s Byte) UnsortedList() []byte {
|
||||
return cast(s).UnsortedList()
|
||||
}
|
||||
|
||||
// PopAny returns a single element from the set.
|
||||
func (s Byte) PopAny() (byte, bool) {
|
||||
return cast(s).PopAny()
|
||||
}
|
||||
|
||||
// Len returns the size of the set.
|
||||
func (s Byte) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2022 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 sets has generic set and specified sets. Generic set will
|
||||
// replace specified ones over time. And specific ones are deprecated.
|
||||
package sets
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Copyright 2022 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 sets
|
||||
|
||||
// Empty is public since it is used by some internal API objects for conversions between external
|
||||
// string arrays and internal sets, and conversion logic requires public types today.
|
||||
type Empty struct{}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright 2022 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 sets
|
||||
|
||||
// Int is a set of ints, implemented via map[int]struct{} for minimal memory consumption.
|
||||
//
|
||||
// Deprecated: use generic Set instead.
|
||||
// new ways:
|
||||
// s1 := Set[int]{}
|
||||
// s2 := New[int]()
|
||||
type Int map[int]Empty
|
||||
|
||||
// NewInt creates a Int from a list of values.
|
||||
func NewInt(items ...int) Int {
|
||||
return Int(New[int](items...))
|
||||
}
|
||||
|
||||
// IntKeySet creates a Int from a keys of a map[int](? extends interface{}).
|
||||
// If the value passed in is not actually a map, this will panic.
|
||||
func IntKeySet[T any](theMap map[int]T) Int {
|
||||
return Int(KeySet(theMap))
|
||||
}
|
||||
|
||||
// Insert adds items to the set.
|
||||
func (s Int) Insert(items ...int) Int {
|
||||
return Int(cast(s).Insert(items...))
|
||||
}
|
||||
|
||||
// Delete removes all items from the set.
|
||||
func (s Int) Delete(items ...int) Int {
|
||||
return Int(cast(s).Delete(items...))
|
||||
}
|
||||
|
||||
// Has returns true if and only if item is contained in the set.
|
||||
func (s Int) Has(item int) bool {
|
||||
return cast(s).Has(item)
|
||||
}
|
||||
|
||||
// HasAll returns true if and only if all items are contained in the set.
|
||||
func (s Int) HasAll(items ...int) bool {
|
||||
return cast(s).HasAll(items...)
|
||||
}
|
||||
|
||||
// HasAny returns true if any items are contained in the set.
|
||||
func (s Int) HasAny(items ...int) bool {
|
||||
return cast(s).HasAny(items...)
|
||||
}
|
||||
|
||||
// Clone returns a new set which is a copy of the current set.
|
||||
func (s Int) Clone() Int {
|
||||
return Int(cast(s).Clone())
|
||||
}
|
||||
|
||||
// Difference returns a set of objects that are not in s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.Difference(s2) = {a3}
|
||||
// s2.Difference(s1) = {a4, a5}
|
||||
func (s1 Int) Difference(s2 Int) Int {
|
||||
return Int(cast(s1).Difference(cast(s2)))
|
||||
}
|
||||
|
||||
// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.SymmetricDifference(s2) = {a3, a4, a5}
|
||||
// s2.SymmetricDifference(s1) = {a3, a4, a5}
|
||||
func (s1 Int) SymmetricDifference(s2 Int) Int {
|
||||
return Int(cast(s1).SymmetricDifference(cast(s2)))
|
||||
}
|
||||
|
||||
// Union returns a new set which includes items in either s1 or s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a3, a4}
|
||||
// s1.Union(s2) = {a1, a2, a3, a4}
|
||||
// s2.Union(s1) = {a1, a2, a3, a4}
|
||||
func (s1 Int) Union(s2 Int) Int {
|
||||
return Int(cast(s1).Union(cast(s2)))
|
||||
}
|
||||
|
||||
// Intersection returns a new set which includes the item in BOTH s1 and s2
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a2, a3}
|
||||
// s1.Intersection(s2) = {a2}
|
||||
func (s1 Int) Intersection(s2 Int) Int {
|
||||
return Int(cast(s1).Intersection(cast(s2)))
|
||||
}
|
||||
|
||||
// IsSuperset returns true if and only if s1 is a superset of s2.
|
||||
func (s1 Int) IsSuperset(s2 Int) bool {
|
||||
return cast(s1).IsSuperset(cast(s2))
|
||||
}
|
||||
|
||||
// Equal returns true if and only if s1 is equal (as a set) to s2.
|
||||
// Two sets are equal if their membership is identical.
|
||||
// (In practice, this means same elements, order doesn't matter)
|
||||
func (s1 Int) Equal(s2 Int) bool {
|
||||
return cast(s1).Equal(cast(s2))
|
||||
}
|
||||
|
||||
// List returns the contents as a sorted int slice.
|
||||
func (s Int) List() []int {
|
||||
return List(cast(s))
|
||||
}
|
||||
|
||||
// UnsortedList returns the slice with contents in random order.
|
||||
func (s Int) UnsortedList() []int {
|
||||
return cast(s).UnsortedList()
|
||||
}
|
||||
|
||||
// PopAny returns a single element from the set.
|
||||
func (s Int) PopAny() (int, bool) {
|
||||
return cast(s).PopAny()
|
||||
}
|
||||
|
||||
// Len returns the size of the set.
|
||||
func (s Int) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright 2022 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 sets
|
||||
|
||||
// Int32 is a set of int32s, implemented via map[int32]struct{} for minimal memory consumption.
|
||||
//
|
||||
// Deprecated: use generic Set instead.
|
||||
// new ways:
|
||||
// s1 := Set[int32]{}
|
||||
// s2 := New[int32]()
|
||||
type Int32 map[int32]Empty
|
||||
|
||||
// NewInt32 creates a Int32 from a list of values.
|
||||
func NewInt32(items ...int32) Int32 {
|
||||
return Int32(New[int32](items...))
|
||||
}
|
||||
|
||||
// Int32KeySet creates a Int32 from a keys of a map[int32](? extends interface{}).
|
||||
// If the value passed in is not actually a map, this will panic.
|
||||
func Int32KeySet[T any](theMap map[int32]T) Int32 {
|
||||
return Int32(KeySet(theMap))
|
||||
}
|
||||
|
||||
// Insert adds items to the set.
|
||||
func (s Int32) Insert(items ...int32) Int32 {
|
||||
return Int32(cast(s).Insert(items...))
|
||||
}
|
||||
|
||||
// Delete removes all items from the set.
|
||||
func (s Int32) Delete(items ...int32) Int32 {
|
||||
return Int32(cast(s).Delete(items...))
|
||||
}
|
||||
|
||||
// Has returns true if and only if item is contained in the set.
|
||||
func (s Int32) Has(item int32) bool {
|
||||
return cast(s).Has(item)
|
||||
}
|
||||
|
||||
// HasAll returns true if and only if all items are contained in the set.
|
||||
func (s Int32) HasAll(items ...int32) bool {
|
||||
return cast(s).HasAll(items...)
|
||||
}
|
||||
|
||||
// HasAny returns true if any items are contained in the set.
|
||||
func (s Int32) HasAny(items ...int32) bool {
|
||||
return cast(s).HasAny(items...)
|
||||
}
|
||||
|
||||
// Clone returns a new set which is a copy of the current set.
|
||||
func (s Int32) Clone() Int32 {
|
||||
return Int32(cast(s).Clone())
|
||||
}
|
||||
|
||||
// Difference returns a set of objects that are not in s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.Difference(s2) = {a3}
|
||||
// s2.Difference(s1) = {a4, a5}
|
||||
func (s1 Int32) Difference(s2 Int32) Int32 {
|
||||
return Int32(cast(s1).Difference(cast(s2)))
|
||||
}
|
||||
|
||||
// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.SymmetricDifference(s2) = {a3, a4, a5}
|
||||
// s2.SymmetricDifference(s1) = {a3, a4, a5}
|
||||
func (s1 Int32) SymmetricDifference(s2 Int32) Int32 {
|
||||
return Int32(cast(s1).SymmetricDifference(cast(s2)))
|
||||
}
|
||||
|
||||
// Union returns a new set which includes items in either s1 or s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a3, a4}
|
||||
// s1.Union(s2) = {a1, a2, a3, a4}
|
||||
// s2.Union(s1) = {a1, a2, a3, a4}
|
||||
func (s1 Int32) Union(s2 Int32) Int32 {
|
||||
return Int32(cast(s1).Union(cast(s2)))
|
||||
}
|
||||
|
||||
// Intersection returns a new set which includes the item in BOTH s1 and s2
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a2, a3}
|
||||
// s1.Intersection(s2) = {a2}
|
||||
func (s1 Int32) Intersection(s2 Int32) Int32 {
|
||||
return Int32(cast(s1).Intersection(cast(s2)))
|
||||
}
|
||||
|
||||
// IsSuperset returns true if and only if s1 is a superset of s2.
|
||||
func (s1 Int32) IsSuperset(s2 Int32) bool {
|
||||
return cast(s1).IsSuperset(cast(s2))
|
||||
}
|
||||
|
||||
// Equal returns true if and only if s1 is equal (as a set) to s2.
|
||||
// Two sets are equal if their membership is identical.
|
||||
// (In practice, this means same elements, order doesn't matter)
|
||||
func (s1 Int32) Equal(s2 Int32) bool {
|
||||
return cast(s1).Equal(cast(s2))
|
||||
}
|
||||
|
||||
// List returns the contents as a sorted int32 slice.
|
||||
func (s Int32) List() []int32 {
|
||||
return List(cast(s))
|
||||
}
|
||||
|
||||
// UnsortedList returns the slice with contents in random order.
|
||||
func (s Int32) UnsortedList() []int32 {
|
||||
return cast(s).UnsortedList()
|
||||
}
|
||||
|
||||
// PopAny returns a single element from the set.
|
||||
func (s Int32) PopAny() (int32, bool) {
|
||||
return cast(s).PopAny()
|
||||
}
|
||||
|
||||
// Len returns the size of the set.
|
||||
func (s Int32) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright 2022 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 sets
|
||||
|
||||
// Int64 is a set of int64s, implemented via map[int64]struct{} for minimal memory consumption.
|
||||
//
|
||||
// Deprecated: use generic Set instead.
|
||||
// new ways:
|
||||
// s1 := Set[int64]{}
|
||||
// s2 := New[int64]()
|
||||
type Int64 map[int64]Empty
|
||||
|
||||
// NewInt64 creates a Int64 from a list of values.
|
||||
func NewInt64(items ...int64) Int64 {
|
||||
return Int64(New[int64](items...))
|
||||
}
|
||||
|
||||
// Int64KeySet creates a Int64 from a keys of a map[int64](? extends interface{}).
|
||||
// If the value passed in is not actually a map, this will panic.
|
||||
func Int64KeySet[T any](theMap map[int64]T) Int64 {
|
||||
return Int64(KeySet(theMap))
|
||||
}
|
||||
|
||||
// Insert adds items to the set.
|
||||
func (s Int64) Insert(items ...int64) Int64 {
|
||||
return Int64(cast(s).Insert(items...))
|
||||
}
|
||||
|
||||
// Delete removes all items from the set.
|
||||
func (s Int64) Delete(items ...int64) Int64 {
|
||||
return Int64(cast(s).Delete(items...))
|
||||
}
|
||||
|
||||
// Has returns true if and only if item is contained in the set.
|
||||
func (s Int64) Has(item int64) bool {
|
||||
return cast(s).Has(item)
|
||||
}
|
||||
|
||||
// HasAll returns true if and only if all items are contained in the set.
|
||||
func (s Int64) HasAll(items ...int64) bool {
|
||||
return cast(s).HasAll(items...)
|
||||
}
|
||||
|
||||
// HasAny returns true if any items are contained in the set.
|
||||
func (s Int64) HasAny(items ...int64) bool {
|
||||
return cast(s).HasAny(items...)
|
||||
}
|
||||
|
||||
// Clone returns a new set which is a copy of the current set.
|
||||
func (s Int64) Clone() Int64 {
|
||||
return Int64(cast(s).Clone())
|
||||
}
|
||||
|
||||
// Difference returns a set of objects that are not in s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.Difference(s2) = {a3}
|
||||
// s2.Difference(s1) = {a4, a5}
|
||||
func (s1 Int64) Difference(s2 Int64) Int64 {
|
||||
return Int64(cast(s1).Difference(cast(s2)))
|
||||
}
|
||||
|
||||
// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.SymmetricDifference(s2) = {a3, a4, a5}
|
||||
// s2.SymmetricDifference(s1) = {a3, a4, a5}
|
||||
func (s1 Int64) SymmetricDifference(s2 Int64) Int64 {
|
||||
return Int64(cast(s1).SymmetricDifference(cast(s2)))
|
||||
}
|
||||
|
||||
// Union returns a new set which includes items in either s1 or s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a3, a4}
|
||||
// s1.Union(s2) = {a1, a2, a3, a4}
|
||||
// s2.Union(s1) = {a1, a2, a3, a4}
|
||||
func (s1 Int64) Union(s2 Int64) Int64 {
|
||||
return Int64(cast(s1).Union(cast(s2)))
|
||||
}
|
||||
|
||||
// Intersection returns a new set which includes the item in BOTH s1 and s2
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a2, a3}
|
||||
// s1.Intersection(s2) = {a2}
|
||||
func (s1 Int64) Intersection(s2 Int64) Int64 {
|
||||
return Int64(cast(s1).Intersection(cast(s2)))
|
||||
}
|
||||
|
||||
// IsSuperset returns true if and only if s1 is a superset of s2.
|
||||
func (s1 Int64) IsSuperset(s2 Int64) bool {
|
||||
return cast(s1).IsSuperset(cast(s2))
|
||||
}
|
||||
|
||||
// Equal returns true if and only if s1 is equal (as a set) to s2.
|
||||
// Two sets are equal if their membership is identical.
|
||||
// (In practice, this means same elements, order doesn't matter)
|
||||
func (s1 Int64) Equal(s2 Int64) bool {
|
||||
return cast(s1).Equal(cast(s2))
|
||||
}
|
||||
|
||||
// List returns the contents as a sorted int64 slice.
|
||||
func (s Int64) List() []int64 {
|
||||
return List(cast(s))
|
||||
}
|
||||
|
||||
// UnsortedList returns the slice with contents in random order.
|
||||
func (s Int64) UnsortedList() []int64 {
|
||||
return cast(s).UnsortedList()
|
||||
}
|
||||
|
||||
// PopAny returns a single element from the set.
|
||||
func (s Int64) PopAny() (int64, bool) {
|
||||
return cast(s).PopAny()
|
||||
}
|
||||
|
||||
// Len returns the size of the set.
|
||||
func (s Int64) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
Copyright 2022 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 sets
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Set is a set of the same type elements, implemented via map[comparable]struct{} for minimal memory consumption.
|
||||
type Set[T comparable] map[T]Empty
|
||||
|
||||
// cast transforms specified set to generic Set[T].
|
||||
func cast[T comparable](s map[T]Empty) Set[T] { return s }
|
||||
|
||||
// New creates a Set from a list of values.
|
||||
// NOTE: type param must be explicitly instantiated if given items are empty.
|
||||
func New[T comparable](items ...T) Set[T] {
|
||||
ss := make(Set[T], len(items))
|
||||
ss.Insert(items...)
|
||||
return ss
|
||||
}
|
||||
|
||||
// KeySet creates a Set from a keys of a map[comparable](? extends interface{}).
|
||||
// If the value passed in is not actually a map, this will panic.
|
||||
func KeySet[T comparable, V any](theMap map[T]V) Set[T] {
|
||||
ret := make(Set[T], len(theMap))
|
||||
for keyValue := range theMap {
|
||||
ret.Insert(keyValue)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Insert adds items to the set.
|
||||
func (s Set[T]) Insert(items ...T) Set[T] {
|
||||
for _, item := range items {
|
||||
s[item] = Empty{}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func Insert[T comparable](set Set[T], items ...T) Set[T] {
|
||||
return set.Insert(items...)
|
||||
}
|
||||
|
||||
// Delete removes all items from the set.
|
||||
func (s Set[T]) Delete(items ...T) Set[T] {
|
||||
for _, item := range items {
|
||||
delete(s, item)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Clear empties the set.
|
||||
// It is preferable to replace the set with a newly constructed set,
|
||||
// but not all callers can do that (when there are other references to the map).
|
||||
func (s Set[T]) Clear() Set[T] {
|
||||
clear(s)
|
||||
return s
|
||||
}
|
||||
|
||||
// Has returns true if and only if item is contained in the set.
|
||||
func (s Set[T]) Has(item T) bool {
|
||||
_, contained := s[item]
|
||||
return contained
|
||||
}
|
||||
|
||||
// HasAll returns true if and only if all items are contained in the set.
|
||||
func (s Set[T]) HasAll(items ...T) bool {
|
||||
for _, item := range items {
|
||||
if !s.Has(item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HasAny returns true if any items are contained in the set.
|
||||
func (s Set[T]) HasAny(items ...T) bool {
|
||||
for _, item := range items {
|
||||
if s.Has(item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Clone returns a new set which is a copy of the current set.
|
||||
func (s Set[T]) Clone() Set[T] {
|
||||
result := make(Set[T], len(s))
|
||||
for key := range s {
|
||||
result.Insert(key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Difference returns a set of objects that are not in s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.Difference(s2) = {a3}
|
||||
// s2.Difference(s1) = {a4, a5}
|
||||
func (s1 Set[T]) Difference(s2 Set[T]) Set[T] {
|
||||
result := New[T]()
|
||||
for key := range s1 {
|
||||
if !s2.Has(key) {
|
||||
result.Insert(key)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.SymmetricDifference(s2) = {a3, a4, a5}
|
||||
// s2.SymmetricDifference(s1) = {a3, a4, a5}
|
||||
func (s1 Set[T]) SymmetricDifference(s2 Set[T]) Set[T] {
|
||||
return s1.Difference(s2).Union(s2.Difference(s1))
|
||||
}
|
||||
|
||||
// Union returns a new set which includes items in either s1 or s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a3, a4}
|
||||
// s1.Union(s2) = {a1, a2, a3, a4}
|
||||
// s2.Union(s1) = {a1, a2, a3, a4}
|
||||
func (s1 Set[T]) Union(s2 Set[T]) Set[T] {
|
||||
result := s1.Clone()
|
||||
for key := range s2 {
|
||||
result.Insert(key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Intersection returns a new set which includes the item in BOTH s1 and s2
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a2, a3}
|
||||
// s1.Intersection(s2) = {a2}
|
||||
func (s1 Set[T]) Intersection(s2 Set[T]) Set[T] {
|
||||
var walk, other Set[T]
|
||||
result := New[T]()
|
||||
if s1.Len() < s2.Len() {
|
||||
walk = s1
|
||||
other = s2
|
||||
} else {
|
||||
walk = s2
|
||||
other = s1
|
||||
}
|
||||
for key := range walk {
|
||||
if other.Has(key) {
|
||||
result.Insert(key)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// IsSuperset returns true if and only if s1 is a superset of s2.
|
||||
func (s1 Set[T]) IsSuperset(s2 Set[T]) bool {
|
||||
for item := range s2 {
|
||||
if !s1.Has(item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal returns true if and only if s1 is equal (as a set) to s2.
|
||||
// Two sets are equal if their membership is identical.
|
||||
// (In practice, this means same elements, order doesn't matter)
|
||||
func (s1 Set[T]) Equal(s2 Set[T]) bool {
|
||||
return len(s1) == len(s2) && s1.IsSuperset(s2)
|
||||
}
|
||||
|
||||
// List returns the contents as a sorted T slice.
|
||||
//
|
||||
// This is a separate function and not a method because not all types supported
|
||||
// by Generic are ordered and only those can be sorted.
|
||||
func List[T cmp.Ordered](s Set[T]) []T {
|
||||
res := s.UnsortedList()
|
||||
slices.Sort(res)
|
||||
return res
|
||||
}
|
||||
|
||||
// UnsortedList returns the slice with contents in random order.
|
||||
func (s Set[T]) UnsortedList() []T {
|
||||
res := make([]T, 0, len(s))
|
||||
for key := range s {
|
||||
res = append(res, key)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// PopAny returns a single element from the set.
|
||||
func (s Set[T]) PopAny() (T, bool) {
|
||||
for key := range s {
|
||||
s.Delete(key)
|
||||
return key, true
|
||||
}
|
||||
var zeroValue T
|
||||
return zeroValue, false
|
||||
}
|
||||
|
||||
// Len returns the size of the set.
|
||||
func (s Set[T]) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright 2022 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 sets
|
||||
|
||||
// String is a set of strings, implemented via map[string]struct{} for minimal memory consumption.
|
||||
//
|
||||
// Deprecated: use generic Set instead.
|
||||
// new ways:
|
||||
// s1 := Set[string]{}
|
||||
// s2 := New[string]()
|
||||
type String map[string]Empty
|
||||
|
||||
// NewString creates a String from a list of values.
|
||||
func NewString(items ...string) String {
|
||||
return String(New[string](items...))
|
||||
}
|
||||
|
||||
// StringKeySet creates a String from a keys of a map[string](? extends interface{}).
|
||||
// If the value passed in is not actually a map, this will panic.
|
||||
func StringKeySet[T any](theMap map[string]T) String {
|
||||
return String(KeySet(theMap))
|
||||
}
|
||||
|
||||
// Insert adds items to the set.
|
||||
func (s String) Insert(items ...string) String {
|
||||
return String(cast(s).Insert(items...))
|
||||
}
|
||||
|
||||
// Delete removes all items from the set.
|
||||
func (s String) Delete(items ...string) String {
|
||||
return String(cast(s).Delete(items...))
|
||||
}
|
||||
|
||||
// Has returns true if and only if item is contained in the set.
|
||||
func (s String) Has(item string) bool {
|
||||
return cast(s).Has(item)
|
||||
}
|
||||
|
||||
// HasAll returns true if and only if all items are contained in the set.
|
||||
func (s String) HasAll(items ...string) bool {
|
||||
return cast(s).HasAll(items...)
|
||||
}
|
||||
|
||||
// HasAny returns true if any items are contained in the set.
|
||||
func (s String) HasAny(items ...string) bool {
|
||||
return cast(s).HasAny(items...)
|
||||
}
|
||||
|
||||
// Clone returns a new set which is a copy of the current set.
|
||||
func (s String) Clone() String {
|
||||
return String(cast(s).Clone())
|
||||
}
|
||||
|
||||
// Difference returns a set of objects that are not in s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.Difference(s2) = {a3}
|
||||
// s2.Difference(s1) = {a4, a5}
|
||||
func (s1 String) Difference(s2 String) String {
|
||||
return String(cast(s1).Difference(cast(s2)))
|
||||
}
|
||||
|
||||
// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection.
|
||||
// For example:
|
||||
// s1 = {a1, a2, a3}
|
||||
// s2 = {a1, a2, a4, a5}
|
||||
// s1.SymmetricDifference(s2) = {a3, a4, a5}
|
||||
// s2.SymmetricDifference(s1) = {a3, a4, a5}
|
||||
func (s1 String) SymmetricDifference(s2 String) String {
|
||||
return String(cast(s1).SymmetricDifference(cast(s2)))
|
||||
}
|
||||
|
||||
// Union returns a new set which includes items in either s1 or s2.
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a3, a4}
|
||||
// s1.Union(s2) = {a1, a2, a3, a4}
|
||||
// s2.Union(s1) = {a1, a2, a3, a4}
|
||||
func (s1 String) Union(s2 String) String {
|
||||
return String(cast(s1).Union(cast(s2)))
|
||||
}
|
||||
|
||||
// Intersection returns a new set which includes the item in BOTH s1 and s2
|
||||
// For example:
|
||||
// s1 = {a1, a2}
|
||||
// s2 = {a2, a3}
|
||||
// s1.Intersection(s2) = {a2}
|
||||
func (s1 String) Intersection(s2 String) String {
|
||||
return String(cast(s1).Intersection(cast(s2)))
|
||||
}
|
||||
|
||||
// IsSuperset returns true if and only if s1 is a superset of s2.
|
||||
func (s1 String) IsSuperset(s2 String) bool {
|
||||
return cast(s1).IsSuperset(cast(s2))
|
||||
}
|
||||
|
||||
// Equal returns true if and only if s1 is equal (as a set) to s2.
|
||||
// Two sets are equal if their membership is identical.
|
||||
// (In practice, this means same elements, order doesn't matter)
|
||||
func (s1 String) Equal(s2 String) bool {
|
||||
return cast(s1).Equal(cast(s2))
|
||||
}
|
||||
|
||||
// List returns the contents as a sorted string slice.
|
||||
func (s String) List() []string {
|
||||
return List(cast(s))
|
||||
}
|
||||
|
||||
// UnsortedList returns the slice with contents in random order.
|
||||
func (s String) UnsortedList() []string {
|
||||
return cast(s).UnsortedList()
|
||||
}
|
||||
|
||||
// PopAny returns a single element from the set.
|
||||
func (s String) PopAny() (string, bool) {
|
||||
return cast(s).PopAny()
|
||||
}
|
||||
|
||||
// Len returns the size of the set.
|
||||
func (s String) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- apelisse
|
||||
- pwittrock
|
||||
reviewers:
|
||||
- apelisse
|
||||
emeritus_approvers:
|
||||
- mengqiy
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright 2017 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 strategicpatch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type LookupPatchMetaError struct {
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e LookupPatchMetaError) Error() string {
|
||||
return fmt.Sprintf("LookupPatchMetaError(%s): %v", e.Path, e.Err)
|
||||
}
|
||||
|
||||
type FieldNotFoundError struct {
|
||||
Path string
|
||||
Field string
|
||||
}
|
||||
|
||||
func (e FieldNotFoundError) Error() string {
|
||||
return fmt.Sprintf("unable to find api field %q in %s", e.Field, e.Path)
|
||||
}
|
||||
|
||||
type InvalidTypeError struct {
|
||||
Path string
|
||||
Expected string
|
||||
Actual string
|
||||
}
|
||||
|
||||
func (e InvalidTypeError) Error() string {
|
||||
return fmt.Sprintf("invalid type for %s: got %q, expected %q", e.Path, e.Actual, e.Expected)
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
Copyright 2017 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 strategicpatch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
forkedjson "k8s.io/apimachinery/third_party/forked/golang/json"
|
||||
openapi "k8s.io/kube-openapi/pkg/util/proto"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
||||
const patchMergeKey = "x-kubernetes-patch-merge-key"
|
||||
const patchStrategy = "x-kubernetes-patch-strategy"
|
||||
|
||||
type PatchMeta struct {
|
||||
patchStrategies []string
|
||||
patchMergeKey string
|
||||
}
|
||||
|
||||
func (pm *PatchMeta) GetPatchStrategies() []string {
|
||||
if pm.patchStrategies == nil {
|
||||
return []string{}
|
||||
}
|
||||
return pm.patchStrategies
|
||||
}
|
||||
|
||||
func (pm *PatchMeta) SetPatchStrategies(ps []string) {
|
||||
pm.patchStrategies = ps
|
||||
}
|
||||
|
||||
func (pm *PatchMeta) GetPatchMergeKey() string {
|
||||
return pm.patchMergeKey
|
||||
}
|
||||
|
||||
func (pm *PatchMeta) SetPatchMergeKey(pmk string) {
|
||||
pm.patchMergeKey = pmk
|
||||
}
|
||||
|
||||
type LookupPatchMeta interface {
|
||||
// LookupPatchMetadataForStruct gets subschema and the patch metadata (e.g. patch strategy and merge key) for map.
|
||||
LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error)
|
||||
// LookupPatchMetadataForSlice get subschema and the patch metadata for slice.
|
||||
LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error)
|
||||
// Get the type name of the field
|
||||
Name() string
|
||||
}
|
||||
|
||||
type PatchMetaFromStruct struct {
|
||||
T reflect.Type
|
||||
}
|
||||
|
||||
func NewPatchMetaFromStruct(dataStruct interface{}) (PatchMetaFromStruct, error) {
|
||||
t, err := getTagStructType(dataStruct)
|
||||
return PatchMetaFromStruct{T: t}, err
|
||||
}
|
||||
|
||||
var _ LookupPatchMeta = PatchMetaFromStruct{}
|
||||
|
||||
func (s PatchMetaFromStruct) LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error) {
|
||||
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadataForStruct(s.T, key)
|
||||
if err != nil {
|
||||
return nil, PatchMeta{}, err
|
||||
}
|
||||
|
||||
return PatchMetaFromStruct{T: fieldType},
|
||||
PatchMeta{
|
||||
patchStrategies: fieldPatchStrategies,
|
||||
patchMergeKey: fieldPatchMergeKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s PatchMetaFromStruct) LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error) {
|
||||
subschema, patchMeta, err := s.LookupPatchMetadataForStruct(key)
|
||||
if err != nil {
|
||||
return nil, PatchMeta{}, err
|
||||
}
|
||||
elemPatchMetaFromStruct := subschema.(PatchMetaFromStruct)
|
||||
t := elemPatchMetaFromStruct.T
|
||||
|
||||
var elemType reflect.Type
|
||||
switch t.Kind() {
|
||||
// If t is an array or a slice, get the element type.
|
||||
// If element is still an array or a slice, return an error.
|
||||
// Otherwise, return element type.
|
||||
case reflect.Array, reflect.Slice:
|
||||
elemType = t.Elem()
|
||||
if elemType.Kind() == reflect.Array || elemType.Kind() == reflect.Slice {
|
||||
return nil, PatchMeta{}, errors.New("unexpected slice of slice")
|
||||
}
|
||||
// If t is an pointer, get the underlying element.
|
||||
// If the underlying element is neither an array nor a slice, the pointer is pointing to a slice,
|
||||
// e.g. https://github.com/kubernetes/kubernetes/blob/bc22e206c79282487ea0bf5696d5ccec7e839a76/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch_test.go#L2782-L2822
|
||||
// If the underlying element is either an array or a slice, return its element type.
|
||||
case reflect.Pointer:
|
||||
t = t.Elem()
|
||||
if t.Kind() == reflect.Array || t.Kind() == reflect.Slice {
|
||||
t = t.Elem()
|
||||
}
|
||||
elemType = t
|
||||
default:
|
||||
return nil, PatchMeta{}, fmt.Errorf("expected slice or array type, but got: %s", s.T.Kind().String())
|
||||
}
|
||||
|
||||
return PatchMetaFromStruct{T: elemType}, patchMeta, nil
|
||||
}
|
||||
|
||||
func (s PatchMetaFromStruct) Name() string {
|
||||
return s.T.Kind().String()
|
||||
}
|
||||
|
||||
func getTagStructType(dataStruct interface{}) (reflect.Type, error) {
|
||||
if dataStruct == nil {
|
||||
return nil, mergepatch.ErrBadArgKind(struct{}{}, nil)
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(dataStruct)
|
||||
// Get the underlying type for pointers
|
||||
if t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil, mergepatch.ErrBadArgKind(struct{}{}, dataStruct)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func GetTagStructTypeOrDie(dataStruct interface{}) reflect.Type {
|
||||
t, err := getTagStructType(dataStruct)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
type PatchMetaFromOpenAPIV3 struct {
|
||||
// SchemaList is required to resolve OpenAPI V3 references
|
||||
SchemaList map[string]*spec.Schema
|
||||
Schema *spec.Schema
|
||||
}
|
||||
|
||||
func (s PatchMetaFromOpenAPIV3) traverse(key string) (PatchMetaFromOpenAPIV3, error) {
|
||||
if s.Schema == nil {
|
||||
return PatchMetaFromOpenAPIV3{}, nil
|
||||
}
|
||||
if len(s.Schema.Properties) == 0 {
|
||||
return PatchMetaFromOpenAPIV3{}, fmt.Errorf("unable to find api field \"%s\"", key)
|
||||
}
|
||||
subschema, ok := s.Schema.Properties[key]
|
||||
if !ok {
|
||||
return PatchMetaFromOpenAPIV3{}, fmt.Errorf("unable to find api field \"%s\"", key)
|
||||
}
|
||||
return PatchMetaFromOpenAPIV3{SchemaList: s.SchemaList, Schema: &subschema}, nil
|
||||
}
|
||||
|
||||
func resolve(l *PatchMetaFromOpenAPIV3) error {
|
||||
if len(l.Schema.AllOf) > 0 {
|
||||
l.Schema = &l.Schema.AllOf[0]
|
||||
}
|
||||
if refString := l.Schema.Ref.String(); refString != "" {
|
||||
str := strings.TrimPrefix(refString, "#/components/schemas/")
|
||||
sch, ok := l.SchemaList[str]
|
||||
if ok {
|
||||
l.Schema = sch
|
||||
} else {
|
||||
return fmt.Errorf("unable to resolve %s in OpenAPI V3", refString)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s PatchMetaFromOpenAPIV3) LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error) {
|
||||
l, err := s.traverse(key)
|
||||
if err != nil {
|
||||
return l, PatchMeta{}, err
|
||||
}
|
||||
p := PatchMeta{}
|
||||
f, ok := l.Schema.Extensions[patchMergeKey]
|
||||
if ok {
|
||||
p.SetPatchMergeKey(f.(string))
|
||||
}
|
||||
g, ok := l.Schema.Extensions[patchStrategy]
|
||||
if ok {
|
||||
p.SetPatchStrategies(strings.Split(g.(string), ","))
|
||||
}
|
||||
|
||||
err = resolve(&l)
|
||||
return l, p, err
|
||||
}
|
||||
|
||||
func (s PatchMetaFromOpenAPIV3) LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error) {
|
||||
l, err := s.traverse(key)
|
||||
if err != nil {
|
||||
return l, PatchMeta{}, err
|
||||
}
|
||||
p := PatchMeta{}
|
||||
f, ok := l.Schema.Extensions[patchMergeKey]
|
||||
if ok {
|
||||
p.SetPatchMergeKey(f.(string))
|
||||
}
|
||||
g, ok := l.Schema.Extensions[patchStrategy]
|
||||
if ok {
|
||||
p.SetPatchStrategies(strings.Split(g.(string), ","))
|
||||
}
|
||||
if l.Schema.Items != nil {
|
||||
l.Schema = l.Schema.Items.Schema
|
||||
}
|
||||
err = resolve(&l)
|
||||
return l, p, err
|
||||
}
|
||||
|
||||
func (s PatchMetaFromOpenAPIV3) Name() string {
|
||||
schema := s.Schema
|
||||
if len(schema.Type) > 0 {
|
||||
return strings.Join(schema.Type, "")
|
||||
}
|
||||
return "Struct"
|
||||
}
|
||||
|
||||
type PatchMetaFromOpenAPI struct {
|
||||
Schema openapi.Schema
|
||||
}
|
||||
|
||||
func NewPatchMetaFromOpenAPI(s openapi.Schema) PatchMetaFromOpenAPI {
|
||||
return PatchMetaFromOpenAPI{Schema: s}
|
||||
}
|
||||
|
||||
var _ LookupPatchMeta = PatchMetaFromOpenAPI{}
|
||||
|
||||
func (s PatchMetaFromOpenAPI) LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error) {
|
||||
if s.Schema == nil {
|
||||
return &PatchMetaFromOpenAPI{}, PatchMeta{}, nil
|
||||
}
|
||||
kindItem := NewKindItem(key, s.Schema.GetPath())
|
||||
s.Schema.Accept(kindItem)
|
||||
|
||||
err := kindItem.Error()
|
||||
if err != nil {
|
||||
return nil, PatchMeta{}, err
|
||||
}
|
||||
return PatchMetaFromOpenAPI{Schema: kindItem.subschema},
|
||||
kindItem.patchmeta, nil
|
||||
}
|
||||
|
||||
func (s PatchMetaFromOpenAPI) LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error) {
|
||||
if s.Schema == nil {
|
||||
return nil, PatchMeta{}, nil
|
||||
}
|
||||
sliceItem := NewSliceItem(key, s.Schema.GetPath())
|
||||
s.Schema.Accept(sliceItem)
|
||||
|
||||
err := sliceItem.Error()
|
||||
if err != nil {
|
||||
return nil, PatchMeta{}, err
|
||||
}
|
||||
return PatchMetaFromOpenAPI{Schema: sliceItem.subschema},
|
||||
sliceItem.patchmeta, nil
|
||||
}
|
||||
|
||||
func (s PatchMetaFromOpenAPI) Name() string {
|
||||
schema := s.Schema
|
||||
return schema.GetName()
|
||||
}
|
||||
+2257
File diff suppressed because it is too large
Load Diff
+193
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
Copyright 2017 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 strategicpatch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
openapi "k8s.io/kube-openapi/pkg/util/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
patchStrategyOpenapiextensionKey = "x-kubernetes-patch-strategy"
|
||||
patchMergeKeyOpenapiextensionKey = "x-kubernetes-patch-merge-key"
|
||||
)
|
||||
|
||||
type LookupPatchItem interface {
|
||||
openapi.SchemaVisitor
|
||||
|
||||
Error() error
|
||||
Path() *openapi.Path
|
||||
}
|
||||
|
||||
type kindItem struct {
|
||||
key string
|
||||
path *openapi.Path
|
||||
err error
|
||||
patchmeta PatchMeta
|
||||
subschema openapi.Schema
|
||||
hasVisitKind bool
|
||||
}
|
||||
|
||||
func NewKindItem(key string, path *openapi.Path) *kindItem {
|
||||
return &kindItem{
|
||||
key: key,
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
var _ LookupPatchItem = &kindItem{}
|
||||
|
||||
func (item *kindItem) Error() error {
|
||||
return item.err
|
||||
}
|
||||
|
||||
func (item *kindItem) Path() *openapi.Path {
|
||||
return item.path
|
||||
}
|
||||
|
||||
func (item *kindItem) VisitPrimitive(schema *openapi.Primitive) {
|
||||
item.err = errors.New("expected kind, but got primitive")
|
||||
}
|
||||
|
||||
func (item *kindItem) VisitArray(schema *openapi.Array) {
|
||||
item.err = errors.New("expected kind, but got slice")
|
||||
}
|
||||
|
||||
func (item *kindItem) VisitMap(schema *openapi.Map) {
|
||||
item.err = errors.New("expected kind, but got map")
|
||||
}
|
||||
|
||||
func (item *kindItem) VisitReference(schema openapi.Reference) {
|
||||
if !item.hasVisitKind {
|
||||
schema.SubSchema().Accept(item)
|
||||
}
|
||||
}
|
||||
|
||||
func (item *kindItem) VisitKind(schema *openapi.Kind) {
|
||||
subschema, ok := schema.Fields[item.key]
|
||||
if !ok {
|
||||
item.err = FieldNotFoundError{Path: schema.GetPath().String(), Field: item.key}
|
||||
return
|
||||
}
|
||||
|
||||
mergeKey, patchStrategies, err := parsePatchMetadata(subschema.GetExtensions())
|
||||
if err != nil {
|
||||
item.err = err
|
||||
return
|
||||
}
|
||||
item.patchmeta = PatchMeta{
|
||||
patchStrategies: patchStrategies,
|
||||
patchMergeKey: mergeKey,
|
||||
}
|
||||
item.subschema = subschema
|
||||
}
|
||||
|
||||
type sliceItem struct {
|
||||
key string
|
||||
path *openapi.Path
|
||||
err error
|
||||
patchmeta PatchMeta
|
||||
subschema openapi.Schema
|
||||
hasVisitKind bool
|
||||
}
|
||||
|
||||
func NewSliceItem(key string, path *openapi.Path) *sliceItem {
|
||||
return &sliceItem{
|
||||
key: key,
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
var _ LookupPatchItem = &sliceItem{}
|
||||
|
||||
func (item *sliceItem) Error() error {
|
||||
return item.err
|
||||
}
|
||||
|
||||
func (item *sliceItem) Path() *openapi.Path {
|
||||
return item.path
|
||||
}
|
||||
|
||||
func (item *sliceItem) VisitPrimitive(schema *openapi.Primitive) {
|
||||
item.err = errors.New("expected slice, but got primitive")
|
||||
}
|
||||
|
||||
func (item *sliceItem) VisitArray(schema *openapi.Array) {
|
||||
if !item.hasVisitKind {
|
||||
item.err = errors.New("expected visit kind first, then visit array")
|
||||
}
|
||||
subschema := schema.SubType
|
||||
item.subschema = subschema
|
||||
}
|
||||
|
||||
func (item *sliceItem) VisitMap(schema *openapi.Map) {
|
||||
item.err = errors.New("expected slice, but got map")
|
||||
}
|
||||
|
||||
func (item *sliceItem) VisitReference(schema openapi.Reference) {
|
||||
if !item.hasVisitKind {
|
||||
schema.SubSchema().Accept(item)
|
||||
} else {
|
||||
item.subschema = schema.SubSchema()
|
||||
}
|
||||
}
|
||||
|
||||
func (item *sliceItem) VisitKind(schema *openapi.Kind) {
|
||||
subschema, ok := schema.Fields[item.key]
|
||||
if !ok {
|
||||
item.err = FieldNotFoundError{Path: schema.GetPath().String(), Field: item.key}
|
||||
return
|
||||
}
|
||||
|
||||
mergeKey, patchStrategies, err := parsePatchMetadata(subschema.GetExtensions())
|
||||
if err != nil {
|
||||
item.err = err
|
||||
return
|
||||
}
|
||||
item.patchmeta = PatchMeta{
|
||||
patchStrategies: patchStrategies,
|
||||
patchMergeKey: mergeKey,
|
||||
}
|
||||
item.hasVisitKind = true
|
||||
subschema.Accept(item)
|
||||
}
|
||||
|
||||
func parsePatchMetadata(extensions map[string]interface{}) (string, []string, error) {
|
||||
ps, foundPS := extensions[patchStrategyOpenapiextensionKey]
|
||||
var patchStrategies []string
|
||||
var mergeKey, patchStrategy string
|
||||
var ok bool
|
||||
if foundPS {
|
||||
patchStrategy, ok = ps.(string)
|
||||
if ok {
|
||||
patchStrategies = strings.Split(patchStrategy, ",")
|
||||
} else {
|
||||
return "", nil, mergepatch.ErrBadArgType(patchStrategy, ps)
|
||||
}
|
||||
}
|
||||
mk, foundMK := extensions[patchMergeKeyOpenapiextensionKey]
|
||||
if foundMK {
|
||||
mergeKey, ok = mk.(string)
|
||||
if !ok {
|
||||
return "", nil, mergepatch.ErrBadArgType(mergeKey, mk)
|
||||
}
|
||||
}
|
||||
return mergeKey, patchStrategies, nil
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
# Disable inheritance as this is an api owners file
|
||||
options:
|
||||
no_parent_owners: true
|
||||
approvers:
|
||||
- api-approvers
|
||||
reviewers:
|
||||
- api-reviewers
|
||||
labels:
|
||||
- kind/api-change
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
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"
|
||||
)
|
||||
|
||||
// NormalizationRule holds a pre-compiled regular expression and its replacement string
|
||||
// for normalizing field paths.
|
||||
type NormalizationRule struct {
|
||||
Regexp *regexp.Regexp
|
||||
Replacement string
|
||||
}
|
||||
|
||||
// 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 value.
|
||||
matchValue bool
|
||||
matchOrigin bool
|
||||
matchDetail func(want, got string) bool
|
||||
requireOriginWhenInvalid bool
|
||||
// normalizationRules holds the pre-compiled regex patterns for path normalization.
|
||||
normalizationRules []NormalizationRule
|
||||
}
|
||||
|
||||
// Matches returns true if the two Error objects match according to the
|
||||
// configured criteria. When field normalization is configured, only the
|
||||
// "got" error's field path is normalized (to bring older API versions up
|
||||
// to the internal/latest format), while "want" is assumed to already be
|
||||
// in the canonical internal API format.
|
||||
func (m ErrorMatcher) Matches(want, got *Error) bool {
|
||||
if m.matchType && want.Type != got.Type {
|
||||
return false
|
||||
}
|
||||
if m.matchField {
|
||||
// Try direct match first (common case)
|
||||
if want.Field != got.Field {
|
||||
// Fields don't match, try normalization if rules are configured.
|
||||
// Only normalize "got" - it may be from an older API version that
|
||||
// needs to be brought up to the internal/latest format that "want"
|
||||
// is already in.
|
||||
if want.Field != m.normalizePath(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
|
||||
}
|
||||
|
||||
// normalizePath applies configured path normalization rules.
|
||||
func (m ErrorMatcher) normalizePath(path string) string {
|
||||
for _, rule := range m.normalizationRules {
|
||||
normalized := rule.Regexp.ReplaceAllString(path, rule.Replacement)
|
||||
if normalized != path {
|
||||
// Only apply the first matching rule.
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// 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()
|
||||
if normalized := m.normalizePath(e.Field); normalized != e.Field {
|
||||
buf.WriteString(fmt.Sprintf("Field=%q (aka %q)", normalized, e.Field))
|
||||
} else {
|
||||
buf.WriteString(fmt.Sprintf("Field=%q", e.Field))
|
||||
}
|
||||
}
|
||||
if m.matchValue {
|
||||
comma()
|
||||
if s, ok := e.BadValue.(string); ok {
|
||||
buf.WriteString(fmt.Sprintf("Value=%q", s))
|
||||
} else {
|
||||
rv := reflect.ValueOf(e.BadValue)
|
||||
if rv.Kind() == reflect.Pointer && !rv.IsNil() {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
if rv.IsValid() && rv.CanInterface() {
|
||||
buf.WriteString(fmt.Sprintf("Value=%v", rv.Interface()))
|
||||
} else {
|
||||
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.
|
||||
// If you need to mutate the field path (e.g. to normalize across versions),
|
||||
// see ByFieldNormalized.
|
||||
func (m ErrorMatcher) ByField() ErrorMatcher {
|
||||
m.matchField = true
|
||||
return m
|
||||
}
|
||||
|
||||
// ByFieldNormalized returns a derived ErrorMatcher which also matches by field path
|
||||
// after applying normalization rules to the actual (got) error's field path.
|
||||
// This allows matching field paths from older API versions against the canonical
|
||||
// internal API format.
|
||||
//
|
||||
// The normalization rules are applied ONLY to the "got" error's field path, bringing
|
||||
// older API version field paths up to the latest/internal format. The "want" error
|
||||
// is assumed to always be in the internal API format (latest).
|
||||
//
|
||||
// The rules slice holds pre-compiled regular expressions and their replacement strings.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rules := []NormalizationRule{
|
||||
// {
|
||||
// Regexp: regexp.MustCompile(`spec\.devices\.requests\[(\d+)\]\.allocationMode`),
|
||||
// Replacement: "spec.devices.requests[$1].exactly.allocationMode",
|
||||
// },
|
||||
// }
|
||||
// matcher := ErrorMatcher{}.ByFieldNormalized(rules)
|
||||
func (m ErrorMatcher) ByFieldNormalized(rules []NormalizationRule) ErrorMatcher {
|
||||
m.matchField = true
|
||||
m.normalizationRules = rules
|
||||
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.
|
||||
// When this is used and an origin is set in the error, the matcher will
|
||||
// consider all expected errors with the same origin to be a match. The only
|
||||
// expception to this is when it finds two errors which are exactly identical,
|
||||
// which is too suspicious to ignore. This multi-matching allows tests to
|
||||
// express a single expectation ("I set the X field to an invalid value, and I
|
||||
// expect an error from origin Y") without having to know exactly how many
|
||||
// errors might be returned, or in what order, or with what wording.
|
||||
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)
|
||||
}
|
||||
|
||||
// Test compares two ErrorLists by the criteria configured in this matcher, and
|
||||
// fails the test if they don't match. The "want" errors are expected to be in
|
||||
// the internal API format (latest), while "got" errors may be from any API version
|
||||
// and will be normalized if field normalization rules are configured.
|
||||
//
|
||||
// If matching by origin is enabled and the error has a non-empty origin, a given
|
||||
// "want" error can match multiple "got" errors, and they will all be consumed.
|
||||
// The only exception to this is if the matcher got multiple identical (in every way,
|
||||
// even those not being matched on) errors, which is likely to indicate a bug.
|
||||
func (m ErrorMatcher) Test(tb TestIntf, want, got ErrorList) {
|
||||
tb.Helper()
|
||||
|
||||
exactly := m.Exactly() // makes a copy
|
||||
|
||||
// If we ever find an EXACT duplicate error, it's almost certainly a bug
|
||||
// worth reporting. If we ever find a use-case where this is not a bug, we
|
||||
// can revisit this assumption.
|
||||
seen := map[string]bool{}
|
||||
for _, g := range got {
|
||||
key := exactly.Render(g)
|
||||
if seen[key] {
|
||||
tb.Errorf("exact duplicate error:\n%s", key)
|
||||
}
|
||||
seen[key] = true
|
||||
}
|
||||
|
||||
remaining := got
|
||||
for _, w := range want {
|
||||
tmp := make(ErrorList, 0, len(remaining))
|
||||
matched := false
|
||||
for i, g := range remaining {
|
||||
if m.Matches(w, g) {
|
||||
matched = true
|
||||
if m.matchOrigin && w.Origin != "" {
|
||||
// When origin is included in the match, we allow multiple
|
||||
// matches against the same wanted error, so that tests
|
||||
// can be insulated from the exact number, order, and
|
||||
// wording of cases that might return more than one error.
|
||||
continue
|
||||
} else {
|
||||
// Single-match, save the rest of the "got" errors and move
|
||||
// on to the next "want" error.
|
||||
tmp = append(tmp, remaining[i+1:]...)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
tmp = append(tmp, g)
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
tb.Errorf("expected an error matching:\n%s", m.Render(w))
|
||||
}
|
||||
remaining = tmp
|
||||
}
|
||||
if len(remaining) > 0 {
|
||||
for _, e := range remaining {
|
||||
tb.Errorf("unmatched error:\n%s", exactly.Render(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
/*
|
||||
Copyright 2014 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
// Error is an implementation of the 'error' interface, which represents a
|
||||
// field-level validation error.
|
||||
type Error struct {
|
||||
Type ErrorType
|
||||
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 (e *Error) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Field, e.ErrorBody())
|
||||
}
|
||||
|
||||
type OmitValueType struct{}
|
||||
|
||||
var omitValue = OmitValueType{}
|
||||
|
||||
// ErrorBody returns the error message without the field name. This is useful
|
||||
// for building nice-looking higher-level error reporting.
|
||||
func (e *Error) ErrorBody() string {
|
||||
var s string
|
||||
switch e.Type {
|
||||
case ErrorTypeRequired, ErrorTypeForbidden, ErrorTypeTooLong, ErrorTypeInternal:
|
||||
s = e.Type.String()
|
||||
case ErrorTypeInvalid, ErrorTypeTypeInvalid, ErrorTypeNotSupported,
|
||||
ErrorTypeNotFound, ErrorTypeDuplicate, ErrorTypeTooMany:
|
||||
if e.BadValue == omitValue {
|
||||
s = e.Type.String()
|
||||
break
|
||||
}
|
||||
switch t := e.BadValue.(type) {
|
||||
case int64, int32, float64, float32, bool:
|
||||
// use simple printer for simple types
|
||||
s = fmt.Sprintf("%s: %v", e.Type, t)
|
||||
case string:
|
||||
s = fmt.Sprintf("%s: %q", e.Type, t)
|
||||
default:
|
||||
// use more complex techniques to render more complex types
|
||||
valstr := ""
|
||||
jb, err := json.Marshal(e.BadValue)
|
||||
if err == nil {
|
||||
// best case
|
||||
valstr = string(jb)
|
||||
} else if stringer, ok := e.BadValue.(fmt.Stringer); ok {
|
||||
// anything that defines String() is better than raw struct
|
||||
valstr = stringer.String()
|
||||
} else {
|
||||
// worst case - 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
|
||||
valstr = fmt.Sprintf("%#v", e.BadValue)
|
||||
}
|
||||
s = fmt.Sprintf("%s: %s", e.Type, valstr)
|
||||
}
|
||||
default:
|
||||
internal := InternalError(nil, fmt.Errorf("unhandled error code: %s: please report this", e.Type))
|
||||
s = internal.ErrorBody()
|
||||
}
|
||||
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.
|
||||
type ErrorType string
|
||||
|
||||
// TODO: These values are duplicated in api/types.go, but there's a circular dep. Fix it.
|
||||
const (
|
||||
// ErrorTypeNotFound is used to report failure to find a requested value
|
||||
// (e.g. looking up an ID). See NotFound().
|
||||
ErrorTypeNotFound ErrorType = "FieldValueNotFound"
|
||||
// ErrorTypeRequired is used to report required values that are not
|
||||
// provided (e.g. empty strings, null values, or empty arrays). See
|
||||
// Required().
|
||||
ErrorTypeRequired ErrorType = "FieldValueRequired"
|
||||
// ErrorTypeDuplicate is used to report collisions of values that must be
|
||||
// unique (e.g. unique IDs). See Duplicate().
|
||||
ErrorTypeDuplicate ErrorType = "FieldValueDuplicate"
|
||||
// ErrorTypeInvalid is used to report malformed values (e.g. failed regex
|
||||
// match, too long, out of bounds). See Invalid().
|
||||
ErrorTypeInvalid ErrorType = "FieldValueInvalid"
|
||||
// ErrorTypeNotSupported is used to report unknown values for enumerated
|
||||
// fields (e.g. a list of valid values). See NotSupported().
|
||||
ErrorTypeNotSupported ErrorType = "FieldValueNotSupported"
|
||||
// ErrorTypeForbidden is used to report valid (as per formatting rules)
|
||||
// values which would be accepted under some conditions, but which are not
|
||||
// permitted by the current conditions (such as security policy). See
|
||||
// Forbidden().
|
||||
ErrorTypeForbidden ErrorType = "FieldValueForbidden"
|
||||
// ErrorTypeTooLong is used to report that the given value is too long.
|
||||
// This is similar to ErrorTypeInvalid, but the error will not include the
|
||||
// too-long value. See TooLong().
|
||||
ErrorTypeTooLong ErrorType = "FieldValueTooLong"
|
||||
// ErrorTypeTooMany is used to report "too many". This is used to
|
||||
// report that a given list has too many items. This is similar to FieldValueTooLong,
|
||||
// but the error indicates quantity instead of length.
|
||||
ErrorTypeTooMany ErrorType = "FieldValueTooMany"
|
||||
// ErrorTypeInternal is used to report other errors that are not related
|
||||
// to user input. See InternalError().
|
||||
ErrorTypeInternal ErrorType = "InternalError"
|
||||
// ErrorTypeTypeInvalid is for the value did not match the schema type for that field
|
||||
ErrorTypeTypeInvalid ErrorType = "FieldValueTypeInvalid"
|
||||
)
|
||||
|
||||
// String converts a ErrorType into its corresponding canonical error message.
|
||||
func (t ErrorType) String() string {
|
||||
switch t {
|
||||
case ErrorTypeNotFound:
|
||||
return "Not found"
|
||||
case ErrorTypeRequired:
|
||||
return "Required value"
|
||||
case ErrorTypeDuplicate:
|
||||
return "Duplicate value"
|
||||
case ErrorTypeInvalid:
|
||||
return "Invalid value"
|
||||
case ErrorTypeNotSupported:
|
||||
return "Unsupported value"
|
||||
case ErrorTypeForbidden:
|
||||
return "Forbidden"
|
||||
case ErrorTypeTooLong:
|
||||
return "Too long"
|
||||
case ErrorTypeTooMany:
|
||||
return "Too many"
|
||||
case ErrorTypeInternal:
|
||||
return "Internal error"
|
||||
case ErrorTypeTypeInvalid:
|
||||
return "Invalid value"
|
||||
default:
|
||||
return fmt.Sprintf("<unknown error %q>", string(t))
|
||||
}
|
||||
}
|
||||
|
||||
// TypeInvalid returns a *Error indicating "type is invalid"
|
||||
func TypeInvalid(field *Path, value interface{}, detail string) *Error {
|
||||
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, "", "", 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, "", 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, "", "", 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, "", false}
|
||||
}
|
||||
|
||||
// NotSupported returns a *Error indicating "unsupported value".
|
||||
// This is used to report unknown values for enumerated fields (e.g. a list of
|
||||
// valid values).
|
||||
func NotSupported[T ~string](field *Path, value interface{}, validValues []T) *Error {
|
||||
detail := ""
|
||||
if len(validValues) > 0 {
|
||||
quotedValues := make([]string, len(validValues))
|
||||
for i, v := range validValues {
|
||||
quotedValues[i] = strconv.Quote(fmt.Sprint(v))
|
||||
}
|
||||
detail = "supported values: " + strings.Join(quotedValues, ", ")
|
||||
}
|
||||
return &Error{ErrorTypeNotSupported, field.String(), value, detail, "", false}
|
||||
}
|
||||
|
||||
// Forbidden returns a *Error indicating "forbidden". This is used to
|
||||
// report valid (as per formatting rules) values which would be accepted under
|
||||
// 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, "", false}
|
||||
}
|
||||
|
||||
// TooLong returns a *Error indicating "too long". This is used to report that
|
||||
// the given value is too long. This is similar to Invalid, but the returned
|
||||
// error will not include the too-long value. If maxLength is negative, it will
|
||||
// be included in the message. The value argument is not used.
|
||||
func TooLong(field *Path, _ interface{}, maxLength int) *Error {
|
||||
var msg string
|
||||
if maxLength >= 0 {
|
||||
bs := "bytes"
|
||||
if maxLength == 1 {
|
||||
bs = "byte"
|
||||
}
|
||||
msg = fmt.Sprintf("may not be more than %d %s", maxLength, bs)
|
||||
} else {
|
||||
msg = "value is too long"
|
||||
}
|
||||
return &Error{ErrorTypeTooLong, field.String(), "<value omitted>", msg, "", false}
|
||||
}
|
||||
|
||||
// TooLongMaxLength returns a *Error indicating "too long".
|
||||
// Deprecated: Use TooLong instead.
|
||||
func TooLongMaxLength(field *Path, value interface{}, maxLength int) *Error {
|
||||
return TooLong(field, "", maxLength)
|
||||
}
|
||||
|
||||
// TooMany returns a *Error indicating "too many". This is used to
|
||||
// report that a given list has too many items. This is similar to TooLong,
|
||||
// but the returned error indicates quantity instead of length.
|
||||
func TooMany(field *Path, actualQuantity, maxQuantity int) *Error {
|
||||
var msg string
|
||||
|
||||
if maxQuantity >= 0 {
|
||||
is := "items"
|
||||
if maxQuantity == 1 {
|
||||
is = "item"
|
||||
}
|
||||
msg = fmt.Sprintf("must have at most %d %s", maxQuantity, is)
|
||||
} else {
|
||||
msg = "has too many items"
|
||||
}
|
||||
|
||||
var actual interface{}
|
||||
if actualQuantity >= 0 {
|
||||
actual = actualQuantity
|
||||
} else {
|
||||
actual = omitValue
|
||||
}
|
||||
|
||||
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(), "", false}
|
||||
}
|
||||
|
||||
// ErrorList holds a set of Errors. It is plausible that we might one day have
|
||||
// non-field errors in this same umbrella package, but for now we don't, so
|
||||
// we can keep it simple and leave ErrorList here.
|
||||
type ErrorList []*Error
|
||||
|
||||
// NewErrorTypeMatcher returns an errors.Matcher that returns true
|
||||
// if the provided error is a Error and has the provided ErrorType.
|
||||
func NewErrorTypeMatcher(t ErrorType) utilerrors.Matcher {
|
||||
return func(err error) bool {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.Type == t
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// PrefixDetail adds a prefix to the Detail for all errors in the list and returns the updated list.
|
||||
func (list ErrorList) PrefixDetail(prefix string) ErrorList {
|
||||
for _, err := range list {
|
||||
err.Detail = prefix + err.Detail
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// ToAggregate converts the ErrorList into an errors.Aggregate.
|
||||
func (list ErrorList) ToAggregate() utilerrors.Aggregate {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
errs := make([]error, 0, len(list))
|
||||
errorMsgs := sets.NewString()
|
||||
for _, err := range list {
|
||||
msg := fmt.Sprintf("%v", err)
|
||||
if errorMsgs.Has(msg) {
|
||||
continue
|
||||
}
|
||||
errorMsgs.Insert(msg)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
func fromAggregate(agg utilerrors.Aggregate) ErrorList {
|
||||
errs := agg.Errors()
|
||||
list := make(ErrorList, len(errs))
|
||||
for i := range errs {
|
||||
list[i] = errs[i].(*Error)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Filter removes items from the ErrorList that match the provided fns.
|
||||
func (list ErrorList) Filter(fns ...utilerrors.Matcher) ErrorList {
|
||||
err := utilerrors.FilterOut(list.ToAggregate(), fns...)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// 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
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
Copyright 2015 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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type pathOptions struct {
|
||||
path *Path
|
||||
}
|
||||
|
||||
// PathOption modifies a pathOptions
|
||||
type PathOption func(o *pathOptions)
|
||||
|
||||
// WithPath generates a PathOption
|
||||
func WithPath(p *Path) PathOption {
|
||||
return func(o *pathOptions) {
|
||||
o.path = p
|
||||
}
|
||||
}
|
||||
|
||||
// ToPath produces *Path from a set of PathOption
|
||||
func ToPath(opts ...PathOption) *Path {
|
||||
c := &pathOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c.path
|
||||
}
|
||||
|
||||
// Path represents the path from some root to a particular field.
|
||||
type Path struct {
|
||||
name string // the name of this field or "" if this is an index
|
||||
index string // if name == "", this is a subscript (index or map key) of the previous element
|
||||
parent *Path // nil if this is the root element
|
||||
}
|
||||
|
||||
// NewPath creates a root Path object.
|
||||
func NewPath(name string, moreNames ...string) *Path {
|
||||
r := &Path{name: name, parent: nil}
|
||||
for _, anotherName := range moreNames {
|
||||
r = &Path{name: anotherName, parent: r}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Root returns the root element of this Path.
|
||||
func (p *Path) Root() *Path {
|
||||
for ; p.parent != nil; p = p.parent {
|
||||
// Do nothing.
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// Child creates a new Path that is a child of the method receiver.
|
||||
func (p *Path) Child(name string, moreNames ...string) *Path {
|
||||
r := NewPath(name, moreNames...)
|
||||
r.Root().parent = p
|
||||
return r
|
||||
}
|
||||
|
||||
// Index indicates that the previous Path is to be subscripted by an int.
|
||||
// This sets the same underlying value as Key.
|
||||
func (p *Path) Index(index int) *Path {
|
||||
return &Path{index: strconv.Itoa(index), parent: p}
|
||||
}
|
||||
|
||||
// Key indicates that the previous Path is to be subscripted by a string.
|
||||
// This sets the same underlying value as Index.
|
||||
func (p *Path) Key(key string) *Path {
|
||||
return &Path{index: key, parent: p}
|
||||
}
|
||||
|
||||
// String produces a string representation of the Path.
|
||||
func (p *Path) String() string {
|
||||
if p == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
// make a slice to iterate
|
||||
elems := []*Path{}
|
||||
for ; p != nil; p = p.parent {
|
||||
elems = append(elems, p)
|
||||
}
|
||||
|
||||
// iterate, but it has to be backwards
|
||||
buf := bytes.NewBuffer(nil)
|
||||
for i := range elems {
|
||||
p := elems[len(elems)-1-i]
|
||||
if p.parent != nil && len(p.name) > 0 {
|
||||
// This is either the root or it is a subscript.
|
||||
buf.WriteString(".")
|
||||
}
|
||||
if len(p.name) > 0 {
|
||||
buf.WriteString(p.name)
|
||||
} else {
|
||||
fmt.Fprintf(buf, "[%s]", p.index)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
+278
@@ -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
|
||||
}
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
/*
|
||||
Copyright 2014 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"
|
||||
"math"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/validate/content"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// IsQualifiedName tests whether the value passed is what Kubernetes calls a
|
||||
// "qualified name". This is a format used in various places throughout the
|
||||
// system. If the value is not valid, a list of error strings is returned.
|
||||
// Otherwise an empty list (or nil) is returned.
|
||||
// Deprecated: Use k8s.io/apimachinery/pkg/api/validate/content.IsQualifiedName instead.
|
||||
var IsQualifiedName = content.IsLabelKey
|
||||
|
||||
// IsFullyQualifiedName checks if the name is fully qualified. This is similar
|
||||
// to IsFullyQualifiedDomainName but requires a minimum of 3 segments instead of
|
||||
// 2 and does not accept a trailing . as valid.
|
||||
// TODO: This function is deprecated and preserved until all callers migrate to
|
||||
// IsFullyQualifiedDomainName; please don't add new callers.
|
||||
func IsFullyQualifiedName(fldPath *field.Path, name string) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if len(name) == 0 {
|
||||
return append(allErrors, field.Required(fldPath, ""))
|
||||
}
|
||||
if errs := IsDNS1123Subdomain(name); len(errs) > 0 {
|
||||
return append(allErrors, field.Invalid(fldPath, name, strings.Join(errs, ",")))
|
||||
}
|
||||
if len(strings.Split(name, ".")) < 3 {
|
||||
return append(allErrors, field.Invalid(fldPath, name, "should be a domain with at least three segments separated by dots"))
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// IsFullyQualifiedDomainName checks if the domain name is fully qualified. This
|
||||
// is similar to IsFullyQualifiedName but only requires a minimum of 2 segments
|
||||
// instead of 3 and accepts a trailing . as valid.
|
||||
func IsFullyQualifiedDomainName(fldPath *field.Path, name string) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if len(name) == 0 {
|
||||
return append(allErrors, field.Required(fldPath, ""))
|
||||
}
|
||||
if strings.HasSuffix(name, ".") {
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
if errs := IsDNS1123Subdomain(name); len(errs) > 0 {
|
||||
return append(allErrors, field.Invalid(fldPath, name, strings.Join(errs, ",")))
|
||||
}
|
||||
if len(strings.Split(name, ".")) < 2 {
|
||||
return append(allErrors, field.Invalid(fldPath, name, "should be a domain with at least two segments separated by dots"))
|
||||
}
|
||||
for _, label := range strings.Split(name, ".") {
|
||||
if errs := IsDNS1123Label(label); len(errs) > 0 {
|
||||
return append(allErrors, field.Invalid(fldPath, label, strings.Join(errs, ",")))
|
||||
}
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// Allowed characters in an HTTP Path as defined by RFC 3986. A HTTP path may
|
||||
// contain:
|
||||
// * unreserved characters (alphanumeric, '-', '.', '_', '~')
|
||||
// * percent-encoded octets
|
||||
// * sub-delims ("!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=")
|
||||
// * a colon character (":")
|
||||
const httpPathFmt string = `[A-Za-z0-9/\-._~%!$&'()*+,;=:]+`
|
||||
|
||||
var httpPathRegexp = regexp.MustCompile("^" + httpPathFmt + "$")
|
||||
|
||||
// IsDomainPrefixedPath checks if the given string is a domain-prefixed path
|
||||
// (e.g. acme.io/foo). All characters before the first "/" must be a valid
|
||||
// subdomain as defined by RFC 1123. All characters trailing the first "/" must
|
||||
// be valid HTTP Path characters as defined by RFC 3986.
|
||||
func IsDomainPrefixedPath(fldPath *field.Path, dpPath string) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
if len(dpPath) == 0 {
|
||||
return append(allErrs, field.Required(fldPath, ""))
|
||||
}
|
||||
|
||||
segments := strings.SplitN(dpPath, "/", 2)
|
||||
if len(segments) != 2 || len(segments[0]) == 0 || len(segments[1]) == 0 {
|
||||
return append(allErrs, field.Invalid(fldPath, dpPath, "must be a domain-prefixed path (such as \"acme.io/foo\")"))
|
||||
}
|
||||
|
||||
host := segments[0]
|
||||
for _, err := range IsDNS1123Subdomain(host) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, host, err))
|
||||
}
|
||||
|
||||
path := segments[1]
|
||||
if !httpPathRegexp.MatchString(path) {
|
||||
return append(allErrs, field.Invalid(fldPath, path, RegexError("Invalid path", httpPathFmt)))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// IsDomainPrefixedKey checks if the given key string is a domain-prefixed key
|
||||
// (e.g. acme.io/foo). All characters before the first "/" must be a valid
|
||||
// subdomain as defined by RFC 1123. All characters trailing the first "/" must
|
||||
// be non-empty and match the regex ^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$.
|
||||
func IsDomainPrefixedKey(fldPath *field.Path, key string) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
if len(key) == 0 {
|
||||
return append(allErrs, field.Required(fldPath, ""))
|
||||
}
|
||||
for _, errMessages := range content.IsLabelKey(key) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, key, errMessages))
|
||||
}
|
||||
|
||||
if len(allErrs) > 0 {
|
||||
return allErrs
|
||||
}
|
||||
|
||||
segments := strings.Split(key, "/")
|
||||
if len(segments) != 2 {
|
||||
return append(allErrs, field.Invalid(fldPath, key, "must be a domain-prefixed key (such as \"acme.io/foo\")"))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// LabelValueMaxLength is a label's max length
|
||||
// Deprecated: Use k8s.io/apimachinery/pkg/api/validate/content.LabelValueMaxLength instead.
|
||||
const LabelValueMaxLength int = content.LabelValueMaxLength
|
||||
|
||||
// IsValidLabelValue tests whether the value passed is a valid label value. If
|
||||
// the value is not valid, a list of error strings is returned. Otherwise an
|
||||
// empty list (or nil) is returned.
|
||||
// Deprecated: Use k8s.io/apimachinery/pkg/api/validate/content.IsLabelValue instead.
|
||||
var IsValidLabelValue = content.IsLabelValue
|
||||
|
||||
const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
|
||||
const dns1123LabelFmtWithUnderscore string = "_?[a-z0-9]([-_a-z0-9]*[a-z0-9])?"
|
||||
|
||||
const dns1123LabelErrMsg string = "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character"
|
||||
|
||||
// DNS1123LabelMaxLength is a label's max length in DNS (RFC 1123)
|
||||
const DNS1123LabelMaxLength int = 63
|
||||
|
||||
var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$")
|
||||
|
||||
// IsDNS1123Label tests for a string that conforms to the definition of a label in
|
||||
// DNS (RFC 1123).
|
||||
func IsDNS1123Label(value string) []string {
|
||||
var errs []string
|
||||
if len(value) > DNS1123LabelMaxLength {
|
||||
errs = append(errs, MaxLenError(DNS1123LabelMaxLength))
|
||||
}
|
||||
if !dns1123LabelRegexp.MatchString(value) {
|
||||
if dns1123SubdomainRegexp.MatchString(value) {
|
||||
// It was a valid subdomain and not a valid label. Since we
|
||||
// already checked length, it must be dots.
|
||||
errs = append(errs, "must not contain dots")
|
||||
} else {
|
||||
errs = append(errs, RegexError(dns1123LabelErrMsg, dns1123LabelFmt, "my-name", "123-abc"))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*"
|
||||
const dns1123SubdomainErrorMsg string = "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character"
|
||||
|
||||
const dns1123SubdomainFmtWithUnderscore string = dns1123LabelFmtWithUnderscore + "(\\." + dns1123LabelFmtWithUnderscore + ")*"
|
||||
const dns1123SubdomainErrorMsgFG string = "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '_', '-' or '.', and must start and end with an alphanumeric character"
|
||||
|
||||
// DNS1123SubdomainMaxLength is a subdomain's max length in DNS (RFC 1123)
|
||||
const DNS1123SubdomainMaxLength int = 253
|
||||
|
||||
var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$")
|
||||
var dns1123SubdomainRegexpWithUnderscore = regexp.MustCompile("^" + dns1123SubdomainFmtWithUnderscore + "$")
|
||||
|
||||
// IsDNS1123Subdomain tests for a string that conforms to the definition of a
|
||||
// subdomain in DNS (RFC 1123).
|
||||
func IsDNS1123Subdomain(value string) []string {
|
||||
var errs []string
|
||||
if len(value) > DNS1123SubdomainMaxLength {
|
||||
errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
|
||||
}
|
||||
if !dns1123SubdomainRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// IsDNS1123SubdomainWithUnderscore tests for a string that conforms to the definition of a
|
||||
// subdomain in DNS (RFC 1123), but allows the use of an underscore in the string
|
||||
func IsDNS1123SubdomainWithUnderscore(value string) []string {
|
||||
var errs []string
|
||||
if len(value) > DNS1123SubdomainMaxLength {
|
||||
errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
|
||||
}
|
||||
if !dns1123SubdomainRegexpWithUnderscore.MatchString(value) {
|
||||
errs = append(errs, RegexError(dns1123SubdomainErrorMsgFG, dns1123SubdomainFmtWithUnderscore, "example.com"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
const dns1035LabelFmt string = "[a-z]([-a-z0-9]*[a-z0-9])?"
|
||||
const dns1035LabelErrMsg string = "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character"
|
||||
|
||||
// DNS1035LabelMaxLength is a label's max length in DNS (RFC 1035)
|
||||
const DNS1035LabelMaxLength int = 63
|
||||
|
||||
var dns1035LabelRegexp = regexp.MustCompile("^" + dns1035LabelFmt + "$")
|
||||
|
||||
// IsDNS1035Label tests for a string that conforms to the definition of a label in
|
||||
// DNS (RFC 1035).
|
||||
func IsDNS1035Label(value string) []string {
|
||||
var errs []string
|
||||
if len(value) > DNS1035LabelMaxLength {
|
||||
errs = append(errs, MaxLenError(DNS1035LabelMaxLength))
|
||||
}
|
||||
if !dns1035LabelRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(dns1035LabelErrMsg, dns1035LabelFmt, "my-name", "abc-123"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// wildcard definition - RFC 1034 section 4.3.3.
|
||||
// examples:
|
||||
// - valid: *.bar.com, *.foo.bar.com
|
||||
// - invalid: *.*.bar.com, *.foo.*.com, *bar.com, f*.bar.com, *
|
||||
const wildcardDNS1123SubdomainFmt = "\\*\\." + dns1123SubdomainFmt
|
||||
const wildcardDNS1123SubdomainErrMsg = "a wildcard DNS-1123 subdomain must start with '*.', followed by a valid DNS subdomain, which must consist of lower case alphanumeric characters, '-' or '.' and end with an alphanumeric character"
|
||||
|
||||
// IsWildcardDNS1123Subdomain tests for a string that conforms to the definition of a
|
||||
// wildcard subdomain in DNS (RFC 1034 section 4.3.3).
|
||||
func IsWildcardDNS1123Subdomain(value string) []string {
|
||||
wildcardDNS1123SubdomainRegexp := regexp.MustCompile("^" + wildcardDNS1123SubdomainFmt + "$")
|
||||
|
||||
var errs []string
|
||||
if len(value) > DNS1123SubdomainMaxLength {
|
||||
errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
|
||||
}
|
||||
if !wildcardDNS1123SubdomainRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(wildcardDNS1123SubdomainErrMsg, wildcardDNS1123SubdomainFmt, "*.example.com"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// IsCIdentifier tests for a string that conforms the definition of an identifier
|
||||
// in C. This checks the format, but not the length.
|
||||
// Deprecated: Use k8s.io/apimachinery/pkg/api/validate/content.IsCIdentifier instead.
|
||||
var IsCIdentifier = content.IsCIdentifier
|
||||
|
||||
// IsValidPortNum tests that the argument is a valid, non-zero port number.
|
||||
func IsValidPortNum(port int) []string {
|
||||
if 1 <= port && port <= 65535 {
|
||||
return nil
|
||||
}
|
||||
return []string{InclusiveRangeError(1, 65535)}
|
||||
}
|
||||
|
||||
// IsInRange tests that the argument is in an inclusive range.
|
||||
func IsInRange(value int, min int, max int) []string {
|
||||
if value >= min && value <= max {
|
||||
return nil
|
||||
}
|
||||
return []string{InclusiveRangeError(min, max)}
|
||||
}
|
||||
|
||||
// Now in libcontainer UID/GID limits is 0 ~ 1<<31 - 1
|
||||
// TODO: once we have a type for UID/GID we should make these that type.
|
||||
const (
|
||||
minUserID = 0
|
||||
maxUserID = math.MaxInt32
|
||||
minGroupID = 0
|
||||
maxGroupID = math.MaxInt32
|
||||
)
|
||||
|
||||
// IsValidGroupID tests that the argument is a valid Unix GID.
|
||||
func IsValidGroupID(gid int64) []string {
|
||||
if minGroupID <= gid && gid <= maxGroupID {
|
||||
return nil
|
||||
}
|
||||
return []string{InclusiveRangeError(minGroupID, maxGroupID)}
|
||||
}
|
||||
|
||||
// IsValidUserID tests that the argument is a valid Unix UID.
|
||||
func IsValidUserID(uid int64) []string {
|
||||
if minUserID <= uid && uid <= maxUserID {
|
||||
return nil
|
||||
}
|
||||
return []string{InclusiveRangeError(minUserID, maxUserID)}
|
||||
}
|
||||
|
||||
var portNameCharsetRegex = regexp.MustCompile("^[-a-z0-9]+$")
|
||||
var portNameOneLetterRegexp = regexp.MustCompile("[a-z]")
|
||||
|
||||
// IsValidPortName check that the argument is valid syntax. It must be
|
||||
// non-empty and no more than 15 characters long. It may contain only [-a-z0-9]
|
||||
// and must contain at least one letter [a-z]. It must not start or end with a
|
||||
// hyphen, nor contain adjacent hyphens.
|
||||
//
|
||||
// Note: We only allow lower-case characters, even though RFC 6335 is case
|
||||
// insensitive.
|
||||
func IsValidPortName(port string) []string {
|
||||
var errs []string
|
||||
if len(port) > 15 {
|
||||
errs = append(errs, MaxLenError(15))
|
||||
}
|
||||
if !portNameCharsetRegex.MatchString(port) {
|
||||
errs = append(errs, "must contain only alpha-numeric characters (a-z, 0-9), and hyphens (-)")
|
||||
}
|
||||
if !portNameOneLetterRegexp.MatchString(port) {
|
||||
errs = append(errs, "must contain at least one letter (a-z)")
|
||||
}
|
||||
if strings.Contains(port, "--") {
|
||||
errs = append(errs, "must not contain consecutive hyphens")
|
||||
}
|
||||
if len(port) > 0 && (port[0] == '-' || port[len(port)-1] == '-') {
|
||||
errs = append(errs, "must not begin or end with a hyphen")
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
const percentFmt string = "[0-9]+%"
|
||||
const percentErrMsg string = "a valid percent string must be a numeric string followed by an ending '%'"
|
||||
|
||||
var percentRegexp = regexp.MustCompile("^" + percentFmt + "$")
|
||||
|
||||
// IsValidPercent checks that string is in the form of a percentage
|
||||
func IsValidPercent(percent string) []string {
|
||||
if !percentRegexp.MatchString(percent) {
|
||||
return []string{RegexError(percentErrMsg, percentFmt, "1%", "93%")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const httpHeaderNameFmt string = "[-A-Za-z0-9]+"
|
||||
const httpHeaderNameErrMsg string = "a valid HTTP header must consist of alphanumeric characters or '-'"
|
||||
|
||||
var httpHeaderNameRegexp = regexp.MustCompile("^" + httpHeaderNameFmt + "$")
|
||||
|
||||
// IsHTTPHeaderName checks that a string conforms to the Go HTTP library's
|
||||
// definition of a valid header field name (a stricter subset than RFC7230).
|
||||
func IsHTTPHeaderName(value string) []string {
|
||||
if !httpHeaderNameRegexp.MatchString(value) {
|
||||
return []string{RegexError(httpHeaderNameErrMsg, httpHeaderNameFmt, "X-Header-Name")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const envVarNameFmt = "[-._a-zA-Z][-._a-zA-Z0-9]*"
|
||||
const envVarNameFmtErrMsg string = "a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit"
|
||||
|
||||
// TODO(hirazawaui): Rename this when the RelaxedEnvironmentVariableValidation gate is removed.
|
||||
const relaxedEnvVarNameFmtErrMsg string = "a valid environment variable name must consist only of printable ASCII characters other than '='"
|
||||
|
||||
var envVarNameRegexp = regexp.MustCompile("^" + envVarNameFmt + "$")
|
||||
|
||||
// IsEnvVarName tests if a string is a valid environment variable name.
|
||||
func IsEnvVarName(value string) []string {
|
||||
var errs []string
|
||||
if !envVarNameRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(envVarNameFmtErrMsg, envVarNameFmt, "my.env-name", "MY_ENV.NAME", "MyEnvName1"))
|
||||
}
|
||||
|
||||
errs = append(errs, hasChDirPrefix(value)...)
|
||||
return errs
|
||||
}
|
||||
|
||||
// IsRelaxedEnvVarName tests if a string is a valid environment variable name.
|
||||
func IsRelaxedEnvVarName(value string) []string {
|
||||
var errs []string
|
||||
|
||||
if len(value) == 0 {
|
||||
errs = append(errs, "environment variable name "+EmptyError())
|
||||
}
|
||||
|
||||
for _, r := range value {
|
||||
if r > unicode.MaxASCII || !unicode.IsPrint(r) || r == '=' {
|
||||
errs = append(errs, relaxedEnvVarNameFmtErrMsg)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
const configMapKeyFmt = `[-._a-zA-Z0-9]+`
|
||||
const configMapKeyErrMsg string = "a valid config key must consist of alphanumeric characters, '-', '_' or '.'"
|
||||
|
||||
var configMapKeyRegexp = regexp.MustCompile("^" + configMapKeyFmt + "$")
|
||||
|
||||
// IsConfigMapKey tests for a string that is a valid key for a ConfigMap or Secret
|
||||
func IsConfigMapKey(value string) []string {
|
||||
var errs []string
|
||||
if len(value) > DNS1123SubdomainMaxLength {
|
||||
errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
|
||||
}
|
||||
if !configMapKeyRegexp.MatchString(value) {
|
||||
errs = append(errs, RegexError(configMapKeyErrMsg, configMapKeyFmt, "key.name", "KEY_NAME", "key-name"))
|
||||
}
|
||||
errs = append(errs, hasChDirPrefix(value)...)
|
||||
return errs
|
||||
}
|
||||
|
||||
// MaxLenError returns a string explanation of a "string too long" validation
|
||||
// failure.
|
||||
func MaxLenError(length int) string {
|
||||
return fmt.Sprintf("must be no more than %d characters", length)
|
||||
}
|
||||
|
||||
// RegexError returns a string explanation of a regex validation failure.
|
||||
func RegexError(msg string, fmt string, examples ...string) string {
|
||||
if len(examples) == 0 {
|
||||
return msg + " (regex used for validation is '" + fmt + "')"
|
||||
}
|
||||
msg += " (e.g. "
|
||||
for i := range examples {
|
||||
if i > 0 {
|
||||
msg += " or "
|
||||
}
|
||||
msg += "'" + examples[i] + "', "
|
||||
}
|
||||
msg += "regex used for validation is '" + fmt + "')"
|
||||
return msg
|
||||
}
|
||||
|
||||
// EmptyError returns a string explanation of a "must not be empty" validation
|
||||
// failure.
|
||||
func EmptyError() string {
|
||||
return "must be non-empty"
|
||||
}
|
||||
|
||||
// InclusiveRangeError returns a string explanation of a numeric "must be
|
||||
// between" validation failure.
|
||||
func InclusiveRangeError(lo, hi int) string {
|
||||
return fmt.Sprintf(`must be between %d and %d, inclusive`, lo, hi)
|
||||
}
|
||||
|
||||
func hasChDirPrefix(value string) []string {
|
||||
var errs []string
|
||||
switch {
|
||||
case value == ".":
|
||||
errs = append(errs, `must not be '.'`)
|
||||
case value == "..":
|
||||
errs = append(errs, `must not be '..'`)
|
||||
case strings.HasPrefix(value, ".."):
|
||||
errs = append(errs, `must not start with '..'`)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
Copyright 2016 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 version provides utilities for version number comparisons
|
||||
package version
|
||||
+484
@@ -0,0 +1,484 @@
|
||||
/*
|
||||
Copyright 2016 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 version
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
apimachineryversion "k8s.io/apimachinery/pkg/version"
|
||||
)
|
||||
|
||||
// Version is an opaque representation of a version number
|
||||
type Version struct {
|
||||
components []uint
|
||||
semver bool
|
||||
preRelease string
|
||||
buildMetadata string
|
||||
}
|
||||
|
||||
var (
|
||||
// versionMatchRE splits a version string into numeric and "extra" parts
|
||||
versionMatchRE = regexp.MustCompile(`^\s*v?([0-9]+(?:\.[0-9]+)*)(.*)*$`)
|
||||
// extraMatchRE splits the "extra" part of versionMatchRE into semver pre-release and build metadata; it does not validate the "no leading zeroes" constraint for pre-release
|
||||
extraMatchRE = regexp.MustCompile(`^(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?\s*$`)
|
||||
)
|
||||
|
||||
func parse(str string, semver bool) (*Version, error) {
|
||||
parts := versionMatchRE.FindStringSubmatch(str)
|
||||
if parts == nil {
|
||||
return nil, fmt.Errorf("could not parse %q as version", str)
|
||||
}
|
||||
numbers, extra := parts[1], parts[2]
|
||||
|
||||
components := strings.Split(numbers, ".")
|
||||
if (semver && len(components) != 3) || (!semver && len(components) < 2) {
|
||||
return nil, fmt.Errorf("illegal version string %q", str)
|
||||
}
|
||||
|
||||
v := &Version{
|
||||
components: make([]uint, len(components)),
|
||||
semver: semver,
|
||||
}
|
||||
for i, comp := range components {
|
||||
if (i == 0 || semver) && strings.HasPrefix(comp, "0") && comp != "0" {
|
||||
return nil, fmt.Errorf("illegal zero-prefixed version component %q in %q", comp, str)
|
||||
}
|
||||
num, err := strconv.ParseUint(comp, 10, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("illegal non-numeric version component %q in %q: %v", comp, str, err)
|
||||
}
|
||||
v.components[i] = uint(num)
|
||||
}
|
||||
|
||||
if semver && extra != "" {
|
||||
extraParts := extraMatchRE.FindStringSubmatch(extra)
|
||||
if extraParts == nil {
|
||||
return nil, fmt.Errorf("could not parse pre-release/metadata (%s) in version %q", extra, str)
|
||||
}
|
||||
v.preRelease, v.buildMetadata = extraParts[1], extraParts[2]
|
||||
|
||||
for _, comp := range strings.Split(v.preRelease, ".") {
|
||||
if _, err := strconv.ParseUint(comp, 10, 0); err == nil {
|
||||
if strings.HasPrefix(comp, "0") && comp != "0" {
|
||||
return nil, fmt.Errorf("illegal zero-prefixed version component %q in %q", comp, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// HighestSupportedVersion returns the highest supported version
|
||||
// This function assumes that the highest supported version must be v1.x.
|
||||
func HighestSupportedVersion(versions []string) (*Version, error) {
|
||||
if len(versions) == 0 {
|
||||
return nil, errors.New("empty array for supported versions")
|
||||
}
|
||||
|
||||
var (
|
||||
highestSupportedVersion *Version
|
||||
theErr error
|
||||
)
|
||||
|
||||
for i := len(versions) - 1; i >= 0; i-- {
|
||||
currentHighestVer, err := ParseGeneric(versions[i])
|
||||
if err != nil {
|
||||
theErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
if currentHighestVer.Major() > 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if highestSupportedVersion == nil || highestSupportedVersion.LessThan(currentHighestVer) {
|
||||
highestSupportedVersion = currentHighestVer
|
||||
}
|
||||
}
|
||||
|
||||
if highestSupportedVersion == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"could not find a highest supported version from versions (%v) reported: %+v",
|
||||
versions, theErr)
|
||||
}
|
||||
|
||||
if highestSupportedVersion.Major() != 1 {
|
||||
return nil, fmt.Errorf("highest supported version reported is %v, must be v1.x", highestSupportedVersion)
|
||||
}
|
||||
|
||||
return highestSupportedVersion, nil
|
||||
}
|
||||
|
||||
// ParseGeneric parses a "generic" version string. The version string must consist of two
|
||||
// or more dot-separated numeric fields (the first of which can't have leading zeroes),
|
||||
// followed by arbitrary uninterpreted data (which need not be separated from the final
|
||||
// numeric field by punctuation). For convenience, leading and trailing whitespace is
|
||||
// ignored, and the version can be preceded by the letter "v". See also ParseSemantic.
|
||||
func ParseGeneric(str string) (*Version, error) {
|
||||
return parse(str, false)
|
||||
}
|
||||
|
||||
// MustParseGeneric is like ParseGeneric except that it panics on error
|
||||
func MustParseGeneric(str string) *Version {
|
||||
v, err := ParseGeneric(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Parse tries to do ParseSemantic first to keep more information.
|
||||
// If ParseSemantic fails, it would just do ParseGeneric.
|
||||
func Parse(str string) (*Version, error) {
|
||||
v, err := parse(str, true)
|
||||
if err != nil {
|
||||
return parse(str, false)
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
// MustParse is like Parse except that it panics on error
|
||||
func MustParse(str string) *Version {
|
||||
v, err := Parse(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ParseMajorMinor parses a "generic" version string and returns a version with the major and minor version.
|
||||
func ParseMajorMinor(str string) (*Version, error) {
|
||||
v, err := ParseGeneric(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return MajorMinor(v.Major(), v.Minor()), nil
|
||||
}
|
||||
|
||||
// MustParseMajorMinor is like ParseMajorMinor except that it panics on error
|
||||
func MustParseMajorMinor(str string) *Version {
|
||||
v, err := ParseMajorMinor(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ParseSemantic parses a version string that exactly obeys the syntax and semantics of
|
||||
// the "Semantic Versioning" specification (http://semver.org/) (although it ignores
|
||||
// leading and trailing whitespace, and allows the version to be preceded by "v"). For
|
||||
// version strings that are not guaranteed to obey the Semantic Versioning syntax, use
|
||||
// ParseGeneric.
|
||||
func ParseSemantic(str string) (*Version, error) {
|
||||
return parse(str, true)
|
||||
}
|
||||
|
||||
// MustParseSemantic is like ParseSemantic except that it panics on error
|
||||
func MustParseSemantic(str string) *Version {
|
||||
v, err := ParseSemantic(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// MajorMinor returns a version with the provided major and minor version.
|
||||
func MajorMinor(major, minor uint) *Version {
|
||||
return &Version{components: []uint{major, minor}}
|
||||
}
|
||||
|
||||
// Major returns the major release number
|
||||
func (v *Version) Major() uint {
|
||||
return v.components[0]
|
||||
}
|
||||
|
||||
// Minor returns the minor release number
|
||||
func (v *Version) Minor() uint {
|
||||
return v.components[1]
|
||||
}
|
||||
|
||||
// Patch returns the patch release number if v is a Semantic Version, or 0
|
||||
func (v *Version) Patch() uint {
|
||||
if len(v.components) < 3 {
|
||||
return 0
|
||||
}
|
||||
return v.components[2]
|
||||
}
|
||||
|
||||
// BuildMetadata returns the build metadata, if v is a Semantic Version, or ""
|
||||
func (v *Version) BuildMetadata() string {
|
||||
return v.buildMetadata
|
||||
}
|
||||
|
||||
// PreRelease returns the prerelease metadata, if v is a Semantic Version, or ""
|
||||
func (v *Version) PreRelease() string {
|
||||
return v.preRelease
|
||||
}
|
||||
|
||||
// Components returns the version number components
|
||||
func (v *Version) Components() []uint {
|
||||
return v.components
|
||||
}
|
||||
|
||||
// WithMajor returns copy of the version object with requested major number
|
||||
func (v *Version) WithMajor(major uint) *Version {
|
||||
result := *v
|
||||
result.components = []uint{major, v.Minor(), v.Patch()}
|
||||
return &result
|
||||
}
|
||||
|
||||
// WithMinor returns copy of the version object with requested minor number
|
||||
func (v *Version) WithMinor(minor uint) *Version {
|
||||
result := *v
|
||||
result.components = []uint{v.Major(), minor, v.Patch()}
|
||||
return &result
|
||||
}
|
||||
|
||||
// SubtractMinor returns the version with offset from the original minor, with the same major and no patch.
|
||||
// If -offset >= current minor, the minor would be 0.
|
||||
func (v *Version) OffsetMinor(offset int) *Version {
|
||||
var minor uint
|
||||
if offset >= 0 {
|
||||
minor = v.Minor() + uint(offset)
|
||||
} else {
|
||||
diff := uint(-offset)
|
||||
if diff < v.Minor() {
|
||||
minor = v.Minor() - diff
|
||||
}
|
||||
}
|
||||
return MajorMinor(v.Major(), minor)
|
||||
}
|
||||
|
||||
// SubtractMinor returns the version diff minor versions back, with the same major and no patch.
|
||||
// If diff >= current minor, the minor would be 0.
|
||||
func (v *Version) SubtractMinor(diff uint) *Version {
|
||||
return v.OffsetMinor(-int(diff))
|
||||
}
|
||||
|
||||
// AddMinor returns the version diff minor versions forward, with the same major and no patch.
|
||||
func (v *Version) AddMinor(diff uint) *Version {
|
||||
return v.OffsetMinor(int(diff))
|
||||
}
|
||||
|
||||
// WithPatch returns copy of the version object with requested patch number
|
||||
func (v *Version) WithPatch(patch uint) *Version {
|
||||
result := *v
|
||||
result.components = []uint{v.Major(), v.Minor(), patch}
|
||||
return &result
|
||||
}
|
||||
|
||||
// WithPreRelease returns copy of the version object with requested prerelease
|
||||
func (v *Version) WithPreRelease(preRelease string) *Version {
|
||||
if len(preRelease) == 0 {
|
||||
return v
|
||||
}
|
||||
result := *v
|
||||
result.components = []uint{v.Major(), v.Minor(), v.Patch()}
|
||||
result.preRelease = preRelease
|
||||
return &result
|
||||
}
|
||||
|
||||
// WithBuildMetadata returns copy of the version object with requested buildMetadata
|
||||
func (v *Version) WithBuildMetadata(buildMetadata string) *Version {
|
||||
result := *v
|
||||
result.components = []uint{v.Major(), v.Minor(), v.Patch()}
|
||||
result.buildMetadata = buildMetadata
|
||||
return &result
|
||||
}
|
||||
|
||||
// String converts a Version back to a string; note that for versions parsed with
|
||||
// ParseGeneric, this will not include the trailing uninterpreted portion of the version
|
||||
// number.
|
||||
func (v *Version) String() string {
|
||||
if v == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
|
||||
for i, comp := range v.components {
|
||||
if i > 0 {
|
||||
buffer.WriteString(".")
|
||||
}
|
||||
buffer.WriteString(fmt.Sprintf("%d", comp))
|
||||
}
|
||||
if v.preRelease != "" {
|
||||
buffer.WriteString("-")
|
||||
buffer.WriteString(v.preRelease)
|
||||
}
|
||||
if v.buildMetadata != "" {
|
||||
buffer.WriteString("+")
|
||||
buffer.WriteString(v.buildMetadata)
|
||||
}
|
||||
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// compareInternal returns -1 if v is less than other, 1 if it is greater than other, or 0
|
||||
// if they are equal
|
||||
func (v *Version) compareInternal(other *Version) int {
|
||||
|
||||
vLen := len(v.components)
|
||||
oLen := len(other.components)
|
||||
for i := 0; i < vLen && i < oLen; i++ {
|
||||
switch {
|
||||
case other.components[i] < v.components[i]:
|
||||
return 1
|
||||
case other.components[i] > v.components[i]:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
// If components are common but one has more items and they are not zeros, it is bigger
|
||||
switch {
|
||||
case oLen < vLen && !onlyZeros(v.components[oLen:]):
|
||||
return 1
|
||||
case oLen > vLen && !onlyZeros(other.components[vLen:]):
|
||||
return -1
|
||||
}
|
||||
|
||||
if !v.semver || !other.semver {
|
||||
return 0
|
||||
}
|
||||
|
||||
switch {
|
||||
case v.preRelease == "" && other.preRelease != "":
|
||||
return 1
|
||||
case v.preRelease != "" && other.preRelease == "":
|
||||
return -1
|
||||
case v.preRelease == other.preRelease: // includes case where both are ""
|
||||
return 0
|
||||
}
|
||||
|
||||
vPR := strings.Split(v.preRelease, ".")
|
||||
oPR := strings.Split(other.preRelease, ".")
|
||||
for i := 0; i < len(vPR) && i < len(oPR); i++ {
|
||||
vNum, err := strconv.ParseUint(vPR[i], 10, 0)
|
||||
if err == nil {
|
||||
oNum, err := strconv.ParseUint(oPR[i], 10, 0)
|
||||
if err == nil {
|
||||
switch {
|
||||
case oNum < vNum:
|
||||
return 1
|
||||
case oNum > vNum:
|
||||
return -1
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if oPR[i] < vPR[i] {
|
||||
return 1
|
||||
} else if oPR[i] > vPR[i] {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(oPR) < len(vPR):
|
||||
return 1
|
||||
case len(oPR) > len(vPR):
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// returns false if array contain any non-zero element
|
||||
func onlyZeros(array []uint) bool {
|
||||
for _, num := range array {
|
||||
if num != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// EqualTo tests if a version is equal to a given version.
|
||||
func (v *Version) EqualTo(other *Version) bool {
|
||||
if v == nil {
|
||||
return other == nil
|
||||
}
|
||||
if other == nil {
|
||||
return false
|
||||
}
|
||||
return v.compareInternal(other) == 0
|
||||
}
|
||||
|
||||
// AtLeast tests if a version is at least equal to a given minimum version. If both
|
||||
// Versions are Semantic Versions, this will use the Semantic Version comparison
|
||||
// algorithm. Otherwise, it will compare only the numeric components, with non-present
|
||||
// components being considered "0" (ie, "1.4" is equal to "1.4.0").
|
||||
func (v *Version) AtLeast(min *Version) bool {
|
||||
return v.compareInternal(min) != -1
|
||||
}
|
||||
|
||||
// LessThan tests if a version is less than a given version. (It is exactly the opposite
|
||||
// of AtLeast, for situations where asking "is v too old?" makes more sense than asking
|
||||
// "is v new enough?".)
|
||||
func (v *Version) LessThan(other *Version) bool {
|
||||
return v.compareInternal(other) == -1
|
||||
}
|
||||
|
||||
// GreaterThan tests if a version is greater than a given version.
|
||||
func (v *Version) GreaterThan(other *Version) bool {
|
||||
return v.compareInternal(other) == 1
|
||||
}
|
||||
|
||||
// Compare compares v against a version string (which will be parsed as either Semantic
|
||||
// or non-Semantic depending on v). On success it returns -1 if v is less than other, 1 if
|
||||
// it is greater than other, or 0 if they are equal.
|
||||
func (v *Version) Compare(other string) (int, error) {
|
||||
ov, err := parse(other, v.semver)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return v.compareInternal(ov), nil
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
return &apimachineryversion.Info{
|
||||
Major: Itoa(v.Major()),
|
||||
Minor: Itoa(v.Minor()),
|
||||
GitVersion: v.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func Itoa(i uint) string {
|
||||
if i == 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.Itoa(int(i))
|
||||
}
|
||||
+518
@@ -0,0 +1,518 @@
|
||||
/*
|
||||
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 wait
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/utils/clock"
|
||||
)
|
||||
|
||||
// Backoff holds parameters applied to a Backoff function.
|
||||
type Backoff struct {
|
||||
// The initial duration.
|
||||
Duration time.Duration
|
||||
// Duration is multiplied by factor each iteration, if factor is not zero
|
||||
// and the limits imposed by Steps and Cap have not been reached.
|
||||
// Should not be negative.
|
||||
// The jitter does not contribute to the updates to the duration parameter.
|
||||
Factor float64
|
||||
// The sleep at each iteration is the duration plus an additional
|
||||
// amount chosen uniformly at random from the interval between
|
||||
// zero and `jitter*duration`.
|
||||
Jitter float64
|
||||
// The remaining number of iterations in which the duration
|
||||
// parameter may change (but progress can be stopped earlier by
|
||||
// hitting the cap). If not positive, the duration is not
|
||||
// changed. Used for exponential backoff in combination with
|
||||
// Factor and Cap.
|
||||
Steps int
|
||||
// A limit on revised values of the duration parameter. If a
|
||||
// multiplication by the factor parameter would make the duration
|
||||
// exceed the cap then the duration is set to the cap and the
|
||||
// steps parameter is set to zero.
|
||||
Cap time.Duration
|
||||
}
|
||||
|
||||
// Step returns an amount of time to sleep determined by the original
|
||||
// Duration and Jitter. The backoff is mutated to update its Steps and
|
||||
// Duration. A nil Backoff always has a zero-duration step.
|
||||
func (b *Backoff) Step() time.Duration {
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
var nextDuration time.Duration
|
||||
nextDuration, b.Duration, b.Steps = delay(b.Steps, b.Duration, b.Cap, b.Factor, b.Jitter)
|
||||
return nextDuration
|
||||
}
|
||||
|
||||
// DelayFunc returns a function that will compute the next interval to
|
||||
// wait given the arguments in b. It does not mutate the original backoff
|
||||
// but the function is safe to use only from a single goroutine.
|
||||
func (b Backoff) DelayFunc() DelayFunc {
|
||||
steps := b.Steps
|
||||
duration := b.Duration
|
||||
cap := b.Cap
|
||||
factor := b.Factor
|
||||
jitter := b.Jitter
|
||||
|
||||
return func() time.Duration {
|
||||
var nextDuration time.Duration
|
||||
// jitter is applied per step and is not cumulative over multiple steps
|
||||
nextDuration, duration, steps = delay(steps, duration, cap, factor, jitter)
|
||||
return nextDuration
|
||||
}
|
||||
}
|
||||
|
||||
// Timer returns a timer implementation appropriate to this backoff's parameters
|
||||
// for use with wait functions.
|
||||
func (b Backoff) Timer() Timer {
|
||||
if b.Steps > 1 || b.Jitter != 0 {
|
||||
return &variableTimer{new: internalClock.NewTimer, fn: b.DelayFunc()}
|
||||
}
|
||||
if b.Duration > 0 {
|
||||
return &fixedTimer{new: internalClock.NewTicker, interval: b.Duration}
|
||||
}
|
||||
return newNoopTimer()
|
||||
}
|
||||
|
||||
// delay implements the core delay algorithm used in this package.
|
||||
func delay(steps int, duration, cap time.Duration, factor, jitter float64) (_ time.Duration, next time.Duration, nextSteps int) {
|
||||
// when steps is non-positive, do not alter the base duration
|
||||
if steps < 1 {
|
||||
if jitter > 0 {
|
||||
return Jitter(duration, jitter), duration, 0
|
||||
}
|
||||
return duration, duration, 0
|
||||
}
|
||||
steps--
|
||||
|
||||
// calculate the next step's interval
|
||||
if factor != 0 {
|
||||
next = time.Duration(float64(duration) * factor)
|
||||
if cap > 0 && next > cap {
|
||||
next = cap
|
||||
steps = 0
|
||||
}
|
||||
} else {
|
||||
next = duration
|
||||
}
|
||||
|
||||
// add jitter for this step
|
||||
if jitter > 0 {
|
||||
duration = Jitter(duration, jitter)
|
||||
}
|
||||
|
||||
return duration, next, steps
|
||||
|
||||
}
|
||||
|
||||
// DelayWithReset returns a DelayFunc that will return the appropriate next interval to
|
||||
// wait. Every resetInterval the backoff parameters are reset to their initial state.
|
||||
// This method is safe to invoke from multiple goroutines, but all calls will advance
|
||||
// the backoff state when Factor is set. If Factor is zero, this method is the same as
|
||||
// invoking b.DelayFunc() since Steps has no impact without Factor. If resetInterval is
|
||||
// zero no backoff will be performed as the same calling DelayFunc with a zero factor
|
||||
// and steps.
|
||||
func (b Backoff) DelayWithReset(c clock.Clock, resetInterval time.Duration) DelayFunc {
|
||||
if b.Factor <= 0 {
|
||||
return b.DelayFunc()
|
||||
}
|
||||
if resetInterval <= 0 {
|
||||
b.Steps = 0
|
||||
b.Factor = 0
|
||||
return b.DelayFunc()
|
||||
}
|
||||
return (&backoffManager{
|
||||
backoff: b,
|
||||
initialBackoff: b,
|
||||
resetInterval: resetInterval,
|
||||
|
||||
clock: c,
|
||||
lastStart: c.Now(),
|
||||
timer: nil,
|
||||
}).Step
|
||||
}
|
||||
|
||||
// Until loops until stop channel is closed, running f every period.
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
// UntilWithContext loops until context is done, running f every period.
|
||||
//
|
||||
// UntilWithContext is syntactic sugar on top of JitterUntilWithContext
|
||||
// with zero jitter factor and with sliding = true (which means the timer
|
||||
// for period starts after the f completes).
|
||||
func UntilWithContext(ctx context.Context, f func(context.Context), period time.Duration) {
|
||||
JitterUntilWithContext(ctx, f, period, 0.0, true)
|
||||
}
|
||||
|
||||
// NonSlidingUntil loops until stop channel is closed, running f every
|
||||
// period.
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
// NonSlidingUntilWithContext loops until context is done, running f every
|
||||
// period.
|
||||
//
|
||||
// NonSlidingUntilWithContext is syntactic sugar on top of JitterUntilWithContext
|
||||
// with zero jitter factor, with sliding = false (meaning the timer for period
|
||||
// starts at the same time as the function starts).
|
||||
func NonSlidingUntilWithContext(ctx context.Context, f func(context.Context), period time.Duration) {
|
||||
JitterUntilWithContext(ctx, f, period, 0.0, false)
|
||||
}
|
||||
|
||||
// JitterUntil loops until stop channel is closed, 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.
|
||||
//
|
||||
// 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 <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if !sliding {
|
||||
t = backoff.Backoff()
|
||||
}
|
||||
|
||||
func() {
|
||||
defer runtime.HandleCrashWithContext(ctx)
|
||||
f(ctx)
|
||||
}()
|
||||
|
||||
if sliding {
|
||||
t = backoff.Backoff()
|
||||
}
|
||||
|
||||
// NOTE: b/c there is no priority selection in golang
|
||||
// it is possible for this to race, meaning we could
|
||||
// trigger t.C and stopCh, and t.C select falls through.
|
||||
// In order to mitigate we re-check stopCh at the beginning
|
||||
// of every loop to prevent extra executions of f().
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !t.Stop() {
|
||||
<-t.C()
|
||||
}
|
||||
return
|
||||
case <-t.C():
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// backoffManager provides simple backoff behavior in a threadsafe manner to a caller.
|
||||
type backoffManager struct {
|
||||
backoff Backoff
|
||||
initialBackoff Backoff
|
||||
resetInterval time.Duration
|
||||
|
||||
clock clock.Clock
|
||||
|
||||
lock sync.Mutex
|
||||
lastStart time.Time
|
||||
timer clock.Timer
|
||||
}
|
||||
|
||||
// Step returns the expected next duration to wait.
|
||||
func (b *backoffManager) Step() time.Duration {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
switch {
|
||||
case b.resetInterval == 0:
|
||||
b.backoff = b.initialBackoff
|
||||
case b.clock.Now().Sub(b.lastStart) > b.resetInterval:
|
||||
b.backoff = b.initialBackoff
|
||||
b.lastStart = b.clock.Now()
|
||||
}
|
||||
return b.backoff.Step()
|
||||
}
|
||||
|
||||
// Backoff implements BackoffManager.Backoff, it returns a timer so caller can block on the timer
|
||||
// for exponential backoff. The returned timer must be drained before calling Backoff() the second
|
||||
// time.
|
||||
func (b *backoffManager) Backoff() clock.Timer {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
if b.timer == nil {
|
||||
b.timer = b.clock.NewTimer(b.Step())
|
||||
} else {
|
||||
b.timer.Reset(b.Step())
|
||||
}
|
||||
return b.timer
|
||||
}
|
||||
|
||||
// Timer returns a new Timer instance that shares the clock and the reset behavior with all other
|
||||
// timers.
|
||||
func (b *backoffManager) Timer() Timer {
|
||||
return DelayFunc(b.Step).Timer(b.clock)
|
||||
}
|
||||
|
||||
// BackoffManager manages backoff with a particular scheme based on its underlying implementation.
|
||||
type BackoffManager interface {
|
||||
// Backoff returns a shared clock.Timer that is Reset on every invocation. This method is not
|
||||
// safe for use from multiple threads. It returns a timer for backoff, and caller shall backoff
|
||||
// until Timer.C() drains. If the second Backoff() is called before the timer from the first
|
||||
// Backoff() call finishes, the first timer will NOT be drained and result in undetermined
|
||||
// behavior.
|
||||
Backoff() clock.Timer
|
||||
}
|
||||
|
||||
// Deprecated: Will be removed when the legacy polling functions are removed.
|
||||
type exponentialBackoffManagerImpl struct {
|
||||
backoff *Backoff
|
||||
backoffTimer clock.Timer
|
||||
lastBackoffStart time.Time
|
||||
initialBackoff time.Duration
|
||||
backoffResetDuration time.Duration
|
||||
clock clock.Clock
|
||||
}
|
||||
|
||||
// NewExponentialBackoffManager returns a manager for managing exponential backoff. Each backoff is jittered and
|
||||
// backoff will not exceed the given max. If the backoff is not called within resetDuration, the backoff is reset.
|
||||
// This backoff manager is used to reduce load during upstream unhealthiness.
|
||||
//
|
||||
// Deprecated: Will be removed when the legacy Poll methods are removed. Callers should construct a
|
||||
// Backoff struct, use DelayWithReset() to get a DelayFunc that periodically resets itself, and then
|
||||
// invoke Timer() when calling wait.BackoffUntil.
|
||||
//
|
||||
// Instead of:
|
||||
//
|
||||
// bm := wait.NewExponentialBackoffManager(init, max, reset, factor, jitter, clock)
|
||||
// ...
|
||||
// wait.BackoffUntil(..., bm.Backoff, ...)
|
||||
//
|
||||
// Use:
|
||||
//
|
||||
// delayFn := wait.Backoff{
|
||||
// Duration: init,
|
||||
// Cap: max,
|
||||
// Steps: int(math.Ceil(float64(max) / float64(init))), // now a required argument
|
||||
// Factor: factor,
|
||||
// Jitter: jitter,
|
||||
// }.DelayWithReset(reset, clock)
|
||||
// wait.BackoffUntil(..., delayFn.Timer(), ...)
|
||||
func NewExponentialBackoffManager(initBackoff, maxBackoff, resetDuration time.Duration, backoffFactor, jitter float64, c clock.Clock) BackoffManager {
|
||||
return &exponentialBackoffManagerImpl{
|
||||
backoff: &Backoff{
|
||||
Duration: initBackoff,
|
||||
Factor: backoffFactor,
|
||||
Jitter: jitter,
|
||||
|
||||
// the current impl of wait.Backoff returns Backoff.Duration once steps are used up, which is not
|
||||
// what we ideally need here, we set it to max int and assume we will never use up the steps
|
||||
Steps: math.MaxInt32,
|
||||
Cap: maxBackoff,
|
||||
},
|
||||
backoffTimer: nil,
|
||||
initialBackoff: initBackoff,
|
||||
lastBackoffStart: c.Now(),
|
||||
backoffResetDuration: resetDuration,
|
||||
clock: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *exponentialBackoffManagerImpl) getNextBackoff() time.Duration {
|
||||
if b.clock.Now().Sub(b.lastBackoffStart) > b.backoffResetDuration {
|
||||
b.backoff.Steps = math.MaxInt32
|
||||
b.backoff.Duration = b.initialBackoff
|
||||
}
|
||||
b.lastBackoffStart = b.clock.Now()
|
||||
return b.backoff.Step()
|
||||
}
|
||||
|
||||
// Backoff implements BackoffManager.Backoff, it returns a timer so caller can block on the timer for exponential backoff.
|
||||
// The returned timer must be drained before calling Backoff() the second time
|
||||
func (b *exponentialBackoffManagerImpl) Backoff() clock.Timer {
|
||||
if b.backoffTimer == nil {
|
||||
b.backoffTimer = b.clock.NewTimer(b.getNextBackoff())
|
||||
} else {
|
||||
b.backoffTimer.Reset(b.getNextBackoff())
|
||||
}
|
||||
return b.backoffTimer
|
||||
}
|
||||
|
||||
// Deprecated: Will be removed when the legacy polling functions are removed.
|
||||
type jitteredBackoffManagerImpl struct {
|
||||
clock clock.Clock
|
||||
duration time.Duration
|
||||
jitter float64
|
||||
backoffTimer clock.Timer
|
||||
}
|
||||
|
||||
// NewJitteredBackoffManager returns a BackoffManager that backoffs with given duration plus given jitter. If the jitter
|
||||
// is negative, backoff will not be jittered.
|
||||
//
|
||||
// Deprecated: Will be removed when the legacy Poll methods are removed. Callers should construct a
|
||||
// Backoff struct and invoke Timer() when calling wait.BackoffUntil.
|
||||
//
|
||||
// Instead of:
|
||||
//
|
||||
// bm := wait.NewJitteredBackoffManager(duration, jitter, clock)
|
||||
// ...
|
||||
// wait.BackoffUntil(..., bm.Backoff, ...)
|
||||
//
|
||||
// Use:
|
||||
//
|
||||
// wait.BackoffUntil(..., wait.Backoff{Duration: duration, Jitter: jitter}.Timer(), ...)
|
||||
func NewJitteredBackoffManager(duration time.Duration, jitter float64, c clock.Clock) BackoffManager {
|
||||
return &jitteredBackoffManagerImpl{
|
||||
clock: c,
|
||||
duration: duration,
|
||||
jitter: jitter,
|
||||
backoffTimer: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *jitteredBackoffManagerImpl) getNextBackoff() time.Duration {
|
||||
jitteredPeriod := j.duration
|
||||
if j.jitter > 0.0 {
|
||||
jitteredPeriod = Jitter(j.duration, j.jitter)
|
||||
}
|
||||
return jitteredPeriod
|
||||
}
|
||||
|
||||
// Backoff implements BackoffManager.Backoff, it returns a timer so caller can block on the timer for jittered backoff.
|
||||
// The returned timer must be drained before calling Backoff() the second time
|
||||
func (j *jitteredBackoffManagerImpl) Backoff() clock.Timer {
|
||||
backoff := j.getNextBackoff()
|
||||
if j.backoffTimer == nil {
|
||||
j.backoffTimer = j.clock.NewTimer(backoff)
|
||||
} else {
|
||||
j.backoffTimer.Reset(backoff)
|
||||
}
|
||||
return j.backoffTimer
|
||||
}
|
||||
|
||||
// ExponentialBackoff repeats a condition check with exponential backoff.
|
||||
//
|
||||
// It repeatedly checks the condition and then sleeps, using `backoff.Step()`
|
||||
// to determine the length of the sleep and adjust Duration and Steps.
|
||||
// Stops and returns as soon as:
|
||||
// 1. the condition check returns true or an error,
|
||||
// 2. `backoff.Steps` checks of the condition have been done, or
|
||||
// 3. a sleep truncated by the cap on duration has been completed.
|
||||
// In case (1) the returned error is what the condition function returned.
|
||||
// In all other cases, ErrWaitTimeout is returned.
|
||||
//
|
||||
// Since backoffs are often subject to cancellation, we recommend using
|
||||
// ExponentialBackoffWithContext and passing a context to the method.
|
||||
func ExponentialBackoff(backoff Backoff, condition ConditionFunc) error {
|
||||
for backoff.Steps > 0 {
|
||||
if ok, err := runConditionWithCrashProtection(condition); err != nil || ok {
|
||||
return err
|
||||
}
|
||||
if backoff.Steps == 1 {
|
||||
break
|
||||
}
|
||||
time.Sleep(backoff.Step())
|
||||
}
|
||||
return ErrWaitTimeout
|
||||
}
|
||||
|
||||
// ExponentialBackoffWithContext repeats a condition check with exponential backoff.
|
||||
// It immediately returns an error if the condition returns an error, the context is cancelled
|
||||
// or hits the deadline, or if the maximum attempts defined in backoff is exceeded (ErrWaitTimeout).
|
||||
// If an error is returned by the condition the backoff stops immediately. The condition will
|
||||
// never be invoked more than backoff.Steps times.
|
||||
func ExponentialBackoffWithContext(ctx context.Context, backoff Backoff, condition ConditionWithContextFunc) error {
|
||||
for backoff.Steps > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
if ok, err := runConditionWithCrashProtectionWithContext(ctx, condition); err != nil || ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if backoff.Steps == 1 {
|
||||
break
|
||||
}
|
||||
|
||||
waitBeforeRetry := backoff.Step()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(waitBeforeRetry):
|
||||
}
|
||||
}
|
||||
|
||||
return ErrWaitTimeout
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
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 wait
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/utils/clock"
|
||||
)
|
||||
|
||||
// DelayFunc returns the next time interval to wait.
|
||||
type DelayFunc func() time.Duration
|
||||
|
||||
// Timer takes an arbitrary delay function and returns a timer that can handle arbitrary interval changes.
|
||||
// Use Backoff{...}.Timer() for simple delays and more efficient timers.
|
||||
func (fn DelayFunc) Timer(c clock.Clock) Timer {
|
||||
return &variableTimer{fn: fn, new: c.NewTimer}
|
||||
}
|
||||
|
||||
// Until takes an arbitrary delay function and runs until cancelled or the condition indicates exit. This
|
||||
// offers all of the functionality of the methods in this package.
|
||||
func (fn DelayFunc) Until(ctx context.Context, immediate, sliding bool, condition ConditionWithContextFunc) error {
|
||||
return loopConditionUntilContext(ctx, &variableTimer{fn: fn, new: internalClock.NewTimer}, immediate, sliding, condition)
|
||||
}
|
||||
|
||||
// Concurrent returns a version of this DelayFunc that is safe for use by multiple goroutines that
|
||||
// wish to share a single delay timer.
|
||||
func (fn DelayFunc) Concurrent() DelayFunc {
|
||||
var lock sync.Mutex
|
||||
return func() time.Duration {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
return fn()
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2014 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 wait provides tools for polling or listening for changes
|
||||
// to a condition.
|
||||
package wait
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
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 wait
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// ErrWaitTimeout is returned when the condition was not satisfied in time.
|
||||
//
|
||||
// Deprecated: This type will be made private in favor of Interrupted()
|
||||
// for checking errors or ErrorInterrupted(err) for returning a wrapped error.
|
||||
var ErrWaitTimeout = ErrorInterrupted(errors.New("timed out waiting for the condition"))
|
||||
|
||||
// Interrupted returns true if the error indicates a Poll, ExponentialBackoff, or
|
||||
// Until loop exited for any reason besides the condition returning true or an
|
||||
// error. A loop is considered interrupted if the calling context is cancelled,
|
||||
// the context reaches its deadline, or a backoff reaches its maximum allowed
|
||||
// steps.
|
||||
//
|
||||
// Callers should use this method instead of comparing the error value directly to
|
||||
// ErrWaitTimeout, as methods that cancel a context may not return that error.
|
||||
//
|
||||
// Instead of:
|
||||
//
|
||||
// err := wait.Poll(...)
|
||||
// if err == wait.ErrWaitTimeout {
|
||||
// log.Infof("Wait for operation exceeded")
|
||||
// } else ...
|
||||
//
|
||||
// Use:
|
||||
//
|
||||
// err := wait.Poll(...)
|
||||
// if wait.Interrupted(err) {
|
||||
// log.Infof("Wait for operation exceeded")
|
||||
// } else ...
|
||||
func Interrupted(err error) bool {
|
||||
switch {
|
||||
case errors.Is(err, errWaitTimeout),
|
||||
errors.Is(err, context.Canceled),
|
||||
errors.Is(err, context.DeadlineExceeded):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// errInterrupted
|
||||
type errInterrupted struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
// ErrorInterrupted returns an error that indicates the wait was ended
|
||||
// early for a given reason. If no cause is provided a generic error
|
||||
// will be used but callers are encouraged to provide a real cause for
|
||||
// clarity in debugging.
|
||||
func ErrorInterrupted(cause error) error {
|
||||
switch cause.(type) {
|
||||
case errInterrupted:
|
||||
// no need to wrap twice since errInterrupted is only needed
|
||||
// once in a chain
|
||||
return cause
|
||||
default:
|
||||
return errInterrupted{cause}
|
||||
}
|
||||
}
|
||||
|
||||
// errWaitTimeout is the private version of the previous ErrWaitTimeout
|
||||
// and is private to prevent direct comparison. Use ErrorInterrupted(err)
|
||||
// to get an error that will return true for Interrupted(err).
|
||||
var errWaitTimeout = errInterrupted{}
|
||||
|
||||
func (e errInterrupted) Unwrap() error { return e.cause }
|
||||
func (e errInterrupted) Is(target error) bool { return target == errWaitTimeout }
|
||||
func (e errInterrupted) Error() string {
|
||||
if e.cause == nil {
|
||||
// returns the same error message as historical behavior
|
||||
return "timed out waiting for the condition"
|
||||
}
|
||||
return e.cause.Error()
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
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 wait
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
)
|
||||
|
||||
// loopConditionUntilContext executes the provided condition at intervals defined by
|
||||
// the provided timer until the provided context is cancelled, the condition returns
|
||||
// true, or the condition returns an error. If sliding is true, the period is computed
|
||||
// after condition runs. If it is false then period includes the runtime for condition.
|
||||
// If immediate is false the first delay happens before any call to condition, if
|
||||
// immediate is true the condition will be invoked before waiting and guarantees that
|
||||
// the condition is invoked at least once, regardless of whether the context has been
|
||||
// cancelled. The returned error is the error returned by the last condition or the
|
||||
// context error if the context was terminated.
|
||||
//
|
||||
// This is the common loop construct for all polling in the wait package.
|
||||
func loopConditionUntilContext(ctx context.Context, t Timer, immediate, sliding bool, condition ConditionWithContextFunc) error {
|
||||
defer t.Stop()
|
||||
|
||||
var timeCh <-chan time.Time
|
||||
doneCh := ctx.Done()
|
||||
|
||||
if !sliding {
|
||||
timeCh = t.C()
|
||||
}
|
||||
|
||||
// if immediate is true the condition is
|
||||
// guaranteed to be executed at least once,
|
||||
// if we haven't requested immediate execution, delay once
|
||||
if immediate {
|
||||
if ok, err := func() (bool, error) {
|
||||
defer runtime.HandleCrashWithContext(ctx)
|
||||
return condition(ctx)
|
||||
}(); err != nil || ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if sliding {
|
||||
timeCh = t.C()
|
||||
}
|
||||
|
||||
for {
|
||||
|
||||
// Wait for either the context to be cancelled or the next invocation be called
|
||||
select {
|
||||
case <-doneCh:
|
||||
return ctx.Err()
|
||||
case <-timeCh:
|
||||
}
|
||||
|
||||
// IMPORTANT: Because there is no channel priority selection in golang
|
||||
// it is possible for very short timers to "win" the race in the previous select
|
||||
// repeatedly even when the context has been canceled. We therefore must
|
||||
// explicitly check for context cancellation on every loop and exit if true to
|
||||
// guarantee that we don't invoke condition more than once after context has
|
||||
// been cancelled.
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !sliding {
|
||||
t.Next()
|
||||
}
|
||||
if ok, err := func() (bool, error) {
|
||||
defer runtime.HandleCrashWithContext(ctx)
|
||||
return condition(ctx)
|
||||
}(); err != nil || ok {
|
||||
return err
|
||||
}
|
||||
if sliding {
|
||||
t.Next()
|
||||
}
|
||||
}
|
||||
}
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
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 wait
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PollUntilContextCancel tries a condition func until it returns true, an error, or the context
|
||||
// is cancelled or hits a deadline. condition will be invoked after the first interval if the
|
||||
// context is not cancelled first. The returned error will be from ctx.Err(), the condition's
|
||||
// err return value, or nil. If invoking condition takes longer than interval the next condition
|
||||
// will be invoked immediately. When using very short intervals, condition may be invoked multiple
|
||||
// times before a context cancellation is detected. If immediate is true, condition will be
|
||||
// invoked before waiting and guarantees that condition is invoked at least once, regardless of
|
||||
// whether the context has been cancelled.
|
||||
func PollUntilContextCancel(ctx context.Context, interval time.Duration, immediate bool, condition ConditionWithContextFunc) error {
|
||||
return loopConditionUntilContext(ctx, Backoff{Duration: interval}.Timer(), immediate, false, condition)
|
||||
}
|
||||
|
||||
// PollUntilContextTimeout will terminate polling after timeout duration by setting a context
|
||||
// timeout. This is provided as a convenience function for callers not currently executing under
|
||||
// a deadline and is equivalent to:
|
||||
//
|
||||
// deadlineCtx, deadlineCancel := context.WithTimeout(ctx, timeout)
|
||||
// err := PollUntilContextCancel(deadlineCtx, interval, immediate, condition)
|
||||
//
|
||||
// The deadline context will be cancelled if the Poll succeeds before the timeout, simplifying
|
||||
// inline usage. All other behavior is identical to PollUntilContextCancel.
|
||||
func PollUntilContextTimeout(ctx context.Context, interval, timeout time.Duration, immediate bool, condition ConditionWithContextFunc) error {
|
||||
deadlineCtx, deadlineCancel := context.WithTimeout(ctx, timeout)
|
||||
defer deadlineCancel()
|
||||
return loopConditionUntilContext(deadlineCtx, Backoff{Duration: interval}.Timer(), immediate, false, condition)
|
||||
}
|
||||
|
||||
// Poll tries a condition func until it returns true, an error, or the timeout
|
||||
// is reached.
|
||||
//
|
||||
// Poll always waits the interval before the run of 'condition'.
|
||||
// 'condition' will always be invoked at least once.
|
||||
//
|
||||
// Some intervals may be missed if the condition takes too long or the time
|
||||
// window is too short.
|
||||
//
|
||||
// If you want to Poll something forever, see PollInfinite.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextTimeout.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func Poll(interval, timeout time.Duration, condition ConditionFunc) error {
|
||||
return PollWithContext(context.Background(), interval, timeout, condition.WithContext())
|
||||
}
|
||||
|
||||
// PollWithContext tries a condition func until it returns true, an error,
|
||||
// or when the context expires or the timeout is reached, whichever
|
||||
// happens first.
|
||||
//
|
||||
// PollWithContext always waits the interval before the run of 'condition'.
|
||||
// 'condition' will always be invoked at least once.
|
||||
//
|
||||
// Some intervals may be missed if the condition takes too long or the time
|
||||
// window is too short.
|
||||
//
|
||||
// If you want to Poll something forever, see PollInfinite.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextTimeout.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func PollWithContext(ctx context.Context, interval, timeout time.Duration, condition ConditionWithContextFunc) error {
|
||||
return poll(ctx, false, poller(interval, timeout), condition)
|
||||
}
|
||||
|
||||
// PollUntil tries a condition func until it returns true, an error or stopCh is
|
||||
// closed.
|
||||
//
|
||||
// PollUntil always waits interval before the first run of 'condition'.
|
||||
// 'condition' will always be invoked at least once.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextCancel.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func PollUntil(interval time.Duration, condition ConditionFunc, stopCh <-chan struct{}) error {
|
||||
return PollUntilWithContext(ContextForChannel(stopCh), interval, condition.WithContext())
|
||||
}
|
||||
|
||||
// PollUntilWithContext tries a condition func until it returns true,
|
||||
// an error or the specified context is cancelled or expired.
|
||||
//
|
||||
// PollUntilWithContext always waits interval before the first run of 'condition'.
|
||||
// 'condition' will always be invoked at least once.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextCancel.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func PollUntilWithContext(ctx context.Context, interval time.Duration, condition ConditionWithContextFunc) error {
|
||||
return poll(ctx, false, poller(interval, 0), condition)
|
||||
}
|
||||
|
||||
// PollInfinite tries a condition func until it returns true or an error
|
||||
//
|
||||
// PollInfinite always waits the interval before the run of 'condition'.
|
||||
//
|
||||
// Some intervals may be missed if the condition takes too long or the time
|
||||
// window is too short.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextCancel.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func PollInfinite(interval time.Duration, condition ConditionFunc) error {
|
||||
return PollInfiniteWithContext(context.Background(), interval, condition.WithContext())
|
||||
}
|
||||
|
||||
// PollInfiniteWithContext tries a condition func until it returns true or an error
|
||||
//
|
||||
// PollInfiniteWithContext always waits the interval before the run of 'condition'.
|
||||
//
|
||||
// Some intervals may be missed if the condition takes too long or the time
|
||||
// window is too short.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextCancel.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func PollInfiniteWithContext(ctx context.Context, interval time.Duration, condition ConditionWithContextFunc) error {
|
||||
return poll(ctx, false, poller(interval, 0), condition)
|
||||
}
|
||||
|
||||
// PollImmediate tries a condition func until it returns true, an error, or the timeout
|
||||
// is reached.
|
||||
//
|
||||
// PollImmediate always checks 'condition' before waiting for the interval. 'condition'
|
||||
// will always be invoked at least once.
|
||||
//
|
||||
// Some intervals may be missed if the condition takes too long or the time
|
||||
// window is too short.
|
||||
//
|
||||
// If you want to immediately Poll something forever, see PollImmediateInfinite.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextTimeout.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func PollImmediate(interval, timeout time.Duration, condition ConditionFunc) error {
|
||||
return PollImmediateWithContext(context.Background(), interval, timeout, condition.WithContext())
|
||||
}
|
||||
|
||||
// PollImmediateWithContext tries a condition func until it returns true, an error,
|
||||
// or the timeout is reached or the specified context expires, whichever happens first.
|
||||
//
|
||||
// PollImmediateWithContext always checks 'condition' before waiting for the interval.
|
||||
// 'condition' will always be invoked at least once.
|
||||
//
|
||||
// Some intervals may be missed if the condition takes too long or the time
|
||||
// window is too short.
|
||||
//
|
||||
// If you want to immediately Poll something forever, see PollImmediateInfinite.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextTimeout.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func PollImmediateWithContext(ctx context.Context, interval, timeout time.Duration, condition ConditionWithContextFunc) error {
|
||||
return poll(ctx, true, poller(interval, timeout), condition)
|
||||
}
|
||||
|
||||
// PollImmediateUntil tries a condition func until it returns true, an error or stopCh is closed.
|
||||
//
|
||||
// PollImmediateUntil runs the 'condition' before waiting for the interval.
|
||||
// 'condition' will always be invoked at least once.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextCancel.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func PollImmediateUntil(interval time.Duration, condition ConditionFunc, stopCh <-chan struct{}) error {
|
||||
return PollImmediateUntilWithContext(ContextForChannel(stopCh), interval, condition.WithContext())
|
||||
}
|
||||
|
||||
// PollImmediateUntilWithContext tries a condition func until it returns true,
|
||||
// an error or the specified context is cancelled or expired.
|
||||
//
|
||||
// PollImmediateUntilWithContext runs the 'condition' before waiting for the interval.
|
||||
// 'condition' will always be invoked at least once.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextCancel.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func PollImmediateUntilWithContext(ctx context.Context, interval time.Duration, condition ConditionWithContextFunc) error {
|
||||
return poll(ctx, true, poller(interval, 0), condition)
|
||||
}
|
||||
|
||||
// PollImmediateInfinite tries a condition func until it returns true or an error
|
||||
//
|
||||
// PollImmediateInfinite runs the 'condition' before waiting for the interval.
|
||||
//
|
||||
// Some intervals may be missed if the condition takes too long or the time
|
||||
// window is too short.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextCancel.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func PollImmediateInfinite(interval time.Duration, condition ConditionFunc) error {
|
||||
return PollImmediateInfiniteWithContext(context.Background(), interval, condition.WithContext())
|
||||
}
|
||||
|
||||
// PollImmediateInfiniteWithContext tries a condition func until it returns true
|
||||
// or an error or the specified context gets cancelled or expired.
|
||||
//
|
||||
// PollImmediateInfiniteWithContext runs the 'condition' before waiting for the interval.
|
||||
//
|
||||
// Some intervals may be missed if the condition takes too long or the time
|
||||
// window is too short.
|
||||
//
|
||||
// Deprecated: This method does not return errors from context, use PollUntilContextCancel.
|
||||
// Note that the new method will no longer return ErrWaitTimeout and instead return errors
|
||||
// defined by the context package. Will be removed in a future release.
|
||||
func PollImmediateInfiniteWithContext(ctx context.Context, interval time.Duration, condition ConditionWithContextFunc) error {
|
||||
return poll(ctx, true, poller(interval, 0), condition)
|
||||
}
|
||||
|
||||
// Internally used, each of the public 'Poll*' function defined in this
|
||||
// package should invoke this internal function with appropriate parameters.
|
||||
// ctx: the context specified by the caller, for infinite polling pass
|
||||
// a context that never gets cancelled or expired.
|
||||
// immediate: if true, the 'condition' will be invoked before waiting for the interval,
|
||||
// in this case 'condition' will always be invoked at least once.
|
||||
// wait: user specified WaitFunc function that controls at what interval the condition
|
||||
// function should be invoked periodically and whether it is bound by a timeout.
|
||||
// condition: user specified ConditionWithContextFunc function.
|
||||
//
|
||||
// Deprecated: will be removed in favor of loopConditionUntilContext.
|
||||
func poll(ctx context.Context, immediate bool, wait waitWithContextFunc, condition ConditionWithContextFunc) error {
|
||||
if immediate {
|
||||
done, err := runConditionWithCrashProtectionWithContext(ctx, condition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if done {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// returning ctx.Err() will break backward compatibility, use new PollUntilContext*
|
||||
// methods instead
|
||||
return ErrWaitTimeout
|
||||
default:
|
||||
return waitForWithContext(ctx, wait, condition)
|
||||
}
|
||||
}
|
||||
|
||||
// poller returns a WaitFunc that will send to the channel every interval until
|
||||
// timeout has elapsed and then closes the channel.
|
||||
//
|
||||
// Over very short intervals you may receive no ticks before the channel is
|
||||
// closed. A timeout of 0 is interpreted as an infinity, and in such a case
|
||||
// it would be the caller's responsibility to close the done channel.
|
||||
// Failure to do so would result in a leaked goroutine.
|
||||
//
|
||||
// Output ticks are not buffered. If the channel is not ready to receive an
|
||||
// item, the tick is skipped.
|
||||
//
|
||||
// Deprecated: Will be removed in a future release.
|
||||
func poller(interval, timeout time.Duration) waitWithContextFunc {
|
||||
return waitWithContextFunc(func(ctx context.Context) <-chan struct{} {
|
||||
ch := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
|
||||
tick := time.NewTicker(interval)
|
||||
defer tick.Stop()
|
||||
|
||||
var after <-chan time.Time
|
||||
if timeout != 0 {
|
||||
// time.After is more convenient, but it
|
||||
// potentially leaves timers around much longer
|
||||
// than necessary if we exit early.
|
||||
timer := time.NewTimer(timeout)
|
||||
after = timer.C
|
||||
defer timer.Stop()
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
// If the consumer isn't ready for this signal drop it and
|
||||
// check the other channels.
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
case <-after:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
})
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
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 wait
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"k8s.io/utils/clock"
|
||||
)
|
||||
|
||||
// Timer abstracts how wait functions interact with time runtime efficiently. Test
|
||||
// code may implement this interface directly but package consumers are encouraged
|
||||
// to use the Backoff type as the primary mechanism for acquiring a Timer. The
|
||||
// interface is a simplification of clock.Timer to prevent misuse. Timers are not
|
||||
// expected to be safe for calls from multiple goroutines.
|
||||
type Timer interface {
|
||||
// C returns a channel that will receive a struct{} each time the timer fires.
|
||||
// The channel should not be waited on after Stop() is invoked. It is allowed
|
||||
// to cache the returned value of C() for the lifetime of the Timer.
|
||||
C() <-chan time.Time
|
||||
// Next is invoked by wait functions to signal timers that the next interval
|
||||
// should begin. You may only use Next() if you have drained the channel C().
|
||||
// You should not call Next() after Stop() is invoked.
|
||||
Next()
|
||||
// Stop releases the timer. It is safe to invoke if no other methods have been
|
||||
// called.
|
||||
Stop()
|
||||
}
|
||||
|
||||
type noopTimer struct {
|
||||
closedCh <-chan time.Time
|
||||
}
|
||||
|
||||
// newNoopTimer creates a timer with a unique channel to avoid contention
|
||||
// for the channel's lock across multiple unrelated timers.
|
||||
func newNoopTimer() noopTimer {
|
||||
ch := make(chan time.Time)
|
||||
close(ch)
|
||||
return noopTimer{closedCh: ch}
|
||||
}
|
||||
|
||||
func (t noopTimer) C() <-chan time.Time {
|
||||
return t.closedCh
|
||||
}
|
||||
func (noopTimer) Next() {}
|
||||
func (noopTimer) Stop() {}
|
||||
|
||||
type variableTimer struct {
|
||||
fn DelayFunc
|
||||
t clock.Timer
|
||||
new func(time.Duration) clock.Timer
|
||||
}
|
||||
|
||||
func (t *variableTimer) C() <-chan time.Time {
|
||||
if t.t == nil {
|
||||
d := t.fn()
|
||||
t.t = t.new(d)
|
||||
}
|
||||
return t.t.C()
|
||||
}
|
||||
func (t *variableTimer) Next() {
|
||||
if t.t == nil {
|
||||
return
|
||||
}
|
||||
d := t.fn()
|
||||
t.t.Reset(d)
|
||||
}
|
||||
func (t *variableTimer) Stop() {
|
||||
if t.t == nil {
|
||||
return
|
||||
}
|
||||
t.t.Stop()
|
||||
t.t = nil
|
||||
}
|
||||
|
||||
type fixedTimer struct {
|
||||
interval time.Duration
|
||||
t clock.Ticker
|
||||
new func(time.Duration) clock.Ticker
|
||||
}
|
||||
|
||||
func (t *fixedTimer) C() <-chan time.Time {
|
||||
if t.t == nil {
|
||||
t.t = t.new(t.interval)
|
||||
}
|
||||
return t.t.C()
|
||||
}
|
||||
func (t *fixedTimer) Next() {
|
||||
// no-op for fixed timers
|
||||
}
|
||||
func (t *fixedTimer) Stop() {
|
||||
if t.t == nil {
|
||||
return
|
||||
}
|
||||
t.t.Stop()
|
||||
t.t = nil
|
||||
}
|
||||
|
||||
var (
|
||||
// RealTimer can be passed to methods that need a clock.Timer.
|
||||
RealTimer = clock.RealClock{}.NewTimer
|
||||
)
|
||||
|
||||
var (
|
||||
// internalClock is used for test injection of clocks
|
||||
internalClock = clock.RealClock{}
|
||||
)
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
Copyright 2014 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 wait
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
)
|
||||
|
||||
// For any test of the style:
|
||||
//
|
||||
// ...
|
||||
// <- time.After(timeout):
|
||||
// t.Errorf("Timed out")
|
||||
//
|
||||
// The value for timeout should effectively be "forever." Obviously we don't want our tests to truly lock up forever, but 30s
|
||||
// is long enough that it is effectively forever for the things that can slow down a run on a heavily contended machine
|
||||
// (GC, seeks, etc), but not so long as to make a developer ctrl-c a test run if they do happen to break that test.
|
||||
var ForeverTestTimeout = time.Second * 30
|
||||
|
||||
// NeverStop may be passed to Until to make it never stop.
|
||||
var NeverStop <-chan struct{} = make(chan struct{})
|
||||
|
||||
// Group allows to start a group of goroutines and wait for their completion.
|
||||
type Group struct {
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func (g *Group) Wait() {
|
||||
g.wg.Wait()
|
||||
}
|
||||
|
||||
// StartWithChannel starts f in a new goroutine in the group.
|
||||
// stopCh is passed to f as an argument. f should stop when stopCh is available.
|
||||
func (g *Group) StartWithChannel(stopCh <-chan struct{}, f func(stopCh <-chan struct{})) {
|
||||
g.Start(func() {
|
||||
f(stopCh)
|
||||
})
|
||||
}
|
||||
|
||||
// StartWithContext starts f in a new goroutine in the group.
|
||||
// ctx is passed to f as an argument. f should stop when ctx.Done() is available.
|
||||
func (g *Group) StartWithContext(ctx context.Context, f func(context.Context)) {
|
||||
g.Start(func() {
|
||||
f(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// Start starts f in a new goroutine in the group.
|
||||
func (g *Group) Start(f func()) {
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.wg.Done()
|
||||
f()
|
||||
}()
|
||||
}
|
||||
|
||||
// Forever calls f every period for ever.
|
||||
//
|
||||
// Forever is syntactic sugar on top of Until.
|
||||
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.
|
||||
//
|
||||
// This allows clients to avoid converging on periodic behavior. If maxFactor
|
||||
// is 0.0, a suggested default value will be chosen.
|
||||
func Jitter(duration time.Duration, maxFactor float64) time.Duration {
|
||||
if maxFactor <= 0.0 {
|
||||
maxFactor = 1.0
|
||||
}
|
||||
wait := duration + time.Duration(jitterRand()*maxFactor*float64(duration))
|
||||
return wait
|
||||
}
|
||||
|
||||
// ConditionFunc returns true if the condition is satisfied, or an error
|
||||
// if the loop should be aborted.
|
||||
type ConditionFunc func() (done bool, err error)
|
||||
|
||||
// ConditionWithContextFunc returns true if the condition is satisfied, or an error
|
||||
// if the loop should be aborted.
|
||||
//
|
||||
// The caller passes along a context that can be used by the condition function.
|
||||
type ConditionWithContextFunc func(context.Context) (done bool, err error)
|
||||
|
||||
// WithContext converts a ConditionFunc into a ConditionWithContextFunc
|
||||
func (cf ConditionFunc) WithContext() ConditionWithContextFunc {
|
||||
return func(context.Context) (done bool, err error) {
|
||||
return cf()
|
||||
}
|
||||
}
|
||||
|
||||
// ContextForChannel provides a context that will be treated as cancelled
|
||||
// when the provided parentCh is closed. The implementation returns
|
||||
// context.Canceled for Err() if and only if the parentCh is closed.
|
||||
func ContextForChannel(parentCh <-chan struct{}) context.Context {
|
||||
return channelContext{stopCh: parentCh}
|
||||
}
|
||||
|
||||
var _ context.Context = channelContext{}
|
||||
|
||||
// channelContext will behave as if the context were cancelled when stopCh is
|
||||
// closed.
|
||||
type channelContext struct {
|
||||
stopCh <-chan struct{}
|
||||
}
|
||||
|
||||
func (c channelContext) Done() <-chan struct{} { return c.stopCh }
|
||||
func (c channelContext) Err() error {
|
||||
select {
|
||||
case <-c.stopCh:
|
||||
return context.Canceled
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
func (c channelContext) Deadline() (time.Time, bool) { return time.Time{}, false }
|
||||
func (c channelContext) Value(key any) any { return nil }
|
||||
|
||||
// runConditionWithCrashProtection runs a ConditionFunc with crash protection.
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
|
||||
// runConditionWithCrashProtectionWithContext runs a ConditionWithContextFunc
|
||||
// with crash protection.
|
||||
//
|
||||
// Deprecated: Will be removed when the legacy polling methods are removed.
|
||||
func runConditionWithCrashProtectionWithContext(ctx context.Context, condition ConditionWithContextFunc) (bool, error) {
|
||||
defer runtime.HandleCrashWithContext(ctx)
|
||||
return condition(ctx)
|
||||
}
|
||||
|
||||
// waitFunc creates a channel that receives an item every time a test
|
||||
// should be executed and is closed when the last test should be invoked.
|
||||
//
|
||||
// Deprecated: Will be removed in a future release in favor of
|
||||
// loopConditionUntilContext.
|
||||
type waitFunc func(done <-chan struct{}) <-chan struct{}
|
||||
|
||||
// WithContext converts the WaitFunc to an equivalent WaitWithContextFunc
|
||||
func (w waitFunc) WithContext() waitWithContextFunc {
|
||||
return func(ctx context.Context) <-chan struct{} {
|
||||
return w(ctx.Done())
|
||||
}
|
||||
}
|
||||
|
||||
// waitWithContextFunc creates a channel that receives an item every time a test
|
||||
// should be executed and is closed when the last test should be invoked.
|
||||
//
|
||||
// When the specified context gets cancelled or expires the function
|
||||
// stops sending item and returns immediately.
|
||||
//
|
||||
// Deprecated: Will be removed in a future release in favor of
|
||||
// loopConditionUntilContext.
|
||||
type waitWithContextFunc func(ctx context.Context) <-chan struct{}
|
||||
|
||||
// waitForWithContext continually checks 'fn' as driven by 'wait'.
|
||||
//
|
||||
// waitForWithContext gets a channel from 'wait()”, and then invokes 'fn'
|
||||
// once for every value placed on the channel and once more when the
|
||||
// channel is closed. If the channel is closed and 'fn'
|
||||
// returns false without error, waitForWithContext returns ErrWaitTimeout.
|
||||
//
|
||||
// If 'fn' returns an error the loop ends and that error is returned. If
|
||||
// 'fn' returns true the loop ends and nil is returned.
|
||||
//
|
||||
// context.Canceled will be returned if the ctx.Done() channel is closed
|
||||
// without fn ever returning true.
|
||||
//
|
||||
// When the ctx.Done() channel is closed, because the golang `select` statement is
|
||||
// "uniform pseudo-random", the `fn` might still run one or multiple times,
|
||||
// though eventually `waitForWithContext` will return.
|
||||
//
|
||||
// Deprecated: Will be removed in a future release in favor of
|
||||
// loopConditionUntilContext.
|
||||
func waitForWithContext(ctx context.Context, wait waitWithContextFunc, fn ConditionWithContextFunc) error {
|
||||
waitCtx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
c := wait(waitCtx)
|
||||
for {
|
||||
select {
|
||||
case _, open := <-c:
|
||||
ok, err := runConditionWithCrashProtectionWithContext(ctx, fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
if !open {
|
||||
return ErrWaitTimeout
|
||||
}
|
||||
case <-ctx.Done():
|
||||
// returning ctx.Err() will break backward compatibility, use new PollUntilContext*
|
||||
// methods instead
|
||||
return ErrWaitTimeout
|
||||
}
|
||||
}
|
||||
}
|
||||
+485
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
Copyright 2014 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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
jsonutil "k8s.io/apimachinery/pkg/util/json"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Unmarshal unmarshals the given data
|
||||
// If v is a *map[string]interface{}, *[]interface{}, or *interface{} numbers
|
||||
// are converted to int64 or float64
|
||||
func Unmarshal(data []byte, v interface{}) error {
|
||||
preserveIntFloat := func(d *json.Decoder) *json.Decoder {
|
||||
d.UseNumber()
|
||||
return d
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case *map[string]interface{}:
|
||||
if err := yaml.Unmarshal(data, v, preserveIntFloat); err != nil {
|
||||
return err
|
||||
}
|
||||
return jsonutil.ConvertMapNumbers(*v, 0)
|
||||
case *[]interface{}:
|
||||
if err := yaml.Unmarshal(data, v, preserveIntFloat); err != nil {
|
||||
return err
|
||||
}
|
||||
return jsonutil.ConvertSliceNumbers(*v, 0)
|
||||
case *interface{}:
|
||||
if err := yaml.Unmarshal(data, v, preserveIntFloat); err != nil {
|
||||
return err
|
||||
}
|
||||
return jsonutil.ConvertInterfaceNumbers(v, 0)
|
||||
default:
|
||||
return yaml.Unmarshal(data, v)
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalStrict unmarshals the given data
|
||||
// strictly (erroring when there are duplicate fields).
|
||||
func UnmarshalStrict(data []byte, v interface{}) error {
|
||||
preserveIntFloat := func(d *json.Decoder) *json.Decoder {
|
||||
d.UseNumber()
|
||||
return d
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case *map[string]interface{}:
|
||||
if err := yaml.UnmarshalStrict(data, v, preserveIntFloat); err != nil {
|
||||
return err
|
||||
}
|
||||
return jsonutil.ConvertMapNumbers(*v, 0)
|
||||
case *[]interface{}:
|
||||
if err := yaml.UnmarshalStrict(data, v, preserveIntFloat); err != nil {
|
||||
return err
|
||||
}
|
||||
return jsonutil.ConvertSliceNumbers(*v, 0)
|
||||
case *interface{}:
|
||||
if err := yaml.UnmarshalStrict(data, v, preserveIntFloat); err != nil {
|
||||
return err
|
||||
}
|
||||
return jsonutil.ConvertInterfaceNumbers(v, 0)
|
||||
default:
|
||||
return yaml.UnmarshalStrict(data, v)
|
||||
}
|
||||
}
|
||||
|
||||
// ToJSON converts a single YAML document into a JSON document
|
||||
// or returns an error. If the document appears to be JSON the
|
||||
// YAML decoding path is not used (so that error messages are
|
||||
// JSON specific).
|
||||
func ToJSON(data []byte) ([]byte, error) {
|
||||
if IsJSONBuffer(data) {
|
||||
return data, nil
|
||||
}
|
||||
return yaml.YAMLToJSON(data)
|
||||
}
|
||||
|
||||
// YAMLToJSONDecoder decodes YAML documents from an io.Reader by
|
||||
// separating individual documents. It first converts the YAML
|
||||
// body to JSON, then unmarshals the JSON.
|
||||
type YAMLToJSONDecoder struct {
|
||||
reader Reader
|
||||
inputOffset int
|
||||
}
|
||||
|
||||
// NewYAMLToJSONDecoder decodes YAML documents from the provided
|
||||
// stream in chunks by converting each document (as defined by
|
||||
// the YAML spec) into its own chunk, converting it to JSON via
|
||||
// yaml.YAMLToJSON, and then passing it to json.Decoder.
|
||||
func NewYAMLToJSONDecoder(r io.Reader) *YAMLToJSONDecoder {
|
||||
reader := bufio.NewReader(r)
|
||||
return &YAMLToJSONDecoder{
|
||||
reader: NewYAMLReader(reader),
|
||||
}
|
||||
}
|
||||
|
||||
// Decode reads a YAML document as JSON from the stream or returns
|
||||
// an error. The decoding rules match json.Unmarshal, not
|
||||
// yaml.Unmarshal.
|
||||
func (d *YAMLToJSONDecoder) Decode(into interface{}) error {
|
||||
bytes, err := d.reader.Read()
|
||||
if err != nil && err != io.EOF { //nolint:errorlint
|
||||
return err
|
||||
}
|
||||
|
||||
if len(bytes) != 0 {
|
||||
err := yaml.Unmarshal(bytes, into)
|
||||
if err != nil {
|
||||
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 {
|
||||
r io.ReadCloser
|
||||
scanner *bufio.Scanner
|
||||
remaining []byte
|
||||
}
|
||||
|
||||
// NewDocumentDecoder decodes YAML documents from the provided
|
||||
// stream in chunks by converting each document (as defined by
|
||||
// the YAML spec) into its own chunk. io.ErrShortBuffer will be
|
||||
// returned if the entire buffer could not be read to assist
|
||||
// the caller in framing the chunk.
|
||||
func NewDocumentDecoder(r io.ReadCloser) io.ReadCloser {
|
||||
scanner := bufio.NewScanner(r)
|
||||
// the size of initial allocation for buffer 4k
|
||||
buf := make([]byte, 4*1024)
|
||||
// the maximum size used to buffer a token 5M
|
||||
scanner.Buffer(buf, 5*1024*1024)
|
||||
scanner.Split(splitYAMLDocument)
|
||||
return &YAMLDecoder{
|
||||
r: r,
|
||||
scanner: scanner,
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads the previous slice into the buffer, or attempts to read
|
||||
// the next chunk.
|
||||
// TODO: switch to readline approach.
|
||||
func (d *YAMLDecoder) Read(data []byte) (n int, err error) {
|
||||
left := len(d.remaining)
|
||||
if left == 0 {
|
||||
// return the next chunk from the stream
|
||||
if !d.scanner.Scan() {
|
||||
err := d.scanner.Err()
|
||||
if err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
out := d.scanner.Bytes()
|
||||
d.remaining = out
|
||||
left = len(out)
|
||||
}
|
||||
|
||||
// fits within data
|
||||
if left <= len(data) {
|
||||
copy(data, d.remaining)
|
||||
d.remaining = nil
|
||||
return left, nil
|
||||
}
|
||||
|
||||
// caller will need to reread
|
||||
copy(data, d.remaining[:len(data)])
|
||||
d.remaining = d.remaining[len(data):]
|
||||
return len(data), io.ErrShortBuffer
|
||||
}
|
||||
|
||||
func (d *YAMLDecoder) Close() error {
|
||||
return d.r.Close()
|
||||
}
|
||||
|
||||
const yamlSeparator = "\n---"
|
||||
const separator = "---"
|
||||
|
||||
// splitYAMLDocument is a bufio.SplitFunc for splitting YAML streams into individual documents.
|
||||
func splitYAMLDocument(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
sep := len([]byte(yamlSeparator))
|
||||
if i := bytes.Index(data, []byte(yamlSeparator)); i >= 0 {
|
||||
// We have a potential document terminator
|
||||
i += sep
|
||||
after := data[i:]
|
||||
if len(after) == 0 {
|
||||
// we can't read any more characters
|
||||
if atEOF {
|
||||
return len(data), data[:len(data)-sep], nil
|
||||
}
|
||||
return 0, nil, nil
|
||||
}
|
||||
if j := bytes.IndexByte(after, '\n'); j >= 0 {
|
||||
return i + j + 1, data[0 : i-sep], nil
|
||||
}
|
||||
return 0, nil, nil
|
||||
}
|
||||
// If we're at EOF, we have a final, non-terminated line. Return it.
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
// Request more data.
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
json *json.Decoder
|
||||
jsonConsumed int64 // of the stream total, how much was JSON?
|
||||
yaml *YAMLToJSONDecoder
|
||||
yamlConsumed int64 // of the stream total, how much was YAML?
|
||||
stream *StreamReader
|
||||
count int // how many objects have been decoded
|
||||
}
|
||||
|
||||
type JSONSyntaxError struct {
|
||||
Offset int64
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e JSONSyntaxError) Error() string {
|
||||
return fmt.Sprintf("json: offset %d: %s", e.Offset, e.Err.Error())
|
||||
}
|
||||
|
||||
type YAMLSyntaxError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e YAMLSyntaxError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
// NewYAMLOrJSONDecoder returns a decoder that will process YAML documents
|
||||
// or JSON documents from the given reader as a stream. bufferSize determines
|
||||
// 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 {
|
||||
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 {
|
||||
// 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.count++
|
||||
consumed := d.json.InputOffset() - d.jsonConsumed
|
||||
d.stream.Consume(int(consumed))
|
||||
d.jsonConsumed += consumed
|
||||
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 {
|
||||
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.count++
|
||||
consumed := int64(d.yaml.InputOffset()) - d.yamlConsumed
|
||||
d.stream.Consume(int(consumed))
|
||||
d.yamlConsumed += consumed
|
||||
return nil
|
||||
}
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
return err
|
||||
}
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
consumed += sz
|
||||
if r == '\n' {
|
||||
d.stream.Consume(consumed)
|
||||
return nil
|
||||
}
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
break
|
||||
}
|
||||
}
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
type Reader interface {
|
||||
Read() ([]byte, error)
|
||||
}
|
||||
|
||||
type YAMLReader struct {
|
||||
reader Reader
|
||||
}
|
||||
|
||||
func NewYAMLReader(r *bufio.Reader) *YAMLReader {
|
||||
return &YAMLReader{
|
||||
reader: &LineReader{reader: r},
|
||||
}
|
||||
}
|
||||
|
||||
// Read returns a full YAML document.
|
||||
func (r *YAMLReader) Read() ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
for {
|
||||
line, err := r.reader.Read()
|
||||
if err != nil && err != io.EOF { //nolint:errorlint
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sep := len([]byte(separator))
|
||||
if i := bytes.Index(line, []byte(separator)); i == 0 {
|
||||
// We have a potential document terminator
|
||||
i += sep
|
||||
trimmed := strings.TrimSpace(string(line[i:]))
|
||||
// We only allow comments and spaces following the yaml doc separator, otherwise we'll return an error
|
||||
if len(trimmed) > 0 && string(trimmed[0]) != "#" {
|
||||
return nil, YAMLSyntaxError{
|
||||
err: fmt.Errorf("invalid Yaml document separator: %s", trimmed),
|
||||
}
|
||||
}
|
||||
if buffer.Len() != 0 {
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
buffer.Write(line)
|
||||
}
|
||||
}
|
||||
|
||||
type LineReader struct {
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
// Read returns a single line (with '\n' ended) from the underlying reader.
|
||||
// An error is returned iff there is an error with the underlying reader.
|
||||
func (r *LineReader) Read() ([]byte, error) {
|
||||
var (
|
||||
isPrefix bool = true
|
||||
err error = nil
|
||||
line []byte
|
||||
buffer bytes.Buffer
|
||||
)
|
||||
|
||||
for isPrefix && err == nil {
|
||||
line, isPrefix, err = r.reader.ReadLine()
|
||||
buffer.Write(line)
|
||||
}
|
||||
buffer.WriteByte('\n')
|
||||
return buffer.Bytes(), err
|
||||
}
|
||||
|
||||
// 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) (*StreamReader, []byte, bool) {
|
||||
buffer := NewStreamReader(r, size)
|
||||
b, _ := buffer.Peek(size)
|
||||
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 hasPrefix(buf, jsonPrefix)
|
||||
}
|
||||
|
||||
var jsonPrefix = []byte("{")
|
||||
|
||||
// Return true if the first non-whitespace bytes in buf is
|
||||
// prefix.
|
||||
func hasPrefix(buf []byte, prefix []byte) bool {
|
||||
trim := bytes.TrimLeftFunc(buf, unicode.IsSpace)
|
||||
return bytes.HasPrefix(trim, prefix)
|
||||
}
|
||||
+130
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user