cert-manager/pkg/internal/api/mutation/registry.go
joshvanl 4dd6d19011 Adds review comment suggestions/cleanup
Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
2021-03-11 19:12:02 +00:00

258 lines
8.1 KiB
Go

/*
Copyright 2020 The cert-manager 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 mutation allows a caller to automatically register, lookup and call
// API mutation functions.
// It is similar to runtime.Scheme and is designed to make writing and
// consuming API mutation functions easier.
// This registry also handles adding scheme defaults, even if no mutation
// functions are defined for that type. Any type where defaulting is desired
// should be registered.
// Functions are designed to update the incoming object, or new object, which
// will then have a patch generated and returned to the master Mutation.
package mutation
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"github.com/mattbaird/jsonpatch"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
apijson "k8s.io/apimachinery/pkg/runtime/serializer/json"
)
// Registry is used to store and lookup references to mutation functions for
// given Kubernetes API types. API types will be converted into internal API
// versions during mutation, then converted back to the target version when
// generating the patch.
type Registry struct {
codec runtime.Codec
scheme *runtime.Scheme
mutateRegister map[schema.GroupVersionKind]MutateFunc
mutateUpdateRegister map[schema.GroupVersionKind]MutateUpdateFunc
}
type MutateFunc func(req *admissionv1.AdmissionRequest, obj runtime.Object)
type MutateUpdateFunc func(req *admissionv1.AdmissionRequest, old, new runtime.Object)
// NewRegistry creates a new empty registry, backed by the provided Scheme.
func NewRegistry(scheme *runtime.Scheme) *Registry {
factory := serializer.NewCodecFactory(scheme)
serializer := apijson.NewSerializerWithOptions(apijson.DefaultMetaFactory, scheme, scheme, apijson.SerializerOptions{})
encoder := factory.WithoutConversion().EncoderForVersion(serializer, nil)
decoder := factory.UniversalDeserializer()
return &Registry{
codec: runtime.NewCodec(encoder, decoder),
scheme: scheme,
mutateRegister: make(map[schema.GroupVersionKind]MutateFunc),
mutateUpdateRegister: make(map[schema.GroupVersionKind]MutateUpdateFunc),
}
}
// AddMutateFunc will add a new mutation function to the register. The function
// will be run whenever a Mutate is called with a CREATE operation, and API
// type whose internal version is registered. Registered types MUST be that of the
// internal version of the target resource kind.
func (r *Registry) AddMutateFunc(obj runtime.Object, fn MutateFunc) error {
gvks, _, err := r.scheme.ObjectKinds(obj)
if err != nil {
return err
}
for _, gvk := range gvks {
r.appendMutate(gvk, fn)
}
return nil
}
// AddMutateFunc will add a new mutation function to the register. The function
// will be run whenever a Mutate is called with an UPDATE operation, and API
// type whose internal version is registered. Registered types MUST be that of the
// internal version of the target resource kind.
func (r *Registry) AddMutateUpdateFunc(obj runtime.Object, fn MutateUpdateFunc) error {
gvks, _, err := r.scheme.ObjectKinds(obj)
if err != nil {
return err
}
for _, gvk := range gvks {
r.appendMutateUpdate(gvk, fn)
}
return nil
}
// Mutate will run all mutation functions registed on CREATE and UPDATE
// operations over the internal type of the given resource.
// The object is converted to its internal version before either a CREATE or
// UPDATE mutation is applied to the object. The object is then converted to
// the requested version, and defaults applied for that schema.
// A JSON patch is then generated for the target resource version.
// Defaulting is always applied against the given resource, regardless of
// whether any mutation functions are defined.
func (r *Registry) Mutate(req *admissionv1.AdmissionRequest) ([]byte, error) {
// Create GroupVersionKind where the Version is set to internal.
gvk := schema.GroupVersionKind{
Group: req.RequestKind.Group,
// Set version to internal API version
Version: runtime.APIVersionInternal,
Kind: req.RequestKind.Kind,
}
// Convert the incoming resource to the internal type
internal, err := r.convert(req.Object.Raw, gvk)
if err != nil {
return nil, fmt.Errorf("failed to convert object: %s", err)
}
switch req.Operation {
case admissionv1.Create:
// Attempt to retrieve the registered CREATE mutating functions, and apply
// to the internal type.
mutate := r.mutateRegister[gvk]
if mutate == nil {
break
}
mutate(req, internal)
break
case admissionv1.Update:
// Attempt to retrieve the registered UPDATE mutating functions, and apply
// to the internal type.
// decode the old raw object data
oldInternal, err := r.convert(req.OldObject.Raw, gvk)
if err != nil {
return nil, fmt.Errorf("failed to decode old admission object: %s", err)
}
mutate := r.mutateUpdateRegister[gvk]
if mutate == nil {
break
}
// Pass both the old and new internal types to mutate
mutate(req, oldInternal, internal)
break
default:
// If not under a CREATE or UPDATE operation, exit early
return nil, nil
}
// Convert the mutated internal object into the target resource version.
target, err := r.scheme.New(schema.GroupVersionKind{
Group: req.RequestKind.Group,
Version: req.RequestKind.Version,
Kind: req.RequestKind.Kind,
})
if err != nil {
return nil, err
}
if err := r.scheme.Convert(internal, target, nil); err != nil {
return nil, err
}
// Apply defaults to the target resource.
r.scheme.Default(target)
// Generate a JSON patch based on the incoming resource and the mutated object.
return r.createMutatePatch(req, target)
}
// convert converts a raw resource byte slice to an internal versioned resource, based on the given GroupKind.
func (r *Registry) convert(rawObj []byte, gvk schema.GroupVersionKind) (runtime.Object, error) {
gvk.Version = runtime.APIVersionInternal
obj, _, err := r.codec.Decode(rawObj, nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to decode admission object: %s", err)
}
targetObj, err := r.scheme.New(gvk)
if err != nil {
return nil, err
}
if err := r.scheme.Convert(obj, targetObj, nil); err != nil {
return nil, err
}
return targetObj, nil
}
// createMutatePatch will generate a JSON patch based upon the given original
// raw object, and the mutated typed object.
func (r *Registry) createMutatePatch(req *admissionv1.AdmissionRequest, obj runtime.Object) ([]byte, error) {
var buf bytes.Buffer
if err := r.codec.Encode(obj, &buf); err != nil {
return nil, fmt.Errorf("failed to encode object after mutation: %s", err)
}
ops, err := jsonpatch.CreatePatch(req.Object.Raw, buf.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to set mutation patch: %s", err)
}
sortOps(ops)
patch, err := json.Marshal(ops)
if err != nil {
return nil, fmt.Errorf("failed to generate json patch: %s", err)
}
return patch, nil
}
func sortOps(ops []jsonpatch.JsonPatchOperation) {
sort.Slice(ops, func(i, j int) bool {
return ops[i].Path < ops[j].Path
})
}
func (r *Registry) appendMutate(gvk schema.GroupVersionKind, fn MutateFunc) {
existing, ok := r.mutateRegister[gvk]
if !ok {
r.mutateRegister[gvk] = fn
return
}
r.mutateRegister[gvk] = func(aspec *admissionv1.AdmissionRequest, obj runtime.Object) {
existing(aspec, obj)
fn(aspec, obj)
}
}
func (r *Registry) appendMutateUpdate(gvk schema.GroupVersionKind, fn MutateUpdateFunc) {
existing, ok := r.mutateUpdateRegister[gvk]
if !ok {
r.mutateUpdateRegister[gvk] = fn
return
}
r.mutateUpdateRegister[gvk] = func(aspec *admissionv1.AdmissionRequest, oldObj, newObj runtime.Object) {
existing(aspec, oldObj, newObj)
fn(aspec, oldObj, newObj)
}
}