mirror of https://github.com/k3s-io/k3s
265 lines
9.4 KiB
Go
265 lines
9.4 KiB
Go
/*
|
|
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 runtimeclass contains an admission controller for modifying and validating new Pods to
|
|
// take RuntimeClass into account. For RuntimeClass definitions which describe an overhead associated
|
|
// with running a pod, this admission controller will set the pod.Spec.Overhead field accordingly. This
|
|
// field should only be set through this controller, so validation will be carried out to ensure the pod's
|
|
// value matches what is defined in the coresponding RuntimeClass.
|
|
package runtimeclass
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
|
|
nodev1 "k8s.io/api/node/v1"
|
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apiserver/pkg/admission"
|
|
genericadmissioninitailizer "k8s.io/apiserver/pkg/admission/initializer"
|
|
"k8s.io/client-go/informers"
|
|
"k8s.io/client-go/kubernetes"
|
|
nodev1client "k8s.io/client-go/kubernetes/typed/node/v1"
|
|
nodev1listers "k8s.io/client-go/listers/node/v1"
|
|
"k8s.io/component-base/featuregate"
|
|
api "k8s.io/kubernetes/pkg/apis/core"
|
|
node "k8s.io/kubernetes/pkg/apis/node"
|
|
apinodev1 "k8s.io/kubernetes/pkg/apis/node/v1"
|
|
"k8s.io/kubernetes/pkg/features"
|
|
"k8s.io/kubernetes/pkg/util/tolerations"
|
|
)
|
|
|
|
// PluginName indicates name of admission plugin.
|
|
const PluginName = "RuntimeClass"
|
|
|
|
// Register registers a plugin
|
|
func Register(plugins *admission.Plugins) {
|
|
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
|
return NewRuntimeClass(), nil
|
|
})
|
|
}
|
|
|
|
// RuntimeClass is an implementation of admission.Interface.
|
|
// It looks at all new pods and sets pod.Spec.Overhead if a RuntimeClass is specified which
|
|
// defines an Overhead. If pod.Spec.Overhead is set but a RuntimeClass with matching overhead is
|
|
// not specified, the pod is rejected.
|
|
type RuntimeClass struct {
|
|
*admission.Handler
|
|
runtimeClassLister nodev1listers.RuntimeClassLister
|
|
runtimeClassClient nodev1client.RuntimeClassInterface
|
|
|
|
inspectedFeatures bool
|
|
podOverheadEnabled bool
|
|
}
|
|
|
|
var _ admission.MutationInterface = &RuntimeClass{}
|
|
var _ admission.ValidationInterface = &RuntimeClass{}
|
|
|
|
var _ genericadmissioninitailizer.WantsExternalKubeInformerFactory = &RuntimeClass{}
|
|
var _ genericadmissioninitailizer.WantsExternalKubeClientSet = &RuntimeClass{}
|
|
|
|
// SetExternalKubeClientSet sets the client for the plugin
|
|
func (r *RuntimeClass) SetExternalKubeClientSet(client kubernetes.Interface) {
|
|
r.runtimeClassClient = client.NodeV1().RuntimeClasses()
|
|
}
|
|
|
|
// InspectFeatureGates allows setting bools without taking a dep on a global variable
|
|
func (r *RuntimeClass) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
|
r.podOverheadEnabled = featureGates.Enabled(features.PodOverhead)
|
|
r.inspectedFeatures = true
|
|
}
|
|
|
|
// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
|
|
func (r *RuntimeClass) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
|
runtimeClassInformer := f.Node().V1().RuntimeClasses()
|
|
r.SetReadyFunc(runtimeClassInformer.Informer().HasSynced)
|
|
r.runtimeClassLister = runtimeClassInformer.Lister()
|
|
}
|
|
|
|
// ValidateInitialization implements the WantsExternalKubeInformerFactory interface.
|
|
func (r *RuntimeClass) ValidateInitialization() error {
|
|
if !r.inspectedFeatures {
|
|
return fmt.Errorf("InspectFeatureGates was not called")
|
|
}
|
|
if r.runtimeClassLister == nil {
|
|
return fmt.Errorf("missing RuntimeClass lister")
|
|
}
|
|
if r.runtimeClassClient == nil {
|
|
return fmt.Errorf("missing RuntimeClass client")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Admit makes an admission decision based on the request attributes
|
|
func (r *RuntimeClass) Admit(ctx context.Context, attributes admission.Attributes, o admission.ObjectInterfaces) error {
|
|
// Ignore all calls to subresources or resources other than pods.
|
|
if shouldIgnore(attributes) {
|
|
return nil
|
|
}
|
|
|
|
pod, runtimeClass, err := r.prepareObjects(ctx, attributes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if r.podOverheadEnabled {
|
|
if err := setOverhead(attributes, pod, runtimeClass); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := setScheduling(attributes, pod, runtimeClass); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Validate makes sure that pod adhere's to RuntimeClass's definition
|
|
func (r *RuntimeClass) Validate(ctx context.Context, attributes admission.Attributes, o admission.ObjectInterfaces) error {
|
|
// Ignore all calls to subresources or resources other than pods.
|
|
if shouldIgnore(attributes) {
|
|
return nil
|
|
}
|
|
|
|
pod, runtimeClass, err := r.prepareObjects(ctx, attributes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if r.podOverheadEnabled {
|
|
if err := validateOverhead(attributes, pod, runtimeClass); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewRuntimeClass creates a new RuntimeClass admission control handler
|
|
func NewRuntimeClass() *RuntimeClass {
|
|
return &RuntimeClass{
|
|
Handler: admission.NewHandler(admission.Create),
|
|
}
|
|
}
|
|
|
|
// prepareObjects returns pod and runtimeClass types from the given admission attributes
|
|
func (r *RuntimeClass) prepareObjects(ctx context.Context, attributes admission.Attributes) (pod *api.Pod, runtimeClass *nodev1.RuntimeClass, err error) {
|
|
pod, ok := attributes.GetObject().(*api.Pod)
|
|
if !ok {
|
|
return nil, nil, apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
|
|
}
|
|
|
|
if pod.Spec.RuntimeClassName == nil {
|
|
return pod, nil, nil
|
|
}
|
|
|
|
// get RuntimeClass object
|
|
runtimeClass, err = r.runtimeClassLister.Get(*pod.Spec.RuntimeClassName)
|
|
if apierrors.IsNotFound(err) {
|
|
// if not found, our informer cache could be lagging, do a live lookup
|
|
runtimeClass, err = r.runtimeClassClient.Get(ctx, *pod.Spec.RuntimeClassName, metav1.GetOptions{})
|
|
if apierrors.IsNotFound(err) {
|
|
return pod, nil, admission.NewForbidden(attributes, fmt.Errorf("pod rejected: RuntimeClass %q not found", *pod.Spec.RuntimeClassName))
|
|
}
|
|
}
|
|
|
|
// return the pod and runtimeClass.
|
|
return pod, runtimeClass, err
|
|
}
|
|
|
|
func setOverhead(a admission.Attributes, pod *api.Pod, runtimeClass *nodev1.RuntimeClass) (err error) {
|
|
if runtimeClass == nil || runtimeClass.Overhead == nil {
|
|
return nil
|
|
}
|
|
|
|
// convert to internal type and assign to pod's Overhead
|
|
nodeOverhead := &node.Overhead{}
|
|
if err = apinodev1.Convert_v1_Overhead_To_node_Overhead(runtimeClass.Overhead, nodeOverhead, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
// reject pod if Overhead is already set that differs from what is defined in RuntimeClass
|
|
if pod.Spec.Overhead != nil && !apiequality.Semantic.DeepEqual(nodeOverhead.PodFixed, pod.Spec.Overhead) {
|
|
return admission.NewForbidden(a, fmt.Errorf("pod rejected: Pod's Overhead doesn't match RuntimeClass's defined Overhead"))
|
|
}
|
|
|
|
pod.Spec.Overhead = nodeOverhead.PodFixed
|
|
|
|
return nil
|
|
}
|
|
|
|
func setScheduling(a admission.Attributes, pod *api.Pod, runtimeClass *nodev1.RuntimeClass) (err error) {
|
|
if runtimeClass == nil || runtimeClass.Scheduling == nil {
|
|
return nil
|
|
}
|
|
|
|
// convert to internal type and assign to pod's Scheduling
|
|
nodeScheduling := &node.Scheduling{}
|
|
if err = apinodev1.Convert_v1_Scheduling_To_node_Scheduling(runtimeClass.Scheduling, nodeScheduling, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
runtimeNodeSelector := nodeScheduling.NodeSelector
|
|
newNodeSelector := pod.Spec.NodeSelector
|
|
if newNodeSelector == nil {
|
|
newNodeSelector = runtimeNodeSelector
|
|
} else {
|
|
for key, runtimeClassValue := range runtimeNodeSelector {
|
|
if podValue, ok := newNodeSelector[key]; ok && podValue != runtimeClassValue {
|
|
return admission.NewForbidden(a, fmt.Errorf("conflict: runtimeClass.scheduling.nodeSelector[%s] = %s; pod.spec.nodeSelector[%s] = %s", key, runtimeClassValue, key, podValue))
|
|
}
|
|
newNodeSelector[key] = runtimeClassValue
|
|
}
|
|
}
|
|
|
|
newTolerations := tolerations.MergeTolerations(pod.Spec.Tolerations, nodeScheduling.Tolerations)
|
|
|
|
pod.Spec.NodeSelector = newNodeSelector
|
|
pod.Spec.Tolerations = newTolerations
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateOverhead(a admission.Attributes, pod *api.Pod, runtimeClass *nodev1.RuntimeClass) (err error) {
|
|
if runtimeClass != nil && runtimeClass.Overhead != nil {
|
|
// If the Overhead set doesn't match what is provided in the RuntimeClass definition, reject the pod
|
|
nodeOverhead := &node.Overhead{}
|
|
if err := apinodev1.Convert_v1_Overhead_To_node_Overhead(runtimeClass.Overhead, nodeOverhead, nil); err != nil {
|
|
return err
|
|
}
|
|
if !apiequality.Semantic.DeepEqual(nodeOverhead.PodFixed, pod.Spec.Overhead) {
|
|
return admission.NewForbidden(a, fmt.Errorf("pod rejected: Pod's Overhead doesn't match RuntimeClass's defined Overhead"))
|
|
}
|
|
} else {
|
|
// If RuntimeClass with Overhead is not defined but an Overhead is set for pod, reject the pod
|
|
if pod.Spec.Overhead != nil {
|
|
return admission.NewForbidden(a, fmt.Errorf("pod rejected: Pod Overhead set without corresponding RuntimeClass defined Overhead"))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func shouldIgnore(attributes admission.Attributes) bool {
|
|
// Ignore all calls to subresources or resources other than pods.
|
|
if len(attributes.GetSubresource()) != 0 || attributes.GetResource().GroupResource() != api.Resource("pods") {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|