Skip to content

Commit

Permalink
Add webhook
Browse files Browse the repository at this point in the history
Add main.go
Add conversion pkg
  • Loading branch information
qu1queee committed Jul 27, 2023
1 parent 4ea4d17 commit 4d8071f
Show file tree
Hide file tree
Showing 5 changed files with 411 additions and 11 deletions.
109 changes: 109 additions & 0 deletions cmd/shipwright-build-webhook/main.go
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

View workflow job for this annotation

GitHub Actions / e2e (v1.24.7)

cannot find package "." in:

Check failure on line 22 in cmd/shipwright-build-webhook/main.go

View workflow job for this annotation

GitHub Actions / e2e (v1.25.3)

cannot find package "." in:

Check failure on line 22 in cmd/shipwright-build-webhook/main.go

View workflow job for this annotation

GitHub Actions / unit

cannot find package "." in:
)

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)
}
40 changes: 29 additions & 11 deletions pkg/apis/build/v1beta1/build_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ func (srcSpec *BuildSpec) ConvertTo(bs *v1alpha1.BuildSpec) error {
// we only have a single source

// BuildSpec Trigger
for _, t := range srcSpec.Trigger.When {
tw := v1alpha1.TriggerWhen{}
t.convertTo(&tw)
bs.Trigger.When = append(bs.Trigger.When, tw)
if srcSpec.Trigger != nil {
for _, t := range srcSpec.Trigger.When {
tw := v1alpha1.TriggerWhen{}
t.convertTo(&tw)
bs.Trigger.When = append(bs.Trigger.When, tw)
}
}

// BuildSpec Strategy
Expand Down Expand Up @@ -59,7 +61,9 @@ func (srcSpec *BuildSpec) ConvertTo(bs *v1alpha1.BuildSpec) error {
insecure := false
bs.Output.Image = srcSpec.Output.Image
bs.Output.Insecure = &insecure
bs.Output.Credentials.Name = *srcSpec.Output.PushSecret
if srcSpec.Output.PushSecret != nil {
bs.Output.Credentials.Name = *srcSpec.Output.PushSecret
}
bs.Output.Annotations = srcSpec.Output.Annotations
bs.Output.Labels = srcSpec.Output.Labels

Expand All @@ -70,10 +74,19 @@ func (srcSpec *BuildSpec) ConvertTo(bs *v1alpha1.BuildSpec) error {
bs.Env = srcSpec.Env

// BuildSpec Retention
bs.Retention.FailedLimit = srcSpec.Retention.FailedLimit
bs.Retention.SucceededLimit = srcSpec.Retention.SucceededLimit
bs.Retention.TTLAfterFailed = srcSpec.Retention.TTLAfterFailed
bs.Retention.TTLAfterSucceeded = srcSpec.Retention.TTLAfterSucceeded
if srcSpec.Retention != nil && srcSpec.Retention.FailedLimit != nil {
bs.Retention.FailedLimit = srcSpec.Retention.FailedLimit
}
if srcSpec.Retention != nil && srcSpec.Retention.SucceededLimit != nil {

bs.Retention.SucceededLimit = srcSpec.Retention.SucceededLimit
}
if srcSpec.Retention != nil && srcSpec.Retention.TTLAfterFailed != nil {
bs.Retention.TTLAfterFailed = srcSpec.Retention.TTLAfterFailed
}
if srcSpec.Retention != nil && srcSpec.Retention.TTLAfterSucceeded != nil {
bs.Retention.TTLAfterSucceeded = srcSpec.Retention.TTLAfterSucceeded
}

// BuildSpec Volumes
for i, vol := range srcSpec.Volumes {
Expand All @@ -93,6 +106,9 @@ func (dst *Build) ConvertFrom(srcRaw conversion.Hub) error {

// todo: could be placed in its own file
func (p ParamValue) convertTo(dest *v1alpha1.ParamValue) {
if p.SingleValue == nil || p.SingleValue.Value == nil {
return
}
dest.Value = p.Value
dest.ConfigMapValue = (*v1alpha1.ObjectKeyRef)(p.ConfigMapValue)
dest.SecretValue = (*v1alpha1.ObjectKeyRef)(p.SecretValue)
Expand Down Expand Up @@ -137,8 +153,10 @@ func getBuildSource(src BuildSpec) v1alpha1.Source {
Prune: (*v1alpha1.PruneOption)(src.Source.OCIArtifact.Prune),
}
default:
credentials = corev1.LocalObjectReference{
Name: *src.Source.GitSource.CloneSecret,
if *&src.Source.GitSource.CloneSecret != nil {
credentials = corev1.LocalObjectReference{
Name: *src.Source.GitSource.CloneSecret,
}
}
source.URL = src.Source.GitSource.URL
revision = src.Source.GitSource.Revision
Expand Down
193 changes: 193 additions & 0 deletions pkg/webhook/conversion/conversion.go
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

View workflow job for this annotation

GitHub Actions / e2e (v1.24.7)

cannot find package "." in:

Check failure on line 22 in pkg/webhook/conversion/conversion.go

View workflow job for this annotation

GitHub Actions / e2e (v1.25.3)

cannot find package "." in:

Check failure on line 22 in pkg/webhook/conversion/conversion.go

View workflow job for this annotation

GitHub Actions / unit

cannot find package "." in:
beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"

Check failure on line 23 in pkg/webhook/conversion/conversion.go

View workflow job for this annotation

GitHub Actions / e2e (v1.24.7)

cannot find package "." in:

Check failure on line 23 in pkg/webhook/conversion/conversion.go

View workflow job for this annotation

GitHub Actions / e2e (v1.25.3)

cannot find package "." in:

Check failure on line 23 in pkg/webhook/conversion/conversion.go

View workflow job for this annotation

GitHub Actions / unit

cannot find package "." in:
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,
}
}
Loading

0 comments on commit 4d8071f

Please sign in to comment.