knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/resourcesemantics/conversion/conversion.go (about)

     1  /*
     2  Copyright 2020 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package conversion
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  
    24  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    25  	"go.uber.org/zap"
    26  	apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  
    31  	"knative.dev/pkg/apis"
    32  	"knative.dev/pkg/kmeta"
    33  	"knative.dev/pkg/logging"
    34  	"knative.dev/pkg/logging/logkey"
    35  	"knative.dev/pkg/webhook"
    36  )
    37  
    38  // Convert implements webhook.ConversionController
    39  func (r *reconciler) Convert(
    40  	ctx context.Context,
    41  	req *apixv1.ConversionRequest,
    42  ) *apixv1.ConversionResponse {
    43  	if r.withContext != nil {
    44  		ctx = r.withContext(ctx)
    45  	}
    46  
    47  	res := &apixv1.ConversionResponse{
    48  		UID: req.UID,
    49  		Result: metav1.Status{
    50  			Status: metav1.StatusSuccess,
    51  		},
    52  	}
    53  
    54  	result := make([]runtime.RawExtension, 0, len(req.Objects))
    55  
    56  	for _, obj := range req.Objects {
    57  		converted, err := r.convert(ctx, obj, req.DesiredAPIVersion)
    58  		if err != nil {
    59  			logging.FromContext(ctx).Errorw("Conversion failed", zap.Error(err))
    60  			res.Result.Status = metav1.StatusFailure
    61  			res.Result.Message = err.Error()
    62  			break
    63  		}
    64  
    65  		result = append(result, converted)
    66  	}
    67  
    68  	res.ConvertedObjects = result
    69  	return res
    70  }
    71  
    72  func (r *reconciler) convert(
    73  	ctx context.Context,
    74  	inRaw runtime.RawExtension,
    75  	targetVersion string,
    76  ) (runtime.RawExtension, error) {
    77  	logger := logging.FromContext(ctx)
    78  	var ret runtime.RawExtension
    79  
    80  	inGVK, err := parseGVK(inRaw)
    81  	if err != nil {
    82  		return ret, err
    83  	}
    84  
    85  	// otelhttp middleware creates the labeler
    86  	labeler, _ := otelhttp.LabelerFromContext(ctx)
    87  	labeler.Add(
    88  		webhook.KindAttr.With(inGVK.Kind),
    89  	)
    90  
    91  	inGK := inGVK.GroupKind()
    92  	conv, ok := r.kinds[inGK]
    93  	if !ok {
    94  		return ret, fmt.Errorf("no conversion support for type %s", formatGK(inGVK.GroupKind()))
    95  	}
    96  
    97  	outGVK, err := parseAPIVersion(targetVersion, inGK.Kind)
    98  	if err != nil {
    99  		return ret, err
   100  	}
   101  
   102  	inZygote, ok := conv.Zygotes[inGVK.Version]
   103  	if !ok {
   104  		return ret, fmt.Errorf("conversion not supported for type %s", formatGVK(inGVK))
   105  	}
   106  	outZygote, ok := conv.Zygotes[outGVK.Version]
   107  	if !ok {
   108  		return ret, fmt.Errorf("conversion not supported for type %s", formatGVK(outGVK))
   109  	}
   110  	hubZygote, ok := conv.Zygotes[conv.HubVersion]
   111  	if !ok {
   112  		return ret, fmt.Errorf("conversion not supported for type %s", formatGK(inGVK.GroupKind()))
   113  	}
   114  
   115  	in := inZygote.DeepCopyObject().(ConvertibleObject)
   116  	hub := hubZygote.DeepCopyObject().(ConvertibleObject)
   117  	out := outZygote.DeepCopyObject().(ConvertibleObject)
   118  
   119  	hubGVK := inGVK.GroupKind().WithVersion(conv.HubVersion)
   120  
   121  	logger = logger.With(
   122  		zap.String("inputType", formatGVK(inGVK)),
   123  		zap.String("outputType", formatGVK(outGVK)),
   124  		zap.String("hubType", formatGVK(hubGVK)),
   125  	)
   126  
   127  	// TODO(dprotaso) - potentially error on unknown fields
   128  	if err = json.Unmarshal(inRaw.Raw, &in); err != nil {
   129  		return ret, fmt.Errorf("unable to unmarshal input: %w", err)
   130  	}
   131  
   132  	if acc, err := kmeta.DeletionHandlingAccessor(in); err == nil {
   133  		// TODO: right now we don't convert any non-namespaced objects. If we ever do that
   134  		// this needs to updated to deal with it.
   135  		logger = logger.With(zap.String(logkey.Key, acc.GetNamespace()+"/"+acc.GetName()))
   136  	} else {
   137  		logger.Infof("Could not get Accessor for %s: %v", formatGK(inGVK.GroupKind()), err)
   138  	}
   139  	ctx = logging.WithLogger(ctx, logger)
   140  
   141  	if inGVK.Version == conv.HubVersion {
   142  		hub = in
   143  	} else if err = hub.ConvertFrom(ctx, in); err != nil {
   144  		return ret, fmt.Errorf("conversion failed to version %s for type %s -  %w", outGVK.Version, formatGVK(inGVK), err)
   145  	}
   146  
   147  	if outGVK.Version == conv.HubVersion {
   148  		out = hub
   149  	} else if err = hub.ConvertTo(ctx, out); err != nil {
   150  		return ret, fmt.Errorf("conversion failed to version %s for type %s -  %w", outGVK.Version, formatGVK(inGVK), err)
   151  	}
   152  
   153  	out.GetObjectKind().SetGroupVersionKind(outGVK)
   154  
   155  	if defaultable, ok := out.(apis.Defaultable); ok {
   156  		defaultable.SetDefaults(ctx)
   157  	}
   158  
   159  	if ret.Raw, err = json.Marshal(out); err != nil {
   160  		return ret, fmt.Errorf("unable to marshal output: %w", err)
   161  	}
   162  	return ret, nil
   163  }
   164  
   165  func parseGVK(in runtime.RawExtension) (schema.GroupVersionKind, error) {
   166  	var (
   167  		typeMeta metav1.TypeMeta
   168  		gvk      schema.GroupVersionKind
   169  	)
   170  
   171  	if err := json.Unmarshal(in.Raw, &typeMeta); err != nil {
   172  		return gvk, fmt.Errorf("error parsing type meta %q - %w", string(in.Raw), err)
   173  	}
   174  
   175  	gv, err := schema.ParseGroupVersion(typeMeta.APIVersion)
   176  	if err != nil {
   177  		return gvk, fmt.Errorf("error parsing GV %q: %w", typeMeta.APIVersion, err)
   178  	}
   179  	gvk = gv.WithKind(typeMeta.Kind)
   180  
   181  	if gvk.Group == "" || gvk.Version == "" || gvk.Kind == "" {
   182  		return gvk, fmt.Errorf("invalid GroupVersionKind %v", gvk)
   183  	}
   184  
   185  	return gvk, nil
   186  }
   187  
   188  func parseAPIVersion(apiVersion string, kind string) (schema.GroupVersionKind, error) {
   189  	gv, err := schema.ParseGroupVersion(apiVersion)
   190  	if err != nil {
   191  		err = fmt.Errorf("desired API version %q is not valid", apiVersion)
   192  		return schema.GroupVersionKind{}, err
   193  	}
   194  
   195  	if !isValidGV(gv) {
   196  		err = fmt.Errorf("desired API version %q is not valid", apiVersion)
   197  		return schema.GroupVersionKind{}, err
   198  	}
   199  
   200  	return gv.WithKind(kind), nil
   201  }
   202  
   203  func formatGVK(gvk schema.GroupVersionKind) string {
   204  	return fmt.Sprintf("[kind=%s group=%s version=%s]", gvk.Kind, gvk.Group, gvk.Version)
   205  }
   206  
   207  func formatGK(gk schema.GroupKind) string {
   208  	return fmt.Sprintf("[kind=%s group=%s]", gk.Kind, gk.Group)
   209  }
   210  
   211  func isValidGV(gk schema.GroupVersion) bool {
   212  	return gk.Group != "" && gk.Version != ""
   213  }