Update custom-metrics-apiserver and metrics-server

This commit is contained in:
Johannes Würbach 2020-09-27 22:14:53 +02:00
parent 4c673534f2
commit b480e45a67
No known key found for this signature in database
GPG key ID: 74DB0F4D956CCCE3
915 changed files with 63694 additions and 106514 deletions

View file

@ -18,8 +18,8 @@ package filters
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@ -28,61 +28,9 @@ import (
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/klog"
)
/*
* By default, all the following metrics are defined as falling under
* ALPHA stability level https://github.com/kubernetes/enhancements/blob/master/keps/sig-instrumentation/20190404-kubernetes-control-plane-metrics-stability.md#stability-classes)
*
* Promoting the stability level of the metric is a responsibility of the component owner, since it
* involves explicitly acknowledging support for the metric across multiple releases, in accordance with
* the metric stability policy.
*/
const (
successLabel = "success"
failureLabel = "failure"
errorLabel = "error"
)
var (
authenticatedUserCounter = metrics.NewCounterVec(
&metrics.CounterOpts{
Name: "authenticated_user_requests",
Help: "Counter of authenticated requests broken out by username.",
StabilityLevel: metrics.ALPHA,
},
[]string{"username"},
)
authenticatedAttemptsCounter = metrics.NewCounterVec(
&metrics.CounterOpts{
Name: "authentication_attempts",
Help: "Counter of authenticated attempts.",
StabilityLevel: metrics.ALPHA,
},
[]string{"result"},
)
authenticationLatency = metrics.NewHistogramVec(
&metrics.HistogramOpts{
Name: "authentication_duration_seconds",
Help: "Authentication duration in seconds broken out by result.",
Buckets: metrics.ExponentialBuckets(0.001, 2, 15),
StabilityLevel: metrics.ALPHA,
},
[]string{"result"},
)
)
func init() {
legacyregistry.MustRegister(authenticatedUserCounter)
legacyregistry.MustRegister(authenticatedAttemptsCounter)
legacyregistry.MustRegister(authenticationLatency)
}
// WithAuthentication creates an http handler that tries to authenticate the given request as a user, and then
// stores any such user found onto the provided context for the request. If authentication fails or returns an error
// the failed handler is used. On success, "Authorization" header is removed from the request and handler
@ -99,22 +47,18 @@ func WithAuthentication(handler http.Handler, auth authenticator.Request, failed
req = req.WithContext(authenticator.WithAudiences(req.Context(), apiAuds))
}
resp, ok, err := auth.AuthenticateRequest(req)
defer recordAuthMetrics(resp, ok, err, apiAuds, authenticationStart)
if err != nil || !ok {
if err != nil {
klog.Errorf("Unable to authenticate the request due to an error: %v", err)
authenticatedAttemptsCounter.WithLabelValues(errorLabel).Inc()
authenticationLatency.WithLabelValues(errorLabel).Observe(time.Since(authenticationStart).Seconds())
} else if !ok {
authenticatedAttemptsCounter.WithLabelValues(failureLabel).Inc()
authenticationLatency.WithLabelValues(failureLabel).Observe(time.Since(authenticationStart).Seconds())
}
failed.ServeHTTP(w, req)
return
}
if len(apiAuds) > 0 && len(resp.Audiences) > 0 && len(authenticator.Audiences(apiAuds).Intersect(resp.Audiences)) == 0 {
klog.Errorf("Unable to match the audience: %v , accepted: %v", resp.Audiences, apiAuds)
if !audiencesAreAcceptable(apiAuds, resp.Audiences) {
err = fmt.Errorf("unable to match the audience: %v , accepted: %v", resp.Audiences, apiAuds)
klog.Error(err)
failed.ServeHTTP(w, req)
return
}
@ -123,11 +67,6 @@ func WithAuthentication(handler http.Handler, auth authenticator.Request, failed
req.Header.Del("Authorization")
req = req.WithContext(genericapirequest.WithUser(req.Context(), resp.User))
authenticatedUserCounter.WithLabelValues(compressUsername(resp.User.GetName())).Inc()
authenticatedAttemptsCounter.WithLabelValues(successLabel).Inc()
authenticationLatency.WithLabelValues(successLabel).Observe(time.Since(authenticationStart).Seconds())
handler.ServeHTTP(w, req)
})
}
@ -149,24 +88,10 @@ func Unauthorized(s runtime.NegotiatedSerializer, supportsBasicAuth bool) http.H
})
}
// compressUsername maps all possible usernames onto a small set of categories
// of usernames. This is done both to limit the cardinality of the
// authorized_user_requests metric, and to avoid pushing actual usernames in the
// metric.
func compressUsername(username string) string {
switch {
// Known internal identities.
case username == "admin" ||
username == "client" ||
username == "kube_proxy" ||
username == "kubelet" ||
username == "system:serviceaccount:kube-system:default":
return username
// Probably an email address.
case strings.Contains(username, "@"):
return "email_id"
// Anything else (custom service accounts, custom external identities, etc.)
default:
return "other"
func audiencesAreAcceptable(apiAuds, responseAudiences authenticator.Audiences) bool {
if len(apiAuds) == 0 || len(responseAudiences) == 0 {
return true
}
return len(apiAuds.Intersect(responseAudiences)) > 0
}

View file

@ -0,0 +1,115 @@
/*
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 filters
import (
"strings"
"time"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
/*
* By default, all the following metrics are defined as falling under
* ALPHA stability level https://github.com/kubernetes/enhancements/blob/master/keps/sig-instrumentation/20190404-kubernetes-control-plane-metrics-stability.md#stability-classes)
*
* Promoting the stability level of the metric is a responsibility of the component owner, since it
* involves explicitly acknowledging support for the metric across multiple releases, in accordance with
* the metric stability policy.
*/
const (
successLabel = "success"
failureLabel = "failure"
errorLabel = "error"
)
var (
authenticatedUserCounter = metrics.NewCounterVec(
&metrics.CounterOpts{
Name: "authenticated_user_requests",
Help: "Counter of authenticated requests broken out by username.",
StabilityLevel: metrics.ALPHA,
},
[]string{"username"},
)
authenticatedAttemptsCounter = metrics.NewCounterVec(
&metrics.CounterOpts{
Name: "authentication_attempts",
Help: "Counter of authenticated attempts.",
StabilityLevel: metrics.ALPHA,
},
[]string{"result"},
)
authenticationLatency = metrics.NewHistogramVec(
&metrics.HistogramOpts{
Name: "authentication_duration_seconds",
Help: "Authentication duration in seconds broken out by result.",
Buckets: metrics.ExponentialBuckets(0.001, 2, 15),
StabilityLevel: metrics.ALPHA,
},
[]string{"result"},
)
)
func init() {
legacyregistry.MustRegister(authenticatedUserCounter)
legacyregistry.MustRegister(authenticatedAttemptsCounter)
legacyregistry.MustRegister(authenticationLatency)
}
func recordAuthMetrics(resp *authenticator.Response, ok bool, err error, apiAudiences authenticator.Audiences, authStart time.Time) {
var resultLabel string
switch {
case err != nil || (resp != nil && !audiencesAreAcceptable(apiAudiences, resp.Audiences)):
resultLabel = errorLabel
case !ok:
resultLabel = failureLabel
default:
resultLabel = successLabel
authenticatedUserCounter.WithLabelValues(compressUsername(resp.User.GetName())).Inc()
}
authenticatedAttemptsCounter.WithLabelValues(resultLabel).Inc()
authenticationLatency.WithLabelValues(resultLabel).Observe(time.Since(authStart).Seconds())
}
// compressUsername maps all possible usernames onto a small set of categories
// of usernames. This is done both to limit the cardinality of the
// authorized_user_requests metric, and to avoid pushing actual usernames in the
// metric.
func compressUsername(username string) string {
switch {
// Known internal identities.
case username == "admin" ||
username == "client" ||
username == "kube_proxy" ||
username == "kubelet" ||
username == "system:serviceaccount:kube-system:default":
return username
// Probably an email address.
case strings.Contains(username, "@"):
return "email_id"
// Anything else (custom service accounts, custom external identities, etc.)
default:
return "other"
}
}

View file

@ -27,6 +27,7 @@ import (
"unicode/utf8"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
@ -50,7 +51,7 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int
defer trace.LogIfLong(500 * time.Millisecond)
if isDryRun(req.URL) && !utilfeature.DefaultFeatureGate.Enabled(features.DryRun) {
scope.err(errors.NewBadRequest("the dryRun alpha feature is disabled"), w, req)
scope.err(errors.NewBadRequest("the dryRun feature is disabled"), w, req)
return
}
@ -137,31 +138,10 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int
if len(name) == 0 {
_, name, _ = scope.Namer.ObjectName(obj)
}
admissionAttributes := admission.NewAttributesRecord(obj, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Create, options, dryrun.IsDryRun(options.DryRun), userInfo)
if mutatingAdmission, ok := admit.(admission.MutationInterface); ok && mutatingAdmission.Handles(admission.Create) {
err = mutatingAdmission.Admit(ctx, admissionAttributes, scope)
if err != nil {
scope.err(err, w, req)
return
}
}
if scope.FieldManager != nil {
liveObj, err := scope.Creater.New(scope.Kind)
if err != nil {
scope.err(fmt.Errorf("failed to create new object (Create for %v): %v", scope.Kind, err), w, req)
return
}
obj, err = scope.FieldManager.Update(liveObj, obj, managerOrUserAgent(options.FieldManager, req.UserAgent()))
if err != nil {
scope.err(fmt.Errorf("failed to update object (Create for %v) managed fields: %v", scope.Kind, err), w, req)
return
}
}
trace.Step("About to store object in database")
result, err := finishRequest(timeout, func() (runtime.Object, error) {
admissionAttributes := admission.NewAttributesRecord(obj, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Create, options, dryrun.IsDryRun(options.DryRun), userInfo)
requestFunc := func() (runtime.Object, error) {
return r.Create(
ctx,
name,
@ -169,6 +149,30 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int
rest.AdmissionToValidateObjectFunc(admit, admissionAttributes, scope),
options,
)
}
result, err := finishRequest(timeout, func() (runtime.Object, error) {
if scope.FieldManager != nil {
liveObj, err := scope.Creater.New(scope.Kind)
if err != nil {
return nil, fmt.Errorf("failed to create new object (Create for %v): %v", scope.Kind, err)
}
obj = scope.FieldManager.UpdateNoErrors(liveObj, obj, managerOrUserAgent(options.FieldManager, req.UserAgent()))
}
if mutatingAdmission, ok := admit.(admission.MutationInterface); ok && mutatingAdmission.Handles(admission.Create) {
if err := mutatingAdmission.Admit(ctx, admissionAttributes, scope); err != nil {
return nil, err
}
}
result, err := requestFunc()
// If the object wasn't committed to storage because it's serialized size was too large,
// it is safe to remove managedFields (which can be large) and try again.
if isTooLargeError(err) {
if accessor, accessorErr := meta.Accessor(obj); accessorErr == nil {
accessor.SetManagedFields(nil)
result, err = requestFunc()
}
}
return result, err
})
if err != nil {
scope.err(err, w, req)

View file

@ -49,7 +49,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope *RequestSc
defer trace.LogIfLong(500 * time.Millisecond)
if isDryRun(req.URL) && !utilfeature.DefaultFeatureGate.Enabled(features.DryRun) {
scope.err(errors.NewBadRequest("the dryRun alpha feature is disabled"), w, req)
scope.err(errors.NewBadRequest("the dryRun feature is disabled"), w, req)
return
}
@ -167,7 +167,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope *RequestSc
defer trace.LogIfLong(500 * time.Millisecond)
if isDryRun(req.URL) && !utilfeature.DefaultFeatureGate.Enabled(features.DryRun) {
scope.err(errors.NewBadRequest("the dryRun alpha feature is disabled"), w, req)
scope.err(errors.NewBadRequest("the dryRun feature is disabled"), w, req)
return
}

View file

@ -51,12 +51,12 @@ func (f *buildManagerInfoManager) Update(liveObj, newObj runtime.Object, managed
}
// Apply implements Manager.
func (f *buildManagerInfoManager) Apply(liveObj runtime.Object, patch []byte, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
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, patch, managed, manager, force)
return f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force)
}
func (f *buildManagerInfoManager) buildManagerInfo(prefix string, operation metav1.ManagedFieldsOperationType) (string, error) {

View file

@ -19,12 +19,11 @@ package fieldmanager
import (
"fmt"
"sort"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/v3/fieldpath"
)
type capManagersManager struct {
@ -58,8 +57,8 @@ func (f *capManagersManager) Update(liveObj, newObj runtime.Object, managed Mana
}
// Apply implements Manager.
func (f *capManagersManager) Apply(liveObj runtime.Object, patch []byte, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
return f.fieldManager.Apply(liveObj, patch, managed, fieldManager, force)
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,
@ -78,15 +77,15 @@ func (f *capManagersManager) capUpdateManagers(managed Managed) (newManaged Mana
// 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, nTime := managed.Times()[updaters[i]], managed.Times()[updaters[j]], &metav1.Time{Time: time.Time{}}
if iTime == nil {
iTime = nTime
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 {
jTime = nTime
if jTime != nil {
jSeconds = jTime.Unix()
}
if !iTime.Equal(jTime) {
return iTime.Before(jTime)
if iSeconds != jSeconds {
return iSeconds < jSeconds
}
return updaters[i] < updaters[j]
})

View file

@ -18,14 +18,17 @@ package fieldmanager
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/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"k8s.io/klog"
openapiproto "k8s.io/kube-openapi/pkg/util/proto"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/v3/fieldpath"
)
// DefaultMaxUpdateManagers defines the default maximum retained number of managedFields entries from updates
@ -33,6 +36,12 @@ import (
// 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 = internal.NewAtMostEvery(time.Second)
// Managed groups a fieldpath.ManagedFields together with the timestamps associated with each operation.
type Managed interface {
// Fields gets the fieldpath.ManagedFields.
@ -51,7 +60,7 @@ type Manager interface {
// Apply is used when server-side apply is called, as it merges the
// object and updates the managed fields.
Apply(liveObj runtime.Object, patch []byte, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error)
Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error)
}
// FieldManager updates the managed fields and merge applied
@ -90,9 +99,10 @@ func NewDefaultCRDFieldManager(models openapiproto.Models, objectConverter runti
// newDefaultFieldManager is a helper function which wraps a Manager with certain default logic.
func newDefaultFieldManager(f Manager, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind) *FieldManager {
f = NewStripMetaManager(f)
f = NewManagedFieldsUpdater(f)
f = NewBuildManagerInfoManager(f, kind.GroupVersion())
f = NewCapManagersManager(f, DefaultMaxUpdateManagers)
f = NewSkipNonAppliedManager(f, objectCreater, kind)
f = NewProbabilisticSkipNonAppliedManager(f, objectCreater, kind, DefaultTrackOnCreateProbability)
return NewFieldManager(f)
}
@ -102,20 +112,26 @@ func newDefaultFieldManager(f Manager, objectCreater runtime.ObjectCreater, kind
func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (object runtime.Object, err error) {
// 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.
if _, err = meta.Accessor(newObj); err != nil {
newAccessor, err := meta.Accessor(newObj)
if err != nil {
return newObj, nil
}
// First try to decode the managed fields provided in the update,
// This is necessary to allow directly updating managed fields.
var managed Managed
if managed, err = internal.DecodeObjectManagedFields(newObj); err != nil || len(managed.Fields()) == 0 {
if isResetManagedFields(newAccessor.GetManagedFields()) {
managed = internal.NewEmptyManaged()
} else if managed, err = internal.DecodeObjectManagedFields(newAccessor.GetManagedFields()); err != nil || len(managed.Fields()) == 0 {
liveAccessor, err := meta.Accessor(liveObj)
if err != nil {
return newObj, 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 = internal.DecodeObjectManagedFields(liveObj)
if err != nil {
return nil, fmt.Errorf("failed to decode managed fields: %v", err)
if managed, err = internal.DecodeObjectManagedFields(liveAccessor.GetManagedFields()); err != nil {
managed = internal.NewEmptyManaged()
}
}
@ -133,23 +149,58 @@ func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (o
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() {
klog.Errorf("[SHOULD NOT HAPPEN] failed to update managedFields for %v: %v",
newObj.GetObjectKind().GroupVersionKind(),
err)
})
// Explicitly remove managedFields on failure, so that
// we can't have garbage in it.
internal.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 runtime.Object, patch []byte, manager string, force bool) (object runtime.Object, err error) {
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.
if _, err = meta.Accessor(liveObj); err != nil {
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.
var managed Managed
if managed, err = internal.DecodeObjectManagedFields(liveObj); err != nil {
managed, err := internal.DecodeObjectManagedFields(accessor.GetManagedFields())
if err != nil {
return nil, fmt.Errorf("failed to decode managed fields: %v", err)
}
internal.RemoveObjectManagedFields(liveObj)
if object, managed, err = f.fieldManager.Apply(liveObj, patch, managed, manager, force); err != nil {
if object, managed, err = f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force); err != nil {
return nil, err
}

View file

@ -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
}

View file

@ -25,8 +25,8 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/merge"
"sigs.k8s.io/structured-merge-diff/v3/fieldpath"
"sigs.k8s.io/structured-merge-diff/v3/merge"
)
// NewConflictError returns an error including details on the requests apply conflicts

View file

@ -21,12 +21,12 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/v3/fieldpath"
)
// EmptyFields represents a set with no paths
// It looks like metav1.Fields{Raw: []byte("{}")}
var EmptyFields metav1.FieldsV1 = func() metav1.FieldsV1 {
var EmptyFields = func() metav1.FieldsV1 {
f, err := SetToFields(*fieldpath.NewSet())
if err != nil {
panic("should never happen")

View file

@ -22,7 +22,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/schemaconv"
"k8s.io/kube-openapi/pkg/util/proto"
"sigs.k8s.io/structured-merge-diff/typed"
"sigs.k8s.io/structured-merge-diff/v3/typed"
)
// groupVersionKindExtensionKey is the key used to lookup the

View file

@ -20,12 +20,11 @@ import (
"encoding/json"
"fmt"
"sort"
"time"
"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/fieldpath"
"sigs.k8s.io/structured-merge-diff/v3/fieldpath"
)
// ManagedInterface groups a fieldpath.ManagedFields together with the timestamps associated with each operation.
@ -54,6 +53,11 @@ 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{
@ -74,16 +78,8 @@ func RemoveObjectManagedFields(obj runtime.Object) {
}
// DecodeObjectManagedFields extracts and converts the objects ManagedFields into a fieldpath.ManagedFields.
func DecodeObjectManagedFields(from runtime.Object) (ManagedInterface, error) {
if from == nil {
return &managedStruct{}, nil
}
accessor, err := meta.Accessor(from)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
managed, err := decodeManagedFields(accessor.GetManagedFields())
func DecodeObjectManagedFields(from []metav1.ManagedFieldsEntry) (ManagedInterface, error) {
managed, err := decodeManagedFields(from)
if err != nil {
return nil, fmt.Errorf("failed to convert managed fields from API: %v", err)
}
@ -111,7 +107,16 @@ func EncodeObjectManagedFields(obj runtime.Object, managed ManagedInterface) err
func decodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (managed managedStruct, err error) {
managed.fields = make(fieldpath.ManagedFields, len(encodedManagedFields))
managed.times = make(map[string]*metav1.Time, len(encodedManagedFields))
for _, encodedVersionedSet := range encodedManagedFields {
for i, encodedVersionedSet := range encodedManagedFields {
switch encodedVersionedSet.FieldsType {
case "FieldsV1":
// Valid case.
case "":
return managedStruct{}, fmt.Errorf("missing fieldsType in managed fields entry %d", i)
default:
return managedStruct{}, fmt.Errorf("invalid fieldsType %q in managed fields entry %d", encodedVersionedSet.FieldsType, i)
}
manager, err := BuildManagerIdentifier(&encodedVersionedSet)
if err != nil {
return managedStruct{}, fmt.Errorf("error decoding manager from %v: %v", encodedVersionedSet, err)
@ -194,15 +199,15 @@ func sortEncodedManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry)
return p.Operation < q.Operation
}
ntime := &metav1.Time{Time: time.Time{}}
if p.Time == nil {
p.Time = ntime
pSeconds, qSeconds := int64(0), int64(0)
if p.Time != nil {
pSeconds = p.Time.Unix()
}
if q.Time == nil {
q.Time = ntime
if q.Time != nil {
qSeconds = q.Time.Unix()
}
if !p.Time.Equal(q.Time) {
return p.Time.Before(q.Time)
if pSeconds != qSeconds {
return pSeconds < qSeconds
}
if p.Manager != q.Manager {

View file

@ -23,8 +23,8 @@ import (
"strconv"
"strings"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/value"
"sigs.k8s.io/structured-merge-diff/v3/fieldpath"
"sigs.k8s.io/structured-merge-diff/v3/value"
)
const (
@ -110,7 +110,7 @@ func PathElementString(pe fieldpath.PathElement) (string, error) {
case pe.Key != nil:
kv := map[string]json.RawMessage{}
for _, k := range *pe.Key {
b, err := k.Value.ToJSON()
b, err := value.ToJSON(k.Value)
if err != nil {
return "", err
}
@ -127,7 +127,7 @@ func PathElementString(pe fieldpath.PathElement) (string, error) {
}
return Key + ":" + string(b), nil
case pe.Value != nil:
b, err := pe.Value.ToJSON()
b, err := value.ToJSON(*pe.Value)
if err != nil {
return "", err
}

View file

@ -23,8 +23,8 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/util/proto"
"sigs.k8s.io/structured-merge-diff/typed"
"sigs.k8s.io/structured-merge-diff/value"
"sigs.k8s.io/structured-merge-diff/v3/typed"
"sigs.k8s.io/structured-merge-diff/v3/value"
)
// TypeConverter allows you to convert from runtime.Object to
@ -49,11 +49,12 @@ var _ TypeConverter = DeducedTypeConverter{}
// ObjectToTyped converts an object into a TypedValue with a "deduced type".
func (DeducedTypeConverter) ObjectToTyped(obj runtime.Object) (*typed.TypedValue, error) {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
switch o := obj.(type) {
case *unstructured.Unstructured:
return typed.DeducedParseableType.FromUnstructured(o.UnstructuredContent())
default:
return typed.DeducedParseableType.FromStructured(obj)
}
return typed.DeducedParseableType.FromUnstructured(u)
}
// TypedToObject transforms the typed value into a runtime.Object. That
@ -80,29 +81,31 @@ func NewTypeConverter(models proto.Models, preserveUnknownFields bool) (TypeConv
}
func (c *typeConverter) ObjectToTyped(obj runtime.Object) (*typed.TypedValue, error) {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
gvk := obj.GetObjectKind().GroupVersionKind()
t := c.parser.Type(gvk)
if t == nil {
return nil, newNoCorrespondingTypeError(gvk)
}
return t.FromUnstructured(u)
switch o := obj.(type) {
case *unstructured.Unstructured:
return t.FromUnstructured(o.UnstructuredContent())
default:
return t.FromStructured(obj)
}
}
func (c *typeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, error) {
return valueToObject(value.AsValue())
}
func valueToObject(value *value.Value) (runtime.Object, error) {
vu := value.ToUnstructured(false)
u, ok := vu.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("failed to convert typed to unstructured: want map, got %T", vu)
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)
}
return &unstructured.Unstructured{Object: u}, nil
}
type noCorrespondingTypeErr struct {

View file

@ -19,9 +19,9 @@ package internal
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/merge"
"sigs.k8s.io/structured-merge-diff/typed"
"sigs.k8s.io/structured-merge-diff/v3/fieldpath"
"sigs.k8s.io/structured-merge-diff/v3/merge"
"sigs.k8s.io/structured-merge-diff/v3/typed"
)
// versionConverter is an implementation of

View file

@ -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 fieldmanager
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/v3/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)
managed.Times()[manager] = &metav1.Time{Time: time.Now().UTC()}
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
}
}
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) {
formerManaged := managed.Fields().Copy()
object, managed, err := f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
if err != nil {
return object, managed, err
}
if object != nil || !managed.Fields().Equals(formerManaged) {
managed.Times()[fieldManager] = &metav1.Time{Time: time.Now().UTC()}
}
if object == nil {
object = liveObj
}
return object, managed, nil
}

View file

@ -7,10 +7,10 @@ metadata:
volumes.kubernetes.io/controller-managed-attach-detach: "true"
creationTimestamp: "2019-07-09T16:17:29Z"
labels:
beta.kubernetes.io/arch: amd64
kubernetes.io/arch: amd64
beta.kubernetes.io/fluentd-ds-ready: "true"
beta.kubernetes.io/instance-type: n1-standard-4
beta.kubernetes.io/os: linux
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

View file

@ -18,7 +18,9 @@ package fieldmanager
import (
"fmt"
"math/rand"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
@ -28,30 +30,53 @@ type skipNonAppliedManager struct {
objectCreater runtime.ObjectCreater
gvk schema.GroupVersionKind
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, gvk schema.GroupVersionKind) Manager {
return NewProbabilisticSkipNonAppliedManager(fieldManager, objectCreater, gvk, 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, gvk schema.GroupVersionKind, p float32) Manager {
return &skipNonAppliedManager{
fieldManager: fieldManager,
objectCreater: objectCreater,
gvk: gvk,
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) {
if len(managed.Fields()) == 0 {
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 runtime.Object, patch []byte, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
func (f *skipNonAppliedManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
if len(managed.Fields()) == 0 {
emptyObj, err := f.objectCreater.New(f.gvk)
if err != nil {
@ -62,5 +87,5 @@ func (f *skipNonAppliedManager) Apply(liveObj runtime.Object, patch []byte, mana
return nil, nil, fmt.Errorf("failed to create manager for existing fields: %v", err)
}
}
return f.fieldManager.Apply(liveObj, patch, managed, fieldManager, force)
return f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
}

View file

@ -20,7 +20,7 @@ import (
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/v3/fieldpath"
)
type stripMetaManager struct {
@ -64,8 +64,8 @@ func (f *stripMetaManager) Update(liveObj, newObj runtime.Object, managed Manage
}
// Apply implements Manager.
func (f *stripMetaManager) Apply(liveObj runtime.Object, patch []byte, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
newObj, managed, err := f.fieldManager.Apply(liveObj, patch, managed, manager, force)
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
}

View file

@ -18,19 +18,15 @@ package fieldmanager
import (
"fmt"
"time"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"k8s.io/klog"
openapiproto "k8s.io/kube-openapi/pkg/util/proto"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/merge"
"sigs.k8s.io/yaml"
"sigs.k8s.io/structured-merge-diff/v3/fieldpath"
"sigs.k8s.io/structured-merge-diff/v3/merge"
)
type structuredMergeManager struct {
@ -99,59 +95,40 @@ func (f *structuredMergeManager) Update(liveObj, newObj runtime.Object, managed
}
newObjTyped, err := f.typeConverter.ObjectToTyped(newObjVersioned)
if err != nil {
// Return newObj and just by-pass fields update. This really shouldn't happen.
klog.Errorf("[SHOULD NOT HAPPEN] failed to create typed new object: %v", err)
return newObj, managed, nil
return nil, nil, fmt.Errorf("failed to convert new object (%v) to smd typed: %v", newObjVersioned.GetObjectKind().GroupVersionKind(), err)
}
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned)
if err != nil {
// Return newObj and just by-pass fields update. This really shouldn't happen.
klog.Errorf("[SHOULD NOT HAPPEN] failed to create typed live object: %v", err)
return newObj, managed, nil
return nil, nil, fmt.Errorf("failed to convert live object (%v) to smd typed: %v", liveObjVersioned.GetObjectKind().GroupVersionKind(), err)
}
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
// TODO(apelisse) use the first return value when unions are implemented
self := "current-operation"
_, managedFields, err := f.updater.Update(liveObjTyped, newObjTyped, apiVersion, managed.Fields(), self)
_, managedFields, err := f.updater.Update(liveObjTyped, newObjTyped, apiVersion, managed.Fields(), manager)
if err != nil {
return nil, nil, fmt.Errorf("failed to update ManagedFields: %v", err)
}
managed = internal.NewManaged(managedFields, managed.Times())
// 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)
managed.Times()[manager] = &metav1.Time{Time: time.Now().UTC()}
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
}
}
return newObj, managed, nil
}
// Apply implements Manager.
func (f *structuredMergeManager) Apply(liveObj runtime.Object, patch []byte, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
patchObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal(patch, &patchObj.Object); err != nil {
return nil, nil, errors.NewBadRequest(fmt.Sprintf("error decoding YAML: %v", err))
}
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 patchObj.GetAPIVersion() != f.groupVersion.String() {
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",
patchObj.GetAPIVersion(), f.groupVersion.String()))
patchVersion, f.groupVersion))
}
if patchObj.GetManagedFields() != nil {
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(fmt.Sprintf("metadata.managedFields must be nil"))
}
@ -179,8 +156,9 @@ func (f *structuredMergeManager) Apply(liveObj runtime.Object, patch []byte, man
}
managed = internal.NewManaged(managedFields, managed.Times())
// Update the time in the managedFieldsEntry for this operation
managed.Times()[manager] = &metav1.Time{Time: time.Now().UTC()}
if newObjTyped == nil {
return nil, managed, nil
}
newObj, err := f.typeConverter.TypedToObject(newObjTyped)
if err != nil {

View file

@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -48,6 +49,7 @@ import (
"k8s.io/apiserver/pkg/util/dryrun"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utiltrace "k8s.io/utils/trace"
"sigs.k8s.io/yaml"
)
const (
@ -63,7 +65,7 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac
defer trace.LogIfLong(500 * time.Millisecond)
if isDryRun(req.URL) && !utilfeature.DefaultFeatureGate.Enabled(features.DryRun) {
scope.err(errors.NewBadRequest("the dryRun alpha feature is disabled"), w, req)
scope.err(errors.NewBadRequest("the dryRun feature is disabled"), w, req)
return
}
@ -321,9 +323,7 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r
}
if p.fieldManager != nil {
if objToUpdate, err = p.fieldManager.Update(currentObject, objToUpdate, managerOrUserAgent(p.options.FieldManager, p.userAgent)); err != nil {
return nil, fmt.Errorf("failed to update object (json PATCH for %v) managed fields: %v", p.kind, err)
}
objToUpdate = p.fieldManager.UpdateNoErrors(currentObject, objToUpdate, managerOrUserAgent(p.options.FieldManager, p.userAgent))
}
return objToUpdate, nil
}
@ -406,9 +406,7 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru
}
if p.fieldManager != nil {
if newObj, err = p.fieldManager.Update(currentObject, newObj, managerOrUserAgent(p.options.FieldManager, p.userAgent)); err != nil {
return nil, fmt.Errorf("failed to update object (smp PATCH for %v) managed fields: %v", p.kind, err)
}
newObj = p.fieldManager.UpdateNoErrors(currentObject, newObj, managerOrUserAgent(p.options.FieldManager, p.userAgent))
}
return newObj, nil
}
@ -433,7 +431,13 @@ func (p *applyPatcher) applyPatchToCurrentObject(obj runtime.Object) (runtime.Ob
if p.fieldManager == nil {
panic("FieldManager must be installed to run apply")
}
return p.fieldManager.Apply(obj, p.patch, p.options.FieldManager, force)
patchObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal(p.patch, &patchObj.Object); err != nil {
return nil, errors.NewBadRequest(fmt.Sprintf("error decoding YAML: %v", err))
}
return p.fieldManager.Apply(obj, patchObj, p.options.FieldManager, force)
}
func (p *applyPatcher) createNewObject() (runtime.Object, error) {
@ -573,12 +577,28 @@ func (p *patcher) patchResource(ctx context.Context, scope *RequestScope) (runti
wasCreated := false
p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil, p.applyPatch, p.applyAdmission)
result, err := finishRequest(p.timeout, func() (runtime.Object, error) {
requestFunc := func() (runtime.Object, error) {
// Pass in UpdateOptions to override UpdateStrategy.AllowUpdateOnCreate
options := patchToUpdateOptions(p.options)
updateObject, created, updateErr := p.restPatcher.Update(ctx, p.name, p.updatedObjectInfo, p.createValidation, p.updateValidation, p.forceAllowCreate, options)
wasCreated = created
return updateObject, updateErr
}
result, err := finishRequest(p.timeout, func() (runtime.Object, error) {
result, err := requestFunc()
// If the object wasn't committed to storage because it's serialized size was too large,
// it is safe to remove managedFields (which can be large) and try again.
if isTooLargeError(err) && p.patchType != types.ApplyPatchType {
if _, accessorErr := meta.Accessor(p.restPatcher.New()); accessorErr == nil {
p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil, p.applyPatch, p.applyAdmission, func(_ context.Context, obj, _ runtime.Object) (runtime.Object, error) {
accessor, _ := meta.Accessor(obj)
accessor.SetManagedFields(nil)
return obj, nil
})
result, err = requestFunc()
}
}
return result, err
})
return result, wasCreated, err
}

View file

@ -74,7 +74,7 @@ func doTransformObject(ctx context.Context, obj runtime.Object, opts interface{}
return asPartialObjectMetadataList(obj, target.GroupVersion())
case target.Kind == "Table":
options, ok := opts.(*metav1beta1.TableOptions)
options, ok := opts.(*metav1.TableOptions)
if !ok {
return nil, fmt.Errorf("unexpected TableOptions, got %T", opts)
}
@ -93,8 +93,8 @@ func optionsForTransform(mediaType negotiation.MediaTypeOptions, req *http.Reque
switch target := mediaType.Convert; {
case target == nil:
case target.Kind == "Table" && (target.GroupVersion() == metav1beta1.SchemeGroupVersion || target.GroupVersion() == metav1.SchemeGroupVersion):
opts := &metav1beta1.TableOptions{}
if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), metav1beta1.SchemeGroupVersion, opts); err != nil {
opts := &metav1.TableOptions{}
if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), metav1.SchemeGroupVersion, opts); err != nil {
return nil, err
}
switch errs := validation.ValidateTableOptions(opts); len(errs) {
@ -159,7 +159,7 @@ func (e errNotAcceptable) Status() metav1.Status {
}
}
func asTable(ctx context.Context, result runtime.Object, opts *metav1beta1.TableOptions, scope *RequestScope, groupVersion schema.GroupVersion) (runtime.Object, error) {
func asTable(ctx context.Context, result runtime.Object, opts *metav1.TableOptions, scope *RequestScope, groupVersion schema.GroupVersion) (runtime.Object, error) {
switch groupVersion {
case metav1beta1.SchemeGroupVersion, metav1.SchemeGroupVersion:
default:

View file

@ -25,8 +25,12 @@ import (
"net/http"
"net/url"
goruntime "runtime"
"strings"
"time"
grpccodes "google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -416,3 +420,28 @@ func parseTimeout(str string) time.Duration {
func isDryRun(url *url.URL) bool {
return len(url.Query()["dryRun"]) != 0
}
type etcdError interface {
Code() grpccodes.Code
Error() string
}
type grpcError interface {
GRPCStatus() *grpcstatus.Status
}
func isTooLargeError(err error) bool {
if err != nil {
if etcdErr, ok := err.(etcdError); ok {
if etcdErr.Code() == grpccodes.InvalidArgument && etcdErr.Error() == "etcdserver: request is too large" {
return true
}
}
if grpcErr, ok := err.(grpcError); ok {
if grpcErr.GRPCStatus().Code() == grpccodes.ResourceExhausted && strings.Contains(grpcErr.GRPCStatus().Message(), "trying to send message larger than max") {
return true
}
}
}
return false
}

View file

@ -24,6 +24,7 @@ import (
"time"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
@ -49,7 +50,7 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa
defer trace.LogIfLong(500 * time.Millisecond)
if isDryRun(req.URL) && !utilfeature.DefaultFeatureGate.Enabled(features.DryRun) {
scope.err(errors.NewBadRequest("the dryRun alpha feature is disabled"), w, req)
scope.err(errors.NewBadRequest("the dryRun feature is disabled"), w, req)
return
}
@ -124,15 +125,18 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa
userInfo, _ := request.UserFrom(ctx)
transformers := []rest.TransformFunc{}
// allows skipping managedFields update if the resulting object is too big
shouldUpdateManagedFields := true
if scope.FieldManager != nil {
transformers = append(transformers, func(_ context.Context, newObj, liveObj runtime.Object) (runtime.Object, error) {
obj, err := scope.FieldManager.Update(liveObj, newObj, managerOrUserAgent(options.FieldManager, req.UserAgent()))
if err != nil {
return nil, fmt.Errorf("failed to update object (Update for %v) managed fields: %v", scope.Kind, err)
if shouldUpdateManagedFields {
return scope.FieldManager.UpdateNoErrors(liveObj, newObj, managerOrUserAgent(options.FieldManager, req.UserAgent())), nil
}
return obj, nil
return newObj, nil
})
}
if mutatingAdmission, ok := admit.(admission.MutationInterface); ok {
transformers = append(transformers, func(ctx context.Context, newObj, oldObj runtime.Object) (runtime.Object, error) {
isNotZeroObject, err := hasUID(oldObj)
@ -149,7 +153,6 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa
}
return newObj, nil
})
}
createAuthorizerAttributes := authorizer.AttributesRecord{
@ -167,7 +170,7 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa
trace.Step("About to store object in database")
wasCreated := false
result, err := finishRequest(timeout, func() (runtime.Object, error) {
requestFunc := func() (runtime.Object, error) {
obj, created, err := r.Update(
ctx,
name,
@ -184,6 +187,19 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa
)
wasCreated = created
return obj, err
}
result, err := finishRequest(timeout, func() (runtime.Object, error) {
result, err := requestFunc()
// If the object wasn't committed to storage because it's serialized size was too large,
// it is safe to remove managedFields (which can be large) and try again.
if isTooLargeError(err) && scope.FieldManager != nil {
if accessor, accessorErr := meta.Accessor(obj); accessorErr == nil {
accessor.SetManagedFields(nil)
shouldUpdateManagedFields = false
result, err = requestFunc()
}
}
return result, err
})
if err != nil {
scope.err(err, w, req)

View file

@ -25,7 +25,6 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/streaming"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@ -64,6 +63,8 @@ func (w *realTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) {
// serveWatch will serve a watch response.
// TODO: the functionality in this method and in WatchServer.Serve is not cleanly decoupled.
func serveWatch(watcher watch.Interface, scope *RequestScope, mediaTypeOptions negotiation.MediaTypeOptions, req *http.Request, w http.ResponseWriter, timeout time.Duration) {
defer watcher.Stop()
options, err := optionsForTransform(mediaTypeOptions, req)
if err != nil {
scope.err(err, w, req)
@ -125,7 +126,7 @@ func serveWatch(watcher watch.Interface, scope *RequestScope, mediaTypeOptions n
// When we are transformed to a table, use the table options as the state for whether we
// should print headers - on watch, we only want to print table headers on the first object
// and omit them on subsequent events.
if tableOptions, ok := options.(*metav1beta1.TableOptions); ok {
if tableOptions, ok := options.(*metav1.TableOptions); ok {
tableOptions.NoHeaders = true
}
return result
@ -173,13 +174,6 @@ func (s *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
cn, ok := w.(http.CloseNotifier)
if !ok {
err := fmt.Errorf("unable to start watch - can't get http.CloseNotifier: %#v", w)
utilruntime.HandleError(err)
s.Scope.err(errors.NewInternalError(err), w, req)
return
}
flusher, ok := w.(http.Flusher)
if !ok {
err := fmt.Errorf("unable to start watch - can't get http.Flusher: %#v", w)
@ -201,7 +195,6 @@ func (s *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// ensure the connection times out
timeoutCh, cleanup := s.TimeoutFactory.TimeoutCh()
defer cleanup()
defer s.Watching.Stop()
// begin the stream
w.Header().Set("Content-Type", s.MediaType)
@ -214,9 +207,11 @@ func (s *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
outEvent := &metav1.WatchEvent{}
buf := &bytes.Buffer{}
ch := s.Watching.ResultChan()
done := req.Context().Done()
for {
select {
case <-cn.CloseNotify():
case <-done:
return
case <-timeoutCh:
return
@ -286,8 +281,6 @@ func (s *WatchServer) HandleWS(ws *websocket.Conn) {
streamBuf := &bytes.Buffer{}
ch := s.Watching.ResultChan()
defer s.Watching.Stop()
for {
select {
case <-done:

View file

@ -291,12 +291,17 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
var versionedDeleteOptions runtime.Object
var versionedDeleterObject interface{}
deleteReturnsDeletedObject := false
if isGracefulDeleter {
versionedDeleteOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind("DeleteOptions"))
if err != nil {
return nil, err
}
versionedDeleterObject = indirectArbitraryPointer(versionedDeleteOptions)
if mayReturnFullObjectDeleter, ok := storage.(rest.MayReturnFullObjectDeleter); ok {
deleteReturnsDeletedObject = mayReturnFullObjectDeleter.DeleteReturnsDeletedObject()
}
}
versionedStatusPtr, err := a.group.Creater.New(optionsExternalVersion.WithKind("Status"))
@ -769,15 +774,19 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
if isSubresource {
doc = "delete " + subresource + " of" + article + kind
}
deleteReturnType := versionedStatus
if deleteReturnsDeletedObject {
deleteReturnType = producedObject
}
handler := metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, restfulDeleteResource(gracefulDeleter, isGracefulDeleter, reqScope, admit))
route := ws.DELETE(action.Path).To(handler).
Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Operation("delete"+namespaced+kind+strings.Title(subresource)+operationSuffix).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
Writes(versionedStatus).
Returns(http.StatusOK, "OK", versionedStatus).
Returns(http.StatusAccepted, "Accepted", versionedStatus)
Writes(deleteReturnType).
Returns(http.StatusOK, "OK", deleteReturnType).
Returns(http.StatusAccepted, "Accepted", deleteReturnType)
if isGracefulDeleter {
route.Reads(versionedDeleterObject)
route.ParameterNamed("body").Required(false)

View file

@ -27,8 +27,7 @@ import (
"sync"
"time"
"github.com/emicklei/go-restful"
restful "github.com/emicklei/go-restful"
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/types"
utilsets "k8s.io/apimachinery/pkg/util/sets"
@ -48,6 +47,8 @@ type resettableCollector interface {
const (
APIServerComponent string = "apiserver"
OtherContentType string = "other"
OtherRequestMethod string = "other"
)
/*
@ -64,23 +65,14 @@ var (
requestCounter = compbasemetrics.NewCounterVec(
&compbasemetrics.CounterOpts{
Name: "apiserver_request_total",
Help: "Counter of apiserver requests broken out for each verb, dry run value, group, version, resource, scope, component, client, and HTTP response contentType and code.",
Help: "Counter of apiserver requests broken out for each verb, dry run value, group, version, resource, scope, component, and HTTP response contentType and code.",
StabilityLevel: compbasemetrics.ALPHA,
},
// The label_name contentType doesn't follow the label_name convention defined here:
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/instrumentation.md
// But changing it would break backwards compatibility. Future label_names
// should be all lowercase and separated by underscores.
[]string{"verb", "dry_run", "group", "version", "resource", "subresource", "scope", "component", "client", "contentType", "code"},
)
deprecatedRequestCounter = compbasemetrics.NewCounterVec(
&compbasemetrics.CounterOpts{
Name: "apiserver_request_count",
Help: "Counter of apiserver requests broken out for each verb, group, version, resource, scope, component, client, and HTTP response contentType and code.",
StabilityLevel: compbasemetrics.ALPHA,
DeprecatedVersion: "1.14.0",
},
[]string{"verb", "group", "version", "resource", "subresource", "scope", "component", "client", "contentType", "code"},
[]string{"verb", "dry_run", "group", "version", "resource", "subresource", "scope", "component", "contentType", "code"},
)
longRunningRequestGauge = compbasemetrics.NewGaugeVec(
&compbasemetrics.GaugeOpts{
@ -103,29 +95,6 @@ var (
},
[]string{"verb", "dry_run", "group", "version", "resource", "subresource", "scope", "component"},
)
deprecatedRequestLatencies = compbasemetrics.NewHistogramVec(
&compbasemetrics.HistogramOpts{
Name: "apiserver_request_latencies",
Help: "Response latency distribution in microseconds for each verb, group, version, resource, subresource, scope and component.",
// Use buckets ranging from 125 ms to 8 seconds.
Buckets: compbasemetrics.ExponentialBuckets(125000, 2.0, 7),
StabilityLevel: compbasemetrics.ALPHA,
DeprecatedVersion: "1.14.0",
},
[]string{"verb", "group", "version", "resource", "subresource", "scope", "component"},
)
deprecatedRequestLatenciesSummary = compbasemetrics.NewSummaryVec(
&compbasemetrics.SummaryOpts{
Name: "apiserver_request_latencies_summary",
Help: "Response latency summary in microseconds for each verb, group, version, resource, subresource, scope and component.",
// Make the sliding window of 5h.
// TODO: The value for this should be based on our SLI definition (medium term).
MaxAge: 5 * time.Hour,
StabilityLevel: compbasemetrics.ALPHA,
DeprecatedVersion: "1.14.0",
},
[]string{"verb", "group", "version", "resource", "subresource", "scope", "component"},
)
responseSizes = compbasemetrics.NewHistogramVec(
&compbasemetrics.HistogramOpts{
Name: "apiserver_response_sizes",
@ -145,15 +114,6 @@ var (
},
[]string{"requestKind"},
)
DeprecatedDroppedRequests = compbasemetrics.NewCounterVec(
&compbasemetrics.CounterOpts{
Name: "apiserver_dropped_requests",
Help: "Number of requests dropped with 'Try again later' response",
StabilityLevel: compbasemetrics.ALPHA,
DeprecatedVersion: "1.14.0",
},
[]string{"requestKind"},
)
// RegisteredWatchers is a number of currently registered watchers splitted by resource.
RegisteredWatchers = compbasemetrics.NewGaugeVec(
&compbasemetrics.GaugeOpts{
@ -203,20 +163,47 @@ var (
metrics = []resettableCollector{
requestCounter,
deprecatedRequestCounter,
longRunningRequestGauge,
requestLatencies,
deprecatedRequestLatencies,
deprecatedRequestLatenciesSummary,
responseSizes,
DroppedRequests,
DeprecatedDroppedRequests,
RegisteredWatchers,
WatchEvents,
WatchEventsSizes,
currentInflightRequests,
requestTerminationsTotal,
}
// these are the known (e.g. whitelisted/known) content types which we will report for
// request metrics. Any other RFC compliant content types will be aggregated under 'unknown'
knownMetricContentTypes = utilsets.NewString(
"application/apply-patch+yaml",
"application/json",
"application/json-patch+json",
"application/merge-patch+json",
"application/strategic-merge-patch+json",
"application/vnd.kubernetes.protobuf",
"application/vnd.kubernetes.protobuf;stream=watch",
"application/yaml",
"text/plain",
"text/plain;charset=utf-8")
// these are the valid request methods which we report in our metrics. Any other request methods
// will be aggregated under 'unknown'
validRequestMethods = utilsets.NewString(
"APPLY",
"CONNECT",
"CREATE",
"DELETE",
"DELETECOLLECTION",
"GET",
"LIST",
"PATCH",
"POST",
"PROXY",
"PUT",
"UPDATE",
"WATCH",
"WATCHLIST")
)
const (
@ -264,6 +251,10 @@ func RecordRequestTermination(req *http.Request, requestInfo *request.RequestInf
// translated to RequestInfo).
// However, we need to tweak it e.g. to differentiate GET from LIST.
verb := canonicalVerb(strings.ToUpper(req.Method), scope)
// set verbs to a bounded set of known and expected verbs
if !validRequestMethods.Has(verb) {
verb = OtherRequestMethod
}
if requestInfo.IsResourceRequest {
requestTerminationsTotal.WithLabelValues(cleanVerb(verb, req), requestInfo.APIGroup, requestInfo.APIVersion, requestInfo.Resource, requestInfo.Subresource, scope, component, codeToString(code)).Inc()
} else {
@ -300,15 +291,10 @@ func RecordLongRunning(req *http.Request, requestInfo *request.RequestInfo, comp
func MonitorRequest(req *http.Request, verb, group, version, resource, subresource, scope, component, contentType string, httpCode, respSize int, elapsed time.Duration) {
reportedVerb := cleanVerb(verb, req)
dryRun := cleanDryRun(req.URL)
// blank out client string here, in order to avoid cardinality issues
client := ""
elapsedMicroseconds := float64(elapsed / time.Microsecond)
elapsedSeconds := elapsed.Seconds()
requestCounter.WithLabelValues(reportedVerb, dryRun, group, version, resource, subresource, scope, component, client, contentType, codeToString(httpCode)).Inc()
deprecatedRequestCounter.WithLabelValues(reportedVerb, group, version, resource, subresource, scope, component, client, contentType, codeToString(httpCode)).Inc()
cleanContentType := cleanContentType(contentType)
requestCounter.WithLabelValues(reportedVerb, dryRun, group, version, resource, subresource, scope, component, cleanContentType, codeToString(httpCode)).Inc()
requestLatencies.WithLabelValues(reportedVerb, dryRun, group, version, resource, subresource, scope, component).Observe(elapsedSeconds)
deprecatedRequestLatencies.WithLabelValues(reportedVerb, group, version, resource, subresource, scope, component).Observe(elapsedMicroseconds)
deprecatedRequestLatenciesSummary.WithLabelValues(reportedVerb, group, version, resource, subresource, scope, component).Observe(elapsedMicroseconds)
// We are only interested in response sizes of read requests.
if verb == "GET" || verb == "LIST" {
responseSizes.WithLabelValues(reportedVerb, group, version, resource, subresource, scope, component).Observe(float64(respSize))
@ -362,6 +348,19 @@ func InstrumentHandlerFunc(verb, group, version, resource, subresource, scope, c
}
}
// cleanContentType binds the contentType (for metrics related purposes) to a
// bounded set of known/expected content-types.
func cleanContentType(contentType string) string {
normalizedContentType := strings.ToLower(contentType)
if strings.HasSuffix(contentType, " stream=watch") || strings.HasSuffix(contentType, " charset=utf-8") {
normalizedContentType = strings.ReplaceAll(contentType, " ", "")
}
if knownMetricContentTypes.Has(normalizedContentType) {
return normalizedContentType
}
return OtherContentType
}
// CleanScope returns the scope of the request.
func CleanScope(requestInfo *request.RequestInfo) string {
if requestInfo.Namespace != "" {
@ -406,7 +405,10 @@ func cleanVerb(verb string, request *http.Request) string {
if verb == "PATCH" && request.Header.Get("Content-Type") == string(types.ApplyPatchType) && utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
reportedVerb = "APPLY"
}
return reportedVerb
if validRequestMethods.Has(reportedVerb) {
return reportedVerb
}
return OtherRequestMethod
}
func cleanDryRun(u *url.URL) string {
@ -425,19 +427,6 @@ func cleanDryRun(u *url.URL) string {
return strings.Join(utilsets.NewString(dryRun...).List(), ",")
}
func cleanUserAgent(ua string) string {
// We collapse all "web browser"-type user agents into one "browser" to reduce metric cardinality.
if strings.HasPrefix(ua, "Mozilla/") {
return "Browser"
}
// If an old "kubectl.exe" has passed us its full path, we discard the path portion.
if kubectlExeRegexp.MatchString(ua) {
// avoid an allocation
ua = kubectlExeRegexp.ReplaceAllString(ua, "$1")
}
return ua
}
// ResponseWriterDelegator interface wraps http.ResponseWriter to additionally record content-length, status-code, etc.
type ResponseWriterDelegator struct {
http.ResponseWriter