From 90d10ded28f5f758371283ff468edf0b0da3d94a Mon Sep 17 00:00:00 2001 From: Sneha Chhabria Date: Mon, 19 Jul 2021 16:44:47 -0700 Subject: [PATCH] feat(upgrade): add handlers for conversion of CRD's in OSM This commit adds a handler for all the CRD's that OSM manages. The actaul business logic for conversion will be added once a CRD has multiple versions. Fixs #3396 Signed-off-by: Sneha Chhabria --- go.mod | 1 + go.sum | 1 + pkg/crdconversion/crdconversion.go | 12 +- pkg/crdconversion/egresspolicyconversion.go | 27 +++ pkg/crdconversion/framework.go | 155 ++++++++++++++++++ pkg/crdconversion/httproutegroupconversion.go | 27 +++ .../ingressbackendspolicyconversion.go | 27 +++ pkg/crdconversion/meshconfigconversion.go | 27 +++ .../multiclusterserviceconversion.go | 27 +++ pkg/crdconversion/tcproutesconversion.go | 27 +++ pkg/crdconversion/trafficaccessconversion.go | 27 +++ pkg/crdconversion/trafficsplitconversion.go | 27 +++ 12 files changed, 382 insertions(+), 3 deletions(-) create mode 100644 pkg/crdconversion/egresspolicyconversion.go create mode 100644 pkg/crdconversion/framework.go create mode 100644 pkg/crdconversion/httproutegroupconversion.go create mode 100644 pkg/crdconversion/ingressbackendspolicyconversion.go create mode 100644 pkg/crdconversion/meshconfigconversion.go create mode 100644 pkg/crdconversion/multiclusterserviceconversion.go create mode 100644 pkg/crdconversion/tcproutesconversion.go create mode 100644 pkg/crdconversion/trafficaccessconversion.go create mode 100644 pkg/crdconversion/trafficsplitconversion.go diff --git a/go.mod b/go.mod index 54ac6dd0ae..461652a67e 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/matm/gocov-html v0.0.0-20200509184451-71874e2e203b github.com/mitchellh/gox v1.0.1 github.com/mitchellh/hashstructure/v2 v2.0.1 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 github.com/norwoodj/helm-docs v1.4.0 github.com/olekukonko/tablewriter v0.0.4 github.com/onsi/ginkgo v1.16.1 diff --git a/go.sum b/go.sum index a47996f8a7..3d68626d0e 100644 --- a/go.sum +++ b/go.sum @@ -778,6 +778,7 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= github.com/munnerz/crd-schema-fuzz v1.0.0/go.mod h1:4z/rcm37JxUkSsExFcLL6ZIT1SgDRdLiu7qq1evdVS0= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= diff --git a/pkg/crdconversion/crdconversion.go b/pkg/crdconversion/crdconversion.go index f9698b5542..d6b37e9417 100644 --- a/pkg/crdconversion/crdconversion.go +++ b/pkg/crdconversion/crdconversion.go @@ -36,6 +36,7 @@ var crdConversionWebhookConfiguration = map[string]string{ "egresses.policy.openservicemesh.io": "/egresspolicyconversion", "trafficsplits.split.smi-spec.io": "/trafficsplitconversion", "tcproutes.specs.smi-spec.io": "/tcproutesconversion", + "ingressbackends.policy.openservicemesh.io": "/ingressbackendspolicyconversion", } var conversionReviewVersions = []string{"v1beta1", "v1"} @@ -79,8 +80,14 @@ func (crdWh *crdConversionWebhook) run(stop <-chan struct{}) { defer cancel() webhookMux := http.NewServeMux() - - // TODO (snchh): add handler and logic for conversion stratergy of each CRD in OSM + webhookMux.HandleFunc("/meshconfigconversion", serveMeshConfigConversion) + webhookMux.HandleFunc("/trafficaccessconversion", serveTrafficAccessConversion) + webhookMux.HandleFunc("/httproutegroupconversion", serveHTTPRouteGroupConversion) + webhookMux.HandleFunc("/multiclusterserviceconversion", serveMultiClusterServiceConversion) + webhookMux.HandleFunc("/egresspolicyconversion", serveEgressPolicyConversion) + webhookMux.HandleFunc("/trafficsplitconversion", serveTrafficSplitConversion) + webhookMux.HandleFunc("/tcproutesconversion", serveTCPRouteConversion) + webhookMux.HandleFunc("/ingressbackendspolicyconversion", serveIngressBackendsPolicyConversion) webhookServer := &http.Server{ Addr: fmt.Sprintf(":%d", crdWh.config.ListenPort), @@ -108,7 +115,6 @@ func (crdWh *crdConversionWebhook) run(stop <-chan struct{}) { }() healthMux := http.NewServeMux() - healthMux.HandleFunc(webhookHealthPath, healthHandler) healthServer := &http.Server{ diff --git a/pkg/crdconversion/egresspolicyconversion.go b/pkg/crdconversion/egresspolicyconversion.go new file mode 100644 index 0000000000..da07fc58c7 --- /dev/null +++ b/pkg/crdconversion/egresspolicyconversion.go @@ -0,0 +1,27 @@ +package crdconversion + +import ( + "net/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// serveEgressPolicyConversion servers endpoint for the converter defined as convertEgressPolicy function. +func serveEgressPolicyConversion(w http.ResponseWriter, r *http.Request) { + serve(w, r, convertEgressPolicy) +} + +// convertEgressPolicy contains the business logic to convert egresses.policy.openservicemesh.io CRD +// Example implementation reference : https://github.com/kubernetes/kubernetes/blob/release-1.21/test/images/agnhost/crd-conversion-webhook/converter/example_converter.go +func convertEgressPolicy(Object *unstructured.Unstructured, toVersion string) (*unstructured.Unstructured, metav1.Status) { + convertedObject := Object.DeepCopy() + fromVersion := Object.GetAPIVersion() + + if toVersion == fromVersion { + return nil, statusErrorWithMessage("EgressPolicy: conversion from a version to itself should not call the webhook: %s", toVersion) + } + + log.Debug().Msg("EgressPolicy: successfully converted object") + return convertedObject, statusSucceed() +} diff --git a/pkg/crdconversion/framework.go b/pkg/crdconversion/framework.go new file mode 100644 index 0000000000..80543c250e --- /dev/null +++ b/pkg/crdconversion/framework.go @@ -0,0 +1,155 @@ +package crdconversion + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/munnerz/goautoneg" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer/json" +) + +// convertFunc is the user defined function for any conversion. The code in this file is a +// template that can be use for any CR conversion given this function. +type convertFunc func(Object *unstructured.Unstructured, version string) (*unstructured.Unstructured, metav1.Status) + +// conversionResponseFailureWithMessagef is a helper function to create an AdmissionResponse +// with a formatted embedded error message. +func conversionResponseFailureWithMessagef(msg string, params ...interface{}) *v1beta1.ConversionResponse { + return &v1beta1.ConversionResponse{ + Result: metav1.Status{ + Message: fmt.Sprintf(msg, params...), + Status: metav1.StatusFailure, + }, + } +} + +func statusErrorWithMessage(msg string, params ...interface{}) metav1.Status { + return metav1.Status{ + Message: fmt.Sprintf(msg, params...), + Status: metav1.StatusFailure, + } +} + +func statusSucceed() metav1.Status { + return metav1.Status{ + Status: metav1.StatusSuccess, + } +} + +// doConversion converts the requested object given the conversion function and returns a conversion response. +// failures will be reported as Reason in the conversion response. +func doConversion(convertRequest *v1beta1.ConversionRequest, convert convertFunc) *v1beta1.ConversionResponse { + var convertedObjects []runtime.RawExtension + for _, obj := range convertRequest.Objects { + cr := unstructured.Unstructured{} + if err := cr.UnmarshalJSON(obj.Raw); err != nil { + log.Error().Err(err) + return conversionResponseFailureWithMessagef("failed to unmarshall object (%v) with error: %v", string(obj.Raw), err) + } + convertedCR, status := convert(&cr, convertRequest.DesiredAPIVersion) + if status.Status != metav1.StatusSuccess { + log.Error().Msgf(status.String()) + return &v1beta1.ConversionResponse{ + Result: status, + } + } + convertedCR.SetAPIVersion(convertRequest.DesiredAPIVersion) + convertedObjects = append(convertedObjects, runtime.RawExtension{Object: convertedCR}) + } + return &v1beta1.ConversionResponse{ + ConvertedObjects: convertedObjects, + Result: statusSucceed(), + } +} + +func serve(w http.ResponseWriter, r *http.Request, convert convertFunc) { + var body []byte + if r.Body != nil { + if data, err := ioutil.ReadAll(r.Body); err == nil { + body = data + } + } + + contentType := r.Header.Get("Content-Type") + serializer := getInputSerializer(contentType) + if serializer == nil { + msg := fmt.Sprintf("invalid Content-Type header `%s`", contentType) + log.Error().Msgf(msg) + http.Error(w, msg, http.StatusBadRequest) + return + } + + log.Debug().Msgf("handling request: %v", body) + convertReview := v1beta1.ConversionReview{} + if _, _, err := serializer.Decode(body, nil, &convertReview); err != nil { + log.Error().Err(err) + convertReview.Response = conversionResponseFailureWithMessagef("failed to deserialize body (%v) with error %v", string(body), err) + } else { + convertReview.Response = doConversion(convertReview.Request, convert) + convertReview.Response.UID = convertReview.Request.UID + } + log.Debug().Msgf(fmt.Sprintf("sending response: %v", convertReview.Response)) + + // reset the request, it is not needed in a response. + convertReview.Request = &v1beta1.ConversionRequest{} + + accept := r.Header.Get("Accept") + outSerializer := getOutputSerializer(accept) + if outSerializer == nil { + msg := fmt.Sprintf("invalid accept header `%s`", accept) + log.Error().Msgf(msg) + http.Error(w, msg, http.StatusBadRequest) + return + } + err := outSerializer.Encode(&convertReview, w) + if err != nil { + log.Error().Err(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +type mediaType struct { + Type, SubType string +} + +var scheme = runtime.NewScheme() +var serializers = map[mediaType]runtime.Serializer{ + {"application", "json"}: json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false), + {"application", "yaml"}: json.NewYAMLSerializer(json.DefaultMetaFactory, scheme, scheme), +} + +func getInputSerializer(contentType string) runtime.Serializer { + parts := strings.SplitN(contentType, "/", 2) + if len(parts) != 2 { + return nil + } + return serializers[mediaType{parts[0], parts[1]}] +} + +func getOutputSerializer(accept string) runtime.Serializer { + if len(accept) == 0 { + return serializers[mediaType{"application", "json"}] + } + + clauses := goautoneg.ParseAccept(accept) + for _, clause := range clauses { + for k, v := range serializers { + switch { + case clause.Type == k.Type && clause.SubType == k.SubType, + clause.Type == k.Type && clause.SubType == "*", + clause.Type == "*" && clause.SubType == "*": + return v + } + } + } + + return nil +} diff --git a/pkg/crdconversion/httproutegroupconversion.go b/pkg/crdconversion/httproutegroupconversion.go new file mode 100644 index 0000000000..3cf6946f7a --- /dev/null +++ b/pkg/crdconversion/httproutegroupconversion.go @@ -0,0 +1,27 @@ +package crdconversion + +import ( + "net/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// serveHTTPRouteGroupConversion servers endpoint for the converter defined as convertHTTPRouteGroup function. +func serveHTTPRouteGroupConversion(w http.ResponseWriter, r *http.Request) { + serve(w, r, convertHTTPRouteGroup) +} + +// convertEgressPolicy contains the business logic to convert httproutegroups.specs.smi-spec.io CRD +// Example implementation reference : https://github.com/kubernetes/kubernetes/blob/release-1.21/test/images/agnhost/crd-conversion-webhook/converter/example_converter.go +func convertHTTPRouteGroup(Object *unstructured.Unstructured, toVersion string) (*unstructured.Unstructured, metav1.Status) { + convertedObject := Object.DeepCopy() + fromVersion := Object.GetAPIVersion() + + if toVersion == fromVersion { + return nil, statusErrorWithMessage("HTTPRouteGroup: conversion from a version to itself should not call the webhook: %s", toVersion) + } + + log.Debug().Msg("HTTPRouteGroup: successfully converted object") + return convertedObject, statusSucceed() +} diff --git a/pkg/crdconversion/ingressbackendspolicyconversion.go b/pkg/crdconversion/ingressbackendspolicyconversion.go new file mode 100644 index 0000000000..ee9f8df975 --- /dev/null +++ b/pkg/crdconversion/ingressbackendspolicyconversion.go @@ -0,0 +1,27 @@ +package crdconversion + +import ( + "net/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// serveIngressBackendsPolicyConversion servers endpoint for the converter defined as convertIngressBackendsPolicy function. +func serveIngressBackendsPolicyConversion(w http.ResponseWriter, r *http.Request) { + serve(w, r, convertIngressBackendsPolicy) +} + +// convertIngressBackendsPolicy contains the business logic to convert ingressbackends.policy.openservicemesh.io CRD +// Example implementation reference : https://github.com/kubernetes/kubernetes/blob/release-1.21/test/images/agnhost/crd-conversion-webhook/converter/example_converter.go +func convertIngressBackendsPolicy(Object *unstructured.Unstructured, toVersion string) (*unstructured.Unstructured, metav1.Status) { + convertedObject := Object.DeepCopy() + fromVersion := Object.GetAPIVersion() + + if toVersion == fromVersion { + return nil, statusErrorWithMessage("IngressBackendsPolicy: conversion from a version to itself should not call the webhook: %s", toVersion) + } + + log.Debug().Msg("IngressBackendsPolicy: successfully converted object") + return convertedObject, statusSucceed() +} diff --git a/pkg/crdconversion/meshconfigconversion.go b/pkg/crdconversion/meshconfigconversion.go new file mode 100644 index 0000000000..aaf16125a2 --- /dev/null +++ b/pkg/crdconversion/meshconfigconversion.go @@ -0,0 +1,27 @@ +package crdconversion + +import ( + "net/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// serveMeshConfigConversion servers endpoint for the converter defined as convertMeshConfig function. +func serveMeshConfigConversion(w http.ResponseWriter, r *http.Request) { + serve(w, r, convertMeshConfig) +} + +// convertMeshConfig contains the business logic to convert meshconfigs.config.openservicemesh.io CRD +// Example implementation reference : https://github.com/kubernetes/kubernetes/blob/release-1.21/test/images/agnhost/crd-conversion-webhook/converter/example_converter.go +func convertMeshConfig(Object *unstructured.Unstructured, toVersion string) (*unstructured.Unstructured, metav1.Status) { + convertedObject := Object.DeepCopy() + fromVersion := Object.GetAPIVersion() + + if toVersion == fromVersion { + return nil, statusErrorWithMessage("MeshConfig: conversion from a version to itself should not call the webhook: %s", toVersion) + } + + log.Debug().Msg("MeshConfig: successfully converted object") + return convertedObject, statusSucceed() +} diff --git a/pkg/crdconversion/multiclusterserviceconversion.go b/pkg/crdconversion/multiclusterserviceconversion.go new file mode 100644 index 0000000000..6d0e2aaa46 --- /dev/null +++ b/pkg/crdconversion/multiclusterserviceconversion.go @@ -0,0 +1,27 @@ +package crdconversion + +import ( + "net/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// serveMultiClusterServiceConversion servers endpoint for the converter defined as convertMultiClusterService function. +func serveMultiClusterServiceConversion(w http.ResponseWriter, r *http.Request) { + serve(w, r, convertMultiClusterService) +} + +// convertMultiClusterService contains the business logic to convert multiclusterservices.config.openservicemesh.io CRD +// Example implementation reference : https://github.com/kubernetes/kubernetes/blob/release-1.21/test/images/agnhost/crd-conversion-webhook/converter/example_converter.go +func convertMultiClusterService(Object *unstructured.Unstructured, toVersion string) (*unstructured.Unstructured, metav1.Status) { + convertedObject := Object.DeepCopy() + fromVersion := Object.GetAPIVersion() + + if toVersion == fromVersion { + return nil, statusErrorWithMessage("MultiClusterService: conversion from a version to itself should not call the webhook: %s", toVersion) + } + + log.Debug().Msg("MultiClusterService: successfully converted object") + return convertedObject, statusSucceed() +} diff --git a/pkg/crdconversion/tcproutesconversion.go b/pkg/crdconversion/tcproutesconversion.go new file mode 100644 index 0000000000..b4a3b535cd --- /dev/null +++ b/pkg/crdconversion/tcproutesconversion.go @@ -0,0 +1,27 @@ +package crdconversion + +import ( + "net/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// serveTCPRouteConversion servers endpoint for the converter defined as convertTCPRoute function. +func serveTCPRouteConversion(w http.ResponseWriter, r *http.Request) { + serve(w, r, convertTCPRoute) +} + +// convertTCPRoute contains the business logic to convert tcproutes.specs.smi-spec.io CRD +// Example implementation reference : https://github.com/kubernetes/kubernetes/blob/release-1.21/test/images/agnhost/crd-conversion-webhook/converter/example_converter.go +func convertTCPRoute(Object *unstructured.Unstructured, toVersion string) (*unstructured.Unstructured, metav1.Status) { + convertedObject := Object.DeepCopy() + fromVersion := Object.GetAPIVersion() + + if toVersion == fromVersion { + return nil, statusErrorWithMessage("TCPRoute: conversion from a version to itself should not call the webhook: %s", toVersion) + } + + log.Info().Msgf("TCPRoute: successfully converted object") + return convertedObject, statusSucceed() +} diff --git a/pkg/crdconversion/trafficaccessconversion.go b/pkg/crdconversion/trafficaccessconversion.go new file mode 100644 index 0000000000..181a423853 --- /dev/null +++ b/pkg/crdconversion/trafficaccessconversion.go @@ -0,0 +1,27 @@ +package crdconversion + +import ( + "net/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// serveTrafficAccessConversion servers endpoint for the converter defined as convertTrafficAccess function. +func serveTrafficAccessConversion(w http.ResponseWriter, r *http.Request) { + serve(w, r, convertTrafficAccess) +} + +// convertTrafficAccess contains the business logic to convert traffictargets.access.smi-spec.io CRD +// Example implementation reference : https://github.com/kubernetes/kubernetes/blob/release-1.21/test/images/agnhost/crd-conversion-webhook/converter/example_converter.go +func convertTrafficAccess(Object *unstructured.Unstructured, toVersion string) (*unstructured.Unstructured, metav1.Status) { + convertedObject := Object.DeepCopy() + fromVersion := Object.GetAPIVersion() + + if toVersion == fromVersion { + return nil, statusErrorWithMessage("TrafficAccess: conversion from a version to itself should not call the webhook: %s", toVersion) + } + + log.Debug().Msg("TrafficAccess: successfully converted object") + return convertedObject, statusSucceed() +} diff --git a/pkg/crdconversion/trafficsplitconversion.go b/pkg/crdconversion/trafficsplitconversion.go new file mode 100644 index 0000000000..39084ffb16 --- /dev/null +++ b/pkg/crdconversion/trafficsplitconversion.go @@ -0,0 +1,27 @@ +package crdconversion + +import ( + "net/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// serveTrafficSplitConversion servers endpoint for the converter defined as convertTrafficSplit function. +func serveTrafficSplitConversion(w http.ResponseWriter, r *http.Request) { + serve(w, r, convertTrafficSplit) +} + +// convertTrafficSplit contains the business logic to convert trafficsplits.access.smi-spec.io CRD +// Example implementation reference : https://github.com/kubernetes/kubernetes/blob/release-1.21/test/images/agnhost/crd-conversion-webhook/converter/example_converter.go +func convertTrafficSplit(Object *unstructured.Unstructured, toVersion string) (*unstructured.Unstructured, metav1.Status) { + convertedObject := Object.DeepCopy() + fromVersion := Object.GetAPIVersion() + + if toVersion == fromVersion { + return nil, statusErrorWithMessage("TrafficSplit: conversion from a version to itself should not call the webhook: %s", toVersion) + } + + log.Debug().Msg("TrafficSplit: successfully converted object") + return convertedObject, statusSucceed() +}