Skip to content

Commit

Permalink
feat(upgrade): add handlers for conversion of CRD's in OSM
Browse files Browse the repository at this point in the history
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 openservicemesh#3396

Signed-off-by: Sneha Chhabria <snchh@microsoft.com>
  • Loading branch information
snehachhabria committed Jul 19, 2021
1 parent 80fa5ad commit 90d10de
Show file tree
Hide file tree
Showing 12 changed files with 382 additions and 3 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
12 changes: 9 additions & 3 deletions pkg/crdconversion/crdconversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -108,7 +115,6 @@ func (crdWh *crdConversionWebhook) run(stop <-chan struct{}) {
}()

healthMux := http.NewServeMux()

healthMux.HandleFunc(webhookHealthPath, healthHandler)

healthServer := &http.Server{
Expand Down
27 changes: 27 additions & 0 deletions pkg/crdconversion/egresspolicyconversion.go
Original file line number Diff line number Diff line change
@@ -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()
}
155 changes: 155 additions & 0 deletions pkg/crdconversion/framework.go
Original file line number Diff line number Diff line change
@@ -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
}
27 changes: 27 additions & 0 deletions pkg/crdconversion/httproutegroupconversion.go
Original file line number Diff line number Diff line change
@@ -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()
}
27 changes: 27 additions & 0 deletions pkg/crdconversion/ingressbackendspolicyconversion.go
Original file line number Diff line number Diff line change
@@ -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()
}
27 changes: 27 additions & 0 deletions pkg/crdconversion/meshconfigconversion.go
Original file line number Diff line number Diff line change
@@ -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()
}
27 changes: 27 additions & 0 deletions pkg/crdconversion/multiclusterserviceconversion.go
Original file line number Diff line number Diff line change
@@ -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()
}
27 changes: 27 additions & 0 deletions pkg/crdconversion/tcproutesconversion.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading

0 comments on commit 90d10de

Please sign in to comment.