mirror of https://github.com/k3s-io/k3s
322 lines
12 KiB
Go
322 lines
12 KiB
Go
|
/*
|
||
|
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 ensurer
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strconv"
|
||
|
|
||
|
flowcontrolv1beta1 "k8s.io/api/flowcontrol/v1beta1"
|
||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||
|
"k8s.io/apimachinery/pkg/runtime"
|
||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||
|
"k8s.io/klog/v2"
|
||
|
|
||
|
"github.com/google/go-cmp/cmp"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
fieldManager = "api-priority-and-fairness-config-producer-v1"
|
||
|
)
|
||
|
|
||
|
// ensureStrategy provides a strategy for ensuring apf bootstrap configurationWrapper.
|
||
|
// We have two types of configurationWrapper objects:
|
||
|
// - mandatory: the mandatory configurationWrapper objects are about ensuring that the P&F
|
||
|
// system itself won't crash; we have to be sure there's 'catch-all' place for
|
||
|
// everything to go. Any changes made by the cluster operators to these
|
||
|
// configurationWrapper objects will be stomped by the apiserver.
|
||
|
//
|
||
|
// - suggested: additional configurationWrapper objects for initial behavior.
|
||
|
// the cluster operators have an option to edit or delete these configurationWrapper objects.
|
||
|
type ensureStrategy interface {
|
||
|
// Name of the strategy, for now we have two: 'mandatory' and 'suggested'.
|
||
|
// This comes handy in logging.
|
||
|
Name() string
|
||
|
|
||
|
// ShouldUpdate accepts the current and the bootstrap configuration and determines
|
||
|
// whether an update is necessary.
|
||
|
// current is the existing in-cluster configuration object.
|
||
|
// bootstrap is the configuration the kube-apiserver maintains in-memory.
|
||
|
//
|
||
|
// ok: true if auto update is required, otherwise false
|
||
|
// object: the new object represents the new configuration to be stored in-cluster.
|
||
|
// err: err is set when the function runs into an error and can not
|
||
|
// determine if auto update is needed.
|
||
|
ShouldUpdate(current, bootstrap configurationObject) (object runtime.Object, ok bool, err error)
|
||
|
}
|
||
|
|
||
|
// this internal interface provides abstraction for dealing with the `Spec`
|
||
|
// of both 'FlowSchema' and 'PriorityLevelConfiguration' objects.
|
||
|
// Since the ensure logic for both types is common, we use a few internal interfaces
|
||
|
// to abstract out the differences of these two types.
|
||
|
type specCopier interface {
|
||
|
// HasSpecChanged returns true if the spec of both the bootstrap and
|
||
|
// the current configuration object is same, otherwise false.
|
||
|
HasSpecChanged(bootstrap, current runtime.Object) (bool, error)
|
||
|
|
||
|
// CopySpec makes a deep copy the spec of the bootstrap object
|
||
|
// and copies it to that of the current object.
|
||
|
// CopySpec assumes that the current object is safe to mutate, so it
|
||
|
// rests with the caller to make a deep copy of the current.
|
||
|
CopySpec(bootstrap, current runtime.Object) error
|
||
|
}
|
||
|
|
||
|
// this internal interface provides abstraction for CRUD operation
|
||
|
// related to both 'FlowSchema' and 'PriorityLevelConfiguration' objects.
|
||
|
// Since the ensure logic for both types is common, we use a few internal interfaces
|
||
|
// to abstract out the differences of these two types.
|
||
|
type configurationClient interface {
|
||
|
Create(object runtime.Object) (runtime.Object, error)
|
||
|
Update(object runtime.Object) (runtime.Object, error)
|
||
|
Get(name string) (configurationObject, error)
|
||
|
Delete(name string) error
|
||
|
}
|
||
|
|
||
|
type configurationWrapper interface {
|
||
|
// TypeName returns the type of the configuration that this interface deals with.
|
||
|
// We use it to log the type name of the configuration object being ensured.
|
||
|
// It is either 'PriorityLevelConfiguration' or 'FlowSchema'
|
||
|
TypeName() string
|
||
|
|
||
|
configurationClient
|
||
|
specCopier
|
||
|
}
|
||
|
|
||
|
// A convenient wrapper interface that is used by the ensure logic.
|
||
|
type configurationObject interface {
|
||
|
metav1.Object
|
||
|
runtime.Object
|
||
|
}
|
||
|
|
||
|
func newSuggestedEnsureStrategy(copier specCopier) ensureStrategy {
|
||
|
return &strategy{
|
||
|
copier: copier,
|
||
|
alwaysAutoUpdateSpec: false,
|
||
|
name: "suggested",
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func newMandatoryEnsureStrategy(copier specCopier) ensureStrategy {
|
||
|
return &strategy{
|
||
|
copier: copier,
|
||
|
alwaysAutoUpdateSpec: true,
|
||
|
name: "mandatory",
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// auto-update strategy for the configuration objects
|
||
|
type strategy struct {
|
||
|
copier specCopier
|
||
|
alwaysAutoUpdateSpec bool
|
||
|
name string
|
||
|
}
|
||
|
|
||
|
func (s *strategy) Name() string {
|
||
|
return s.name
|
||
|
}
|
||
|
|
||
|
func (s *strategy) ShouldUpdate(current, bootstrap configurationObject) (runtime.Object, bool, error) {
|
||
|
if current == nil || bootstrap == nil {
|
||
|
return nil, false, nil
|
||
|
}
|
||
|
|
||
|
autoUpdateSpec := s.alwaysAutoUpdateSpec
|
||
|
if !autoUpdateSpec {
|
||
|
autoUpdateSpec = shouldUpdateSpec(current)
|
||
|
}
|
||
|
updateAnnotation := shouldUpdateAnnotation(current, autoUpdateSpec)
|
||
|
|
||
|
var specChanged bool
|
||
|
if autoUpdateSpec {
|
||
|
changed, err := s.copier.HasSpecChanged(bootstrap, current)
|
||
|
if err != nil {
|
||
|
return nil, false, fmt.Errorf("failed to compare spec - %w", err)
|
||
|
}
|
||
|
specChanged = changed
|
||
|
}
|
||
|
|
||
|
if !(updateAnnotation || specChanged) {
|
||
|
// the annotation key is up to date and the spec has not changed, no update is necessary
|
||
|
return nil, false, nil
|
||
|
}
|
||
|
|
||
|
// if we are here, either we need to update the annotation key or the spec.
|
||
|
copy, ok := current.DeepCopyObject().(configurationObject)
|
||
|
if !ok {
|
||
|
// we should never be here
|
||
|
return nil, false, errors.New("incompatible object type")
|
||
|
}
|
||
|
|
||
|
if updateAnnotation {
|
||
|
setAutoUpdateAnnotation(copy, autoUpdateSpec)
|
||
|
}
|
||
|
if specChanged {
|
||
|
s.copier.CopySpec(bootstrap, copy)
|
||
|
}
|
||
|
|
||
|
return copy, true, nil
|
||
|
}
|
||
|
|
||
|
// shouldUpdateSpec inspects the auto-update annotation key and generation field to determine
|
||
|
// whether the configurationWrapper object should be auto-updated.
|
||
|
func shouldUpdateSpec(accessor metav1.Object) bool {
|
||
|
value, _ := accessor.GetAnnotations()[flowcontrolv1beta1.AutoUpdateAnnotationKey]
|
||
|
if autoUpdate, err := strconv.ParseBool(value); err == nil {
|
||
|
return autoUpdate
|
||
|
}
|
||
|
|
||
|
// We are here because of either a or b:
|
||
|
// a. the annotation key is missing.
|
||
|
// b. the annotation key is present but the value does not represent a boolean.
|
||
|
// In either case, if the operator hasn't changed the spec, we can safely auto update.
|
||
|
// Please note that we can't protect the changes made by the operator in the following scenario:
|
||
|
// - The operator deletes and recreates the same object with a variant spec (generation resets to 1).
|
||
|
if accessor.GetGeneration() == 1 {
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// shouldUpdateAnnotation determines whether the current value of the auto-update annotation
|
||
|
// key matches the desired value.
|
||
|
func shouldUpdateAnnotation(accessor metav1.Object, desired bool) bool {
|
||
|
if value, ok := accessor.GetAnnotations()[flowcontrolv1beta1.AutoUpdateAnnotationKey]; ok {
|
||
|
if current, err := strconv.ParseBool(value); err == nil && current == desired {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// setAutoUpdateAnnotation sets the auto-update annotation key to the specified value.
|
||
|
func setAutoUpdateAnnotation(accessor metav1.Object, autoUpdate bool) {
|
||
|
if accessor.GetAnnotations() == nil {
|
||
|
accessor.SetAnnotations(map[string]string{})
|
||
|
}
|
||
|
|
||
|
accessor.GetAnnotations()[flowcontrolv1beta1.AutoUpdateAnnotationKey] = strconv.FormatBool(autoUpdate)
|
||
|
}
|
||
|
|
||
|
// ensureConfiguration ensures the boostrap configurationWrapper on the cluster based on the specified strategy.
|
||
|
func ensureConfiguration(wrapper configurationWrapper, strategy ensureStrategy, bootstrap configurationObject) error {
|
||
|
name := bootstrap.GetName()
|
||
|
configurationType := strategy.Name()
|
||
|
|
||
|
current, err := wrapper.Get(bootstrap.GetName())
|
||
|
if err != nil {
|
||
|
if !apierrors.IsNotFound(err) {
|
||
|
return fmt.Errorf("failed to retrieve %s type=%s name=%q error=%w", wrapper.TypeName(), configurationType, name, err)
|
||
|
}
|
||
|
|
||
|
// we always re-create a missing configuration object
|
||
|
if _, err := wrapper.Create(bootstrap); err != nil {
|
||
|
return fmt.Errorf("cannot create %s type=%s name=%q error=%w", wrapper.TypeName(), configurationType, name, err)
|
||
|
}
|
||
|
|
||
|
klog.V(2).InfoS(fmt.Sprintf("Successfully created %s", wrapper.TypeName()), "type", configurationType, "name", name)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
klog.V(5).InfoS(fmt.Sprintf("The %s already exists, checking whether it is up to date", wrapper.TypeName()), "type", configurationType, "name", name)
|
||
|
newObject, update, err := strategy.ShouldUpdate(current, bootstrap)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("failed to determine whether auto-update is required for %s type=%s name=%q error=%w", wrapper.TypeName(), configurationType, name, err)
|
||
|
}
|
||
|
if !update {
|
||
|
if klog.V(5).Enabled() {
|
||
|
// TODO: if we use structured logging here the diff gets escaped and very awkward to read in the log
|
||
|
klog.Infof("No update required for the %s type=%s name=%q diff: %s", wrapper.TypeName(), configurationType, name, cmp.Diff(current, bootstrap))
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
if _, err := wrapper.Update(newObject); err != nil {
|
||
|
return fmt.Errorf("failed to update the %s, will retry later type=%s name=%q error=%w", wrapper.TypeName(), configurationType, name, err)
|
||
|
}
|
||
|
|
||
|
klog.V(2).Infof("Updated the %s type=%s name=%q diff: %s", wrapper.TypeName(), configurationType, name, cmp.Diff(current, newObject))
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func removeConfiguration(wrapper configurationWrapper, name string) error {
|
||
|
current, err := wrapper.Get(name)
|
||
|
if err != nil {
|
||
|
if apierrors.IsNotFound(err) {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
return fmt.Errorf("failed to retrieve the %s, will retry later name=%q error=%w", wrapper.TypeName(), name, err)
|
||
|
}
|
||
|
|
||
|
value := current.GetAnnotations()[flowcontrolv1beta1.AutoUpdateAnnotationKey]
|
||
|
autoUpdate, err := strconv.ParseBool(value)
|
||
|
if err != nil {
|
||
|
klog.ErrorS(err, fmt.Sprintf("Skipping deletion of the %s", wrapper.TypeName()), "name", name)
|
||
|
|
||
|
// This may need manual intervention, in case the annotation value is malformed,
|
||
|
// so don't return an error, that might trigger futile retry loop.
|
||
|
return nil
|
||
|
}
|
||
|
if !autoUpdate {
|
||
|
klog.V(5).InfoS(fmt.Sprintf("Skipping deletion of the %s", wrapper.TypeName()), "name", name)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
if err := wrapper.Delete(name); err != nil {
|
||
|
if apierrors.IsNotFound(err) {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
return fmt.Errorf("failed to delete the %s, will retry later name=%q error=%w", wrapper.TypeName(), name, err)
|
||
|
}
|
||
|
|
||
|
klog.V(2).InfoS(fmt.Sprintf("Successfully deleted the %s", wrapper.TypeName()), "name", name)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// getRemoveCandidate returns a list of configuration objects we should delete
|
||
|
// from the cluster given a set of bootstrap and current configuration.
|
||
|
// bootstrap: a set of hard coded configuration kube-apiserver maintains in-memory.
|
||
|
// current: a set of configuration objects that exist on the cluster
|
||
|
// Any object present in current is a candidate for removal if both a and b are true:
|
||
|
// a. the object in current is missing from the bootstrap configuration
|
||
|
// b. the object has the designated auto-update annotation key
|
||
|
// This function shares the common logic for both FlowSchema and PriorityLevelConfiguration
|
||
|
// type and hence it accepts metav1.Object only.
|
||
|
func getRemoveCandidate(bootstrap sets.String, current []metav1.Object) []string {
|
||
|
if len(current) == 0 {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
candidates := make([]string, 0)
|
||
|
for i := range current {
|
||
|
object := current[i]
|
||
|
if _, ok := object.GetAnnotations()[flowcontrolv1beta1.AutoUpdateAnnotationKey]; !ok {
|
||
|
// the configuration object does not have the annotation key
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if _, ok := bootstrap[object.GetName()]; !ok {
|
||
|
candidates = append(candidates, object.GetName())
|
||
|
}
|
||
|
}
|
||
|
return candidates
|
||
|
}
|