Implement autobackup-operator controllers, tests, CI and packaging
PVCs and CloudNativePG Clusters need S3 buckets and backup schedules
provisioned consistently. This operator watches the
backups.unkin.net/{schedule,destination} annotations on those objects and
provisions everything needed to back them up, with no new CRDs.
- Add a PVC controller that provisions cephrgw ObjectStoreUser/Bucket/BucketAccess,
auto-generates a restic repo-password Secret and creates a k8up Schedule scoped
to the PVC via spec.backup.volumes[].persistentVolumeClaim.claimName.
- Add a CNPG Cluster controller that provisions the same bucket stack, idempotently
patches spec.backup.barmanObjectStore (leaving a user-set destinationPath alone
with a Warning event) and creates a ScheduledBackup.
- Resolve destinations through a ConfigMap lookup table; requeue until the
BucketAccess is Ready before creating schedule resources; own-reference created
resources and retain bucket data by default.
- Add schedule-mapping helpers (k8up 5-field/shortcut pass-through, CNPG 6-field
seconds-first) and deterministic, length-bounded name derivation.
- Add unit tests (schedule mapping, name derivation, destination resolution) and
envtest controller tests for both paths, wiring the external CRDs into envtest.
- Add kubebuilder-generated RBAC, a Dockerfile (distroless/nonroot), Woodpecker
lint/test/build pipelines and a tag-triggered image push to the artifactapi
docker-internal registry, plus a version-bump Makefile and deploy manifests.
This commit is contained in:
+365
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
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 conversion provides implementation for CRD conversion webhook that implements handler for version conversion requests for types that are convertible.
|
||||
|
||||
See pkg/conversion for interface definitions required to ensure an API Type is convertible.
|
||||
*/
|
||||
package conversion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
apix "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"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"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/conversion"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
conversionmetrics "sigs.k8s.io/controller-runtime/pkg/webhook/conversion/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logf.Log.WithName("conversion-webhook")
|
||||
)
|
||||
|
||||
func NewWebhookHandler(scheme *runtime.Scheme, registry Registry) http.Handler {
|
||||
return &webhook{scheme: scheme, decoder: NewDecoder(scheme), registry: registry}
|
||||
}
|
||||
|
||||
// webhook implements a CRD conversion webhook HTTP handler.
|
||||
type webhook struct {
|
||||
scheme *runtime.Scheme
|
||||
decoder *Decoder
|
||||
registry Registry
|
||||
}
|
||||
|
||||
// ensure Webhook implements http.Handler
|
||||
var _ http.Handler = &webhook{}
|
||||
|
||||
func (wh *webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
convertReview := &apix.ConversionReview{}
|
||||
err := json.NewDecoder(r.Body).Decode(convertReview)
|
||||
if err != nil {
|
||||
log.Error(err, "failed to read conversion request")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if convertReview.Request == nil {
|
||||
log.Error(nil, "conversion request is nil")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(droot): may be move the conversion logic to a separate module to
|
||||
// decouple it from the http layer ?
|
||||
resp, err := wh.handleConvertRequest(ctx, convertReview.Request)
|
||||
if err != nil {
|
||||
log.Error(err, "failed to convert", "request", convertReview.Request.UID)
|
||||
convertReview.Response = errored(err)
|
||||
} else {
|
||||
convertReview.Response = resp
|
||||
}
|
||||
convertReview.Response.UID = convertReview.Request.UID
|
||||
convertReview.Request = nil
|
||||
|
||||
err = json.NewEncoder(w).Encode(convertReview)
|
||||
if err != nil {
|
||||
log.Error(err, "failed to write response")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handles a version conversion request.
|
||||
func (wh *webhook) handleConvertRequest(ctx context.Context, req *apix.ConversionRequest) (_ *apix.ConversionResponse, retErr error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
conversionmetrics.WebhookPanics.WithLabelValues().Inc()
|
||||
|
||||
for _, fn := range utilruntime.PanicHandlers {
|
||||
fn(ctx, r)
|
||||
}
|
||||
retErr = errors.New("internal error occurred during conversion")
|
||||
return
|
||||
}
|
||||
}()
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("conversion request is nil")
|
||||
}
|
||||
var objects []runtime.RawExtension
|
||||
|
||||
for _, obj := range req.Objects {
|
||||
src, gvk, err := wh.decoder.Decode(obj.Raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dst, err := wh.allocateDstObject(req.DesiredAPIVersion, gvk.Kind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = wh.convertObject(ctx, src, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objects = append(objects, runtime.RawExtension{Object: dst})
|
||||
}
|
||||
return &apix.ConversionResponse{
|
||||
UID: req.UID,
|
||||
ConvertedObjects: objects,
|
||||
Result: metav1.Status{
|
||||
Status: metav1.StatusSuccess,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// convertObject will convert given a src object to dst object.
|
||||
// Note(droot): couldn't find a way to reduce the cyclomatic complexity under 10
|
||||
// without compromising readability, so disabling gocyclo linter
|
||||
func (wh *webhook) convertObject(ctx context.Context, src, dst runtime.Object) error {
|
||||
srcGVK := src.GetObjectKind().GroupVersionKind()
|
||||
dstGVK := dst.GetObjectKind().GroupVersionKind()
|
||||
|
||||
if srcGVK.GroupKind() != dstGVK.GroupKind() {
|
||||
return fmt.Errorf("src %T and dst %T does not belong to same API Group", src, dst)
|
||||
}
|
||||
|
||||
if srcGVK == dstGVK {
|
||||
return fmt.Errorf("conversion is not allowed between same type %T", src)
|
||||
}
|
||||
|
||||
if converter, ok := wh.registry.GetConverter(srcGVK.GroupKind()); ok {
|
||||
return converter.ConvertObject(ctx, src, dst)
|
||||
}
|
||||
|
||||
srcIsHub, dstIsHub := isHub(src), isHub(dst)
|
||||
srcIsConvertible, dstIsConvertible := isConvertible(src), isConvertible(dst)
|
||||
|
||||
switch {
|
||||
case srcIsHub && dstIsConvertible:
|
||||
return dst.(conversion.Convertible).ConvertFrom(src.(conversion.Hub))
|
||||
case dstIsHub && srcIsConvertible:
|
||||
return src.(conversion.Convertible).ConvertTo(dst.(conversion.Hub))
|
||||
case srcIsConvertible && dstIsConvertible:
|
||||
return wh.convertViaHub(src.(conversion.Convertible), dst.(conversion.Convertible))
|
||||
default:
|
||||
return fmt.Errorf("%T is not convertible to %T", src, dst)
|
||||
}
|
||||
}
|
||||
|
||||
func (wh *webhook) convertViaHub(src, dst conversion.Convertible) error {
|
||||
hub, err := wh.getHub(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hub == nil {
|
||||
return fmt.Errorf("%s does not have any Hub defined", src)
|
||||
}
|
||||
|
||||
err = src.ConvertTo(hub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%T failed to convert to hub version %T : %w", src, hub, err)
|
||||
}
|
||||
|
||||
err = dst.ConvertFrom(hub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%T failed to convert from hub version %T : %w", dst, hub, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getHub returns an instance of the Hub for passed-in object's group/kind.
|
||||
func (wh *webhook) getHub(obj runtime.Object) (conversion.Hub, error) {
|
||||
gvks, err := objectGVKs(wh.scheme, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(gvks) == 0 {
|
||||
return nil, fmt.Errorf("error retrieving gvks for object : %v", obj)
|
||||
}
|
||||
|
||||
var hub conversion.Hub
|
||||
var hubFoundAlready bool
|
||||
for _, gvk := range gvks {
|
||||
instance, err := wh.scheme.New(gvk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to allocate an instance for gvk %v: %w", gvk, err)
|
||||
}
|
||||
if val, isHub := instance.(conversion.Hub); isHub {
|
||||
if hubFoundAlready {
|
||||
return nil, fmt.Errorf("multiple hub version defined for %T", obj)
|
||||
}
|
||||
hubFoundAlready = true
|
||||
hub = val
|
||||
}
|
||||
}
|
||||
return hub, nil
|
||||
}
|
||||
|
||||
// allocateDstObject returns an instance for a given GVK.
|
||||
func (wh *webhook) allocateDstObject(apiVersion, kind string) (runtime.Object, error) {
|
||||
gvk := schema.FromAPIVersionAndKind(apiVersion, kind)
|
||||
|
||||
obj, err := wh.scheme.New(gvk)
|
||||
if err != nil {
|
||||
return obj, err
|
||||
}
|
||||
|
||||
t, err := meta.TypeAccessor(obj)
|
||||
if err != nil {
|
||||
return obj, err
|
||||
}
|
||||
|
||||
t.SetAPIVersion(apiVersion)
|
||||
t.SetKind(kind)
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// IsConvertible determines if given type is convertible or not. For a type
|
||||
// to be convertible, the group-kind needs to have a Hub type defined and all
|
||||
// non-hub types must be able to convert to/from Hub.
|
||||
func IsConvertible(scheme *runtime.Scheme, obj runtime.Object) (bool, error) {
|
||||
var hubs, spokes, nonSpokes []runtime.Object
|
||||
|
||||
gvks, err := objectGVKs(scheme, obj)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(gvks) == 0 {
|
||||
return false, fmt.Errorf("error retrieving gvks for object : %v", obj)
|
||||
}
|
||||
|
||||
for _, gvk := range gvks {
|
||||
instance, err := scheme.New(gvk)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to allocate an instance for gvk %v: %w", gvk, err)
|
||||
}
|
||||
|
||||
if isHub(instance) {
|
||||
hubs = append(hubs, instance)
|
||||
continue
|
||||
}
|
||||
|
||||
if !isConvertible(instance) {
|
||||
nonSpokes = append(nonSpokes, instance)
|
||||
continue
|
||||
}
|
||||
|
||||
spokes = append(spokes, instance)
|
||||
}
|
||||
|
||||
if len(gvks) == 1 {
|
||||
return false, nil // single version
|
||||
}
|
||||
|
||||
if len(hubs) == 0 && len(spokes) == 0 {
|
||||
// multiple version detected with no conversion implementation. This is
|
||||
// true for multi-version built-in types.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if len(hubs) == 1 && len(nonSpokes) == 0 { // convertible
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, PartialImplementationError{
|
||||
hubs: hubs,
|
||||
nonSpokes: nonSpokes,
|
||||
spokes: spokes,
|
||||
}
|
||||
}
|
||||
|
||||
// objectGVKs returns all (Group,Version,Kind) for the Group/Kind of given object.
|
||||
func objectGVKs(scheme *runtime.Scheme, obj runtime.Object) ([]schema.GroupVersionKind, error) {
|
||||
// NB: we should not use `obj.GetObjectKind().GroupVersionKind()` to get the
|
||||
// GVK here, since it is parsed from apiVersion and kind fields and it may
|
||||
// return empty GVK if obj is an uninitialized object.
|
||||
objGVKs, _, err := scheme.ObjectKinds(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(objGVKs) != 1 {
|
||||
return nil, fmt.Errorf("expect to get only one GVK for %v", obj)
|
||||
}
|
||||
objGVK := objGVKs[0]
|
||||
knownTypes := scheme.AllKnownTypes()
|
||||
|
||||
var gvks []schema.GroupVersionKind
|
||||
for gvk := range knownTypes {
|
||||
if objGVK.GroupKind() == gvk.GroupKind() {
|
||||
gvks = append(gvks, gvk)
|
||||
}
|
||||
}
|
||||
return gvks, nil
|
||||
}
|
||||
|
||||
// PartialImplementationError represents an error due to partial conversion
|
||||
// implementation such as hub without spokes, multiple hubs or spokes without hub.
|
||||
type PartialImplementationError struct {
|
||||
gvk schema.GroupVersionKind
|
||||
hubs []runtime.Object
|
||||
nonSpokes []runtime.Object
|
||||
spokes []runtime.Object
|
||||
}
|
||||
|
||||
func (e PartialImplementationError) Error() string {
|
||||
if len(e.hubs) == 0 {
|
||||
return fmt.Sprintf("no hub defined for gvk %s", e.gvk)
|
||||
}
|
||||
if len(e.hubs) > 1 {
|
||||
return fmt.Sprintf("multiple(%d) hubs defined for group-kind '%s' ",
|
||||
len(e.hubs), e.gvk.GroupKind())
|
||||
}
|
||||
if len(e.nonSpokes) > 0 {
|
||||
return fmt.Sprintf("%d inconvertible types detected for group-kind '%s'",
|
||||
len(e.nonSpokes), e.gvk.GroupKind())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isHub determines if passed-in object is a Hub or not.
|
||||
func isHub(obj runtime.Object) bool {
|
||||
_, yes := obj.(conversion.Hub)
|
||||
return yes
|
||||
}
|
||||
|
||||
// isConvertible determines if passed-in object is a convertible.
|
||||
func isConvertible(obj runtime.Object) bool {
|
||||
_, yes := obj.(conversion.Convertible)
|
||||
return yes
|
||||
}
|
||||
|
||||
// helper to construct error response.
|
||||
func errored(err error) *apix.ConversionResponse {
|
||||
return &apix.ConversionResponse{
|
||||
Result: metav1.Status{
|
||||
Status: metav1.StatusFailure,
|
||||
Message: err.Error(),
|
||||
},
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+173
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package conversion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
)
|
||||
|
||||
func NewHubSpokeConverter[hubObject runtime.Object](hub hubObject, spokeConverter ...SpokeConverter[hubObject]) func(scheme *runtime.Scheme) (Converter, error) {
|
||||
return func(scheme *runtime.Scheme) (Converter, error) {
|
||||
hubGVK, err := apiutil.GVKForObject(hub, scheme)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create hub spoke converter: failed to get GroupVersionKind for hub: %w", err)
|
||||
}
|
||||
allGVKs, err := objectGVKs(scheme, hub)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create hub spoke converter for %s: %w", hubGVK.Kind, err)
|
||||
}
|
||||
spokeVersions := sets.New[string]()
|
||||
for _, gvk := range allGVKs {
|
||||
if gvk != hubGVK {
|
||||
spokeVersions.Insert(gvk.Version)
|
||||
}
|
||||
}
|
||||
|
||||
c := &hubSpokeConverter[hubObject]{
|
||||
scheme: scheme,
|
||||
hubGVK: hubGVK,
|
||||
spokeConverterByGVK: map[schema.GroupVersionKind]SpokeConverter[hubObject]{},
|
||||
}
|
||||
|
||||
spokeConverterVersions := sets.New[string]()
|
||||
for _, sc := range spokeConverter {
|
||||
spokeGVK, err := apiutil.GVKForObject(sc.GetSpoke(), scheme)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create hub spoke converter for %s: "+
|
||||
"failed to get GroupVersionKind for spoke converter: %w",
|
||||
hubGVK.Kind, err)
|
||||
}
|
||||
if hubGVK.GroupKind() != spokeGVK.GroupKind() {
|
||||
return nil, fmt.Errorf("failed to create hub spoke converter for %s: "+
|
||||
"spoke converter GroupKind %s does not match hub GroupKind %s",
|
||||
hubGVK.Kind, spokeGVK.GroupKind(), hubGVK.GroupKind())
|
||||
}
|
||||
|
||||
if _, ok := c.spokeConverterByGVK[spokeGVK]; ok {
|
||||
return nil, fmt.Errorf("failed to create hub spoke converter for %s: "+
|
||||
"duplicate spoke converter for version %s",
|
||||
hubGVK.Kind, spokeGVK.Version)
|
||||
}
|
||||
c.spokeConverterByGVK[spokeGVK] = sc
|
||||
spokeConverterVersions.Insert(spokeGVK.Version)
|
||||
}
|
||||
|
||||
if !spokeConverterVersions.Equal(spokeVersions) {
|
||||
return nil, fmt.Errorf("failed to create hub spoke converter for %s: "+
|
||||
"expected spoke converter for %s got spoke converter for %s",
|
||||
hubGVK.Kind, sortAndJoin(spokeVersions), sortAndJoin(spokeConverterVersions))
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
func sortAndJoin(set sets.Set[string]) string {
|
||||
list := set.UnsortedList()
|
||||
slices.Sort(list)
|
||||
return strings.Join(list, ",")
|
||||
}
|
||||
|
||||
type hubSpokeConverter[hubObject runtime.Object] struct {
|
||||
scheme *runtime.Scheme
|
||||
hubGVK schema.GroupVersionKind
|
||||
spokeConverterByGVK map[schema.GroupVersionKind]SpokeConverter[hubObject]
|
||||
}
|
||||
|
||||
func (c hubSpokeConverter[hubObject]) ConvertObject(ctx context.Context, src, dst runtime.Object) error {
|
||||
srcGVK := src.GetObjectKind().GroupVersionKind()
|
||||
dstGVK := dst.GetObjectKind().GroupVersionKind()
|
||||
|
||||
if srcGVK.GroupKind() != dstGVK.GroupKind() {
|
||||
return fmt.Errorf("src %T and dst %T does not belong to same API Group", src, dst)
|
||||
}
|
||||
|
||||
if srcGVK == dstGVK {
|
||||
return fmt.Errorf("conversion is not allowed between same type %T", src)
|
||||
}
|
||||
|
||||
srcIsHub := c.hubGVK == srcGVK
|
||||
dstIsHub := c.hubGVK == dstGVK
|
||||
_, srcIsConvertible := c.spokeConverterByGVK[srcGVK]
|
||||
_, dstIsConvertible := c.spokeConverterByGVK[dstGVK]
|
||||
|
||||
switch {
|
||||
case srcIsHub && dstIsConvertible:
|
||||
return c.spokeConverterByGVK[dstGVK].ConvertHubToSpoke(ctx, src.(hubObject), dst)
|
||||
case dstIsHub && srcIsConvertible:
|
||||
return c.spokeConverterByGVK[srcGVK].ConvertSpokeToHub(ctx, src, dst.(hubObject))
|
||||
case srcIsConvertible && dstIsConvertible:
|
||||
hub, err := c.scheme.New(c.hubGVK)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to allocate an instance for GroupVersionKind %s: %w", c.hubGVK, err)
|
||||
}
|
||||
if err := c.spokeConverterByGVK[srcGVK].ConvertSpokeToHub(ctx, src, hub.(hubObject)); err != nil {
|
||||
return fmt.Errorf("failed to convert spoke %s to hub %s : %w", srcGVK, c.hubGVK, err)
|
||||
}
|
||||
if err := c.spokeConverterByGVK[dstGVK].ConvertHubToSpoke(ctx, hub.(hubObject), dst); err != nil {
|
||||
return fmt.Errorf("failed to convert hub %s to spoke %s : %w", c.hubGVK, dstGVK, err)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("failed to convert %s to %s: not convertible", srcGVK, dstGVK)
|
||||
}
|
||||
}
|
||||
|
||||
type SpokeConverter[hubObject runtime.Object] interface {
|
||||
GetSpoke() runtime.Object
|
||||
ConvertHubToSpoke(ctx context.Context, hub hubObject, spoke runtime.Object) error
|
||||
ConvertSpokeToHub(ctx context.Context, spoke runtime.Object, hub hubObject) error
|
||||
}
|
||||
|
||||
func NewSpokeConverter[hubObject, spokeObject client.Object](
|
||||
spoke spokeObject,
|
||||
convertHubToSpokeFunc func(ctx context.Context, src hubObject, dst spokeObject) error,
|
||||
convertSpokeToHubFunc func(ctx context.Context, src spokeObject, dst hubObject) error,
|
||||
) SpokeConverter[hubObject] {
|
||||
return &spokeConverter[hubObject, spokeObject]{
|
||||
spoke: spoke,
|
||||
convertSpokeToHubFunc: convertSpokeToHubFunc,
|
||||
convertHubToSpokeFunc: convertHubToSpokeFunc,
|
||||
}
|
||||
}
|
||||
|
||||
type spokeConverter[hubObject, spokeObject runtime.Object] struct {
|
||||
spoke spokeObject
|
||||
convertHubToSpokeFunc func(ctx context.Context, src hubObject, dst spokeObject) error
|
||||
convertSpokeToHubFunc func(ctx context.Context, src spokeObject, dst hubObject) error
|
||||
}
|
||||
|
||||
func (c spokeConverter[hubObject, spokeObject]) GetSpoke() runtime.Object {
|
||||
return c.spoke
|
||||
}
|
||||
|
||||
func (c spokeConverter[hubObject, spokeObject]) ConvertHubToSpoke(ctx context.Context, hub hubObject, spoke runtime.Object) error {
|
||||
return c.convertHubToSpokeFunc(ctx, hub, spoke.(spokeObject))
|
||||
}
|
||||
|
||||
func (c spokeConverter[hubObject, spokeObject]) ConvertSpokeToHub(ctx context.Context, spoke runtime.Object, hub hubObject) error {
|
||||
return c.convertSpokeToHubFunc(ctx, spoke.(spokeObject), hub)
|
||||
}
|
||||
Generated
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package conversion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type Converter interface {
|
||||
ConvertObject(ctx context.Context, src, dst runtime.Object) error
|
||||
}
|
||||
|
||||
type Registry interface {
|
||||
RegisterConverter(gk schema.GroupKind, converter Converter) error
|
||||
GetConverter(gk schema.GroupKind) (Converter, bool)
|
||||
}
|
||||
|
||||
type registry struct {
|
||||
converterByGK map[schema.GroupKind]Converter
|
||||
}
|
||||
|
||||
func NewRegistry() Registry {
|
||||
return registry{
|
||||
converterByGK: map[schema.GroupKind]Converter{},
|
||||
}
|
||||
}
|
||||
func (r registry) RegisterConverter(gk schema.GroupKind, converter Converter) error {
|
||||
if _, ok := r.converterByGK[gk]; ok {
|
||||
return fmt.Errorf("failed to register Converter for GroupKind %s: converter already registered", gk)
|
||||
}
|
||||
|
||||
r.converterByGK[gk] = converter
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r registry) GetConverter(gk schema.GroupKind) (Converter, bool) {
|
||||
c, ok := r.converterByGK[gk]
|
||||
return c, ok
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
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 conversion
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
)
|
||||
|
||||
// Decoder knows how to decode the contents of a CRD version conversion
|
||||
// request into a concrete object.
|
||||
// TODO(droot): consider reusing decoder from admission pkg for this.
|
||||
type Decoder struct {
|
||||
codecs serializer.CodecFactory
|
||||
}
|
||||
|
||||
// NewDecoder creates a Decoder given the runtime.Scheme
|
||||
func NewDecoder(scheme *runtime.Scheme) *Decoder {
|
||||
if scheme == nil {
|
||||
panic("scheme should never be nil")
|
||||
}
|
||||
return &Decoder{codecs: serializer.NewCodecFactory(scheme)}
|
||||
}
|
||||
|
||||
// Decode decodes the inlined object.
|
||||
func (d *Decoder) Decode(content []byte) (runtime.Object, *schema.GroupVersionKind, error) {
|
||||
deserializer := d.codecs.UniversalDeserializer()
|
||||
return deserializer.Decode(content, nil, nil)
|
||||
}
|
||||
|
||||
// DecodeInto decodes the inlined object in the into the passed-in runtime.Object.
|
||||
func (d *Decoder) DecodeInto(content []byte, into runtime.Object) error {
|
||||
deserializer := d.codecs.UniversalDeserializer()
|
||||
return runtime.DecodeInto(deserializer, content, into)
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
// WebhookPanics is a prometheus counter metrics which holds the total
|
||||
// number of panics from conversion webhooks.
|
||||
WebhookPanics = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "controller_runtime_conversion_webhook_panics_total",
|
||||
Help: "Total number of conversion webhook panics",
|
||||
}, []string{})
|
||||
)
|
||||
|
||||
func init() {
|
||||
metrics.Registry.MustRegister(
|
||||
WebhookPanics,
|
||||
)
|
||||
// Init metric.
|
||||
WebhookPanics.WithLabelValues().Add(0)
|
||||
}
|
||||
Reference in New Issue
Block a user