-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
Add main.go Add conversion pkg
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
// Copyright The Shipwright Contributors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package main | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"flag" | ||
"fmt" | ||
"net/http" | ||
"os" | ||
"path" | ||
"runtime" | ||
"time" | ||
|
||
"github.com/shipwright-io/build/pkg/ctxlog" | ||
"github.com/shipwright-io/build/pkg/webhook/conversion" | ||
"github.com/shipwright-io/build/version" | ||
"github.com/spf13/pflag" | ||
"knative.dev/pkg/signals" | ||
Check failure on line 22 in cmd/shipwright-build-webhook/main.go GitHub Actions / e2e (v1.24.7)
Check failure on line 22 in cmd/shipwright-build-webhook/main.go GitHub Actions / e2e (v1.25.3)
|
||
) | ||
|
||
var ( | ||
versionGiven = flag.String("version", "devel", "Version of Shipwright webhook running") | ||
) | ||
|
||
func printVersion(ctx context.Context) { | ||
ctxlog.Info(ctx, fmt.Sprintf("Shipwright Build Webhook Version: %s", version.Version)) | ||
ctxlog.Info(ctx, fmt.Sprintf("Go Version: %s", runtime.Version())) | ||
ctxlog.Info(ctx, fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) | ||
} | ||
|
||
func main() { | ||
// Add the zap logger flag set to the CLI. The flag set must | ||
// be added before calling pflag.Parse(). | ||
pflag.CommandLine.AddGoFlagSet(ctxlog.CustomZapFlagSet()) | ||
|
||
// Add flags registered by imported packages (e.g. glog and | ||
// controller-runtime) | ||
pflag.CommandLine.AddGoFlagSet(flag.CommandLine) | ||
|
||
pflag.Parse() | ||
|
||
if err := Execute(); err != nil { | ||
os.Exit(1) | ||
} | ||
|
||
} | ||
|
||
func Execute() error { | ||
l := ctxlog.NewLogger("shp-build-webhook") | ||
|
||
ctx := ctxlog.NewParentContext(l) | ||
|
||
version.SetVersion(*versionGiven) | ||
printVersion(ctx) | ||
|
||
mux := http.NewServeMux() | ||
mux.HandleFunc("/health", health) | ||
ctxlog.Info(ctx, "adding handlefunc() /health") | ||
|
||
// convert endpoint handles ConversionReview API object serialized to JSON | ||
mux.HandleFunc("/convert", conversion.CRDConvertHandler(ctx)) | ||
ctxlog.Info(ctx, "adding handlefunc() /convert") | ||
|
||
server := &http.Server{ | ||
Addr: ":8443", | ||
Handler: mux, | ||
ReadHeaderTimeout: 32 * time.Second, | ||
TLSConfig: &tls.Config{ | ||
MinVersion: tls.VersionTLS12, | ||
CurvePreferences: []tls.CurveID{tls.CurveP256, tls.CurveP384, tls.X25519}, | ||
CipherSuites: []uint16{ | ||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | ||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | ||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, | ||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | ||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | ||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, | ||
}, | ||
}, | ||
} | ||
|
||
go func() { | ||
ctxlog.Info(ctx, "starting webhook server") | ||
// blocking call, returns on error | ||
if err := server.ListenAndServeTLS(path.Join("/tmp/k8s-webhook-server/serving-certs", "tls.crt"), path.Join("/tmp/k8s-webhook-server/serving-certs", "tls.key")); err != nil { | ||
ctxlog.Error(ctx, err, "webhook server failed to start") | ||
} | ||
}() | ||
|
||
stopCh := signals.SetupSignalHandler() | ||
sig := <-stopCh | ||
|
||
l.Info("Shutting down server.", "signal", sig) | ||
ctxlog.Info(ctx, "shutting down webhook server,", "signal:", sig) | ||
if err := server.Shutdown(context.Background()); err != nil { | ||
l.Error(err, "Failed to gracefully shutdown the server.") | ||
return err | ||
} | ||
return nil | ||
|
||
} | ||
|
||
func health(resp http.ResponseWriter, req *http.Request) { | ||
resp.WriteHeader(http.StatusNoContent) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
// Copyright The Shipwright Contributors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package conversion | ||
|
||
/* | ||
* This code is influenced by the conversion webhook example | ||
* tested in the Kubernetes E2E(see https://github.com/kubernetes/kubernetes/tree/v1.25.3/test/images/agnhost/crd-conversion-webhook/converter), | ||
* as mentioned in the Kubernetes official documentation: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#write-a-conversion-webhook-server | ||
*/ | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/munnerz/goautoneg" | ||
"github.com/shipwright-io/build/pkg/ctxlog" | ||
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | ||
Check failure on line 22 in pkg/webhook/conversion/conversion.go GitHub Actions / e2e (v1.24.7)
Check failure on line 22 in pkg/webhook/conversion/conversion.go GitHub Actions / e2e (v1.25.3)
|
||
beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" | ||
Check failure on line 23 in pkg/webhook/conversion/conversion.go GitHub Actions / e2e (v1.24.7)
Check failure on line 23 in pkg/webhook/conversion/conversion.go GitHub Actions / e2e (v1.25.3)
|
||
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" | ||
utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||
) | ||
|
||
var scheme = runtime.NewScheme() | ||
|
||
func init() { | ||
addToScheme(scheme) | ||
} | ||
|
||
func addToScheme(scheme *runtime.Scheme) { | ||
utilruntime.Must(v1.AddToScheme(scheme)) | ||
utilruntime.Must(beta1.AddToScheme(scheme)) | ||
|
||
} | ||
|
||
var serializers = map[mediaType]runtime.Serializer{ | ||
{"application", "json"}: json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, json.SerializerOptions{Pretty: false}), | ||
{"application", "yaml"}: json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, json.SerializerOptions{Yaml: true}), | ||
} | ||
|
||
type mediaType struct { | ||
Type, SubType string | ||
} | ||
|
||
// convertFunc serves as the Custom Resource conversiob function | ||
type convertFunc func(Object *unstructured.Unstructured, version string, ctx context.Context) (*unstructured.Unstructured, metav1.Status) | ||
|
||
func CRDConvertHandler(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { | ||
return func(w http.ResponseWriter, r *http.Request) { | ||
CRDConvert(w, r, ctx) | ||
} | ||
} | ||
|
||
func CRDConvert(w http.ResponseWriter, r *http.Request, ctx context.Context) { | ||
serve(w, r, convertSHPCR, ctx) | ||
} | ||
|
||
// serve handles a ConversionReview object type, it will process a ConversionRequest object | ||
// and convert that into a ConversionResponse one. | ||
func serve(w http.ResponseWriter, r *http.Request, convert convertFunc, ctx context.Context) { | ||
var body []byte | ||
if r.Body != nil { | ||
if data, err := io.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) | ||
ctxlog.Error(ctx, errors.New(msg), "invalid header") | ||
http.Error(w, msg, http.StatusBadRequest) | ||
return | ||
} | ||
|
||
ctxlog.Info(ctx, "handling request: %v", body) | ||
|
||
obj, gvk, err := serializer.Decode(body, nil, nil) | ||
if err != nil { | ||
msg := fmt.Sprintf("failed to deserialize body (%v) with error %v", string(body), err) | ||
ctxlog.Error(ctx, errors.New(msg), "failed to deserialize") | ||
http.Error(w, msg, http.StatusBadRequest) | ||
return | ||
} | ||
var responseObj runtime.Object | ||
switch *gvk { | ||
case v1.SchemeGroupVersion.WithKind("ConversionReview"): | ||
convertReview, ok := obj.(*v1.ConversionReview) | ||
if !ok { | ||
msg := fmt.Sprintf("Expected v1beta1.ConversionReview but got: %T", obj) | ||
ctxlog.Error(ctx, errors.New(msg), "unexpected kind") | ||
http.Error(w, msg, http.StatusBadRequest) | ||
return | ||
} | ||
convertReview.Response = doConversionToV1alpha1(convertReview.Request, convert, ctx) | ||
convertReview.Response.UID = convertReview.Request.UID | ||
ctxlog.Info(ctx, fmt.Sprintf("sending response: %v", convertReview.Response)) | ||
|
||
convertReview.Request = &v1.ConversionRequest{} | ||
responseObj = convertReview | ||
default: | ||
msg := fmt.Sprintf("Unsupported group version kind: %v", gvk) | ||
ctxlog.Error(ctx, errors.New(msg), "unknown group/version/kind") | ||
http.Error(w, msg, http.StatusBadRequest) | ||
return | ||
} | ||
|
||
accept := r.Header.Get("Accept") | ||
outSerializer := getOutputSerializer(accept) | ||
if outSerializer == nil { | ||
msg := fmt.Sprintf("invalid accept header `%s`", accept) | ||
ctxlog.Error(ctx, errors.New(msg), "invalid header") | ||
http.Error(w, msg, http.StatusBadRequest) | ||
return | ||
} | ||
err = outSerializer.Encode(responseObj, w) | ||
if err != nil { | ||
ctxlog.Error(ctx, err, "outserializer enconding failed") | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
} | ||
|
||
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 | ||
} | ||
|
||
// doConversionToV1alpha1 takes the v1beta1 CR in the v1 ConversionRequest using the convert function | ||
// and returns a ConversionResponse with a v1alpha1 CR | ||
func doConversionToV1alpha1(convertRequest *v1.ConversionRequest, convert convertFunc, ctx context.Context) *v1.ConversionResponse { | ||
var convertedObjects []runtime.RawExtension | ||
for _, obj := range convertRequest.Objects { | ||
cr := unstructured.Unstructured{} | ||
if err := cr.UnmarshalJSON(obj.Raw); err != nil { | ||
ctxlog.Error(ctx, err, "unmarshalling json on convertrequest") | ||
return &v1.ConversionResponse{ | ||
Result: metav1.Status{ | ||
Message: fmt.Sprintf("failed to unmarshall object (%v) with error: %v", string(obj.Raw), err), | ||
Status: metav1.StatusFailure, | ||
}, | ||
} | ||
} | ||
convertedCR, status := convert(&cr, convertRequest.DesiredAPIVersion, ctx) | ||
if status.Status != metav1.StatusSuccess { | ||
ctxlog.Error(ctx, errors.New(status.String()), "status is not success") | ||
return &v1.ConversionResponse{ | ||
Result: status, | ||
} | ||
} | ||
convertedCR.SetAPIVersion(convertRequest.DesiredAPIVersion) | ||
convertedObjects = append(convertedObjects, runtime.RawExtension{Object: convertedCR}) | ||
} | ||
return &v1.ConversionResponse{ | ||
ConvertedObjects: convertedObjects, | ||
Result: statusSucceed(), | ||
} | ||
} | ||
|
||
func statusSucceed() metav1.Status { | ||
return metav1.Status{ | ||
Status: metav1.StatusSuccess, | ||
} | ||
} |