webhook: use new admission-plugin backed validation and mutation handlers

Signed-off-by: James Munnelly <jmunnelly@apple.com>
This commit is contained in:
James Munnelly 2021-12-15 13:00:52 +00:00
parent 9583050538
commit 708de3c580
11 changed files with 37 additions and 113 deletions

View File

@ -9,20 +9,15 @@ go_library(
"//cmd/util:go_default_library", "//cmd/util:go_default_library",
"//cmd/webhook/app/options:go_default_library", "//cmd/webhook/app/options:go_default_library",
"//internal/apis/config/webhook:go_default_library", "//internal/apis/config/webhook:go_default_library",
"//internal/webhook:go_default_library",
"//pkg/logs:go_default_library", "//pkg/logs:go_default_library",
"//pkg/util:go_default_library", "//pkg/util:go_default_library",
"//pkg/util/feature:go_default_library", "//pkg/util/feature:go_default_library",
"//pkg/webhook:go_default_library", "//pkg/webhook:go_default_library",
"//pkg/webhook/authority:go_default_library", "//pkg/webhook/authority:go_default_library",
"//pkg/webhook/configfile:go_default_library", "//pkg/webhook/configfile:go_default_library",
"//pkg/webhook/handlers:go_default_library",
"//pkg/webhook/server:go_default_library",
"//pkg/webhook/server/tls:go_default_library",
"@com_github_go_logr_logr//:go_default_library",
"@com_github_spf13_cobra//:go_default_library", "@com_github_spf13_cobra//:go_default_library",
"@com_github_spf13_pflag//:go_default_library", "@com_github_spf13_pflag//:go_default_library",
"@io_k8s_client_go//kubernetes:go_default_library",
"@io_k8s_client_go//tools/clientcmd:go_default_library",
"@io_k8s_component_base//cli/flag:go_default_library", "@io_k8s_component_base//cli/flag:go_default_library",
], ],
) )

View File

@ -6,8 +6,8 @@ go_library(
importpath = "github.com/jetstack/cert-manager/cmd/webhook/app/testing", importpath = "github.com/jetstack/cert-manager/cmd/webhook/app/testing",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"//cmd/webhook/app:go_default_library",
"//cmd/webhook/app/options:go_default_library", "//cmd/webhook/app/options:go_default_library",
"//internal/webhook:go_default_library",
"//pkg/util/pki:go_default_library", "//pkg/util/pki:go_default_library",
"//pkg/webhook/server:go_default_library", "//pkg/webhook/server:go_default_library",
"@com_github_go_logr_logr//testing:go_default_library", "@com_github_go_logr_logr//testing:go_default_library",

View File

@ -36,8 +36,8 @@ import (
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/utils/pointer" "k8s.io/utils/pointer"
"github.com/jetstack/cert-manager/cmd/webhook/app"
"github.com/jetstack/cert-manager/cmd/webhook/app/options" "github.com/jetstack/cert-manager/cmd/webhook/app/options"
"github.com/jetstack/cert-manager/internal/webhook"
"github.com/jetstack/cert-manager/pkg/util/pki" "github.com/jetstack/cert-manager/pkg/util/pki"
"github.com/jetstack/cert-manager/pkg/webhook/server" "github.com/jetstack/cert-manager/pkg/webhook/server"
) )
@ -56,7 +56,7 @@ type ServerOptions struct {
CAPEM []byte CAPEM []byte
} }
func StartWebhookServer(t *testing.T, ctx context.Context, args []string, argumentsForNewServerWithOptions ...app.ServerOption) (ServerOptions, StopFunc) { func StartWebhookServer(t *testing.T, ctx context.Context, args []string, argumentsForNewServerWithOptions ...webhook.ServerOption) (ServerOptions, StopFunc) {
log := logtesting.NewTestLogger(t) log := logtesting.NewTestLogger(t)
fs := pflag.NewFlagSet("testset", pflag.ExitOnError) fs := pflag.NewFlagSet("testset", pflag.ExitOnError)
@ -99,7 +99,7 @@ func StartWebhookServer(t *testing.T, ctx context.Context, args []string, argume
webhookConfig.HealthzPort = pointer.Int(0) webhookConfig.HealthzPort = pointer.Int(0)
errCh := make(chan error) errCh := make(chan error)
srv, err := app.NewServerWithOptions(log, *webhookFlags, *webhookConfig, argumentsForNewServerWithOptions...) srv, err := webhook.NewCertManagerWebhookServer(log, *webhookFlags, *webhookConfig, argumentsForNewServerWithOptions...)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -22,104 +22,28 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/go-logr/logr"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
cliflag "k8s.io/component-base/cli/flag" cliflag "k8s.io/component-base/cli/flag"
cmdutil "github.com/jetstack/cert-manager/cmd/util" cmdutil "github.com/jetstack/cert-manager/cmd/util"
"github.com/jetstack/cert-manager/cmd/webhook/app/options" "github.com/jetstack/cert-manager/cmd/webhook/app/options"
config "github.com/jetstack/cert-manager/internal/apis/config/webhook" config "github.com/jetstack/cert-manager/internal/apis/config/webhook"
cmwebhook "github.com/jetstack/cert-manager/internal/webhook"
logf "github.com/jetstack/cert-manager/pkg/logs" logf "github.com/jetstack/cert-manager/pkg/logs"
"github.com/jetstack/cert-manager/pkg/util" "github.com/jetstack/cert-manager/pkg/util"
utilfeature "github.com/jetstack/cert-manager/pkg/util/feature" utilfeature "github.com/jetstack/cert-manager/pkg/util/feature"
"github.com/jetstack/cert-manager/pkg/webhook" "github.com/jetstack/cert-manager/pkg/webhook"
"github.com/jetstack/cert-manager/pkg/webhook/authority" "github.com/jetstack/cert-manager/pkg/webhook/authority"
"github.com/jetstack/cert-manager/pkg/webhook/configfile" "github.com/jetstack/cert-manager/pkg/webhook/configfile"
"github.com/jetstack/cert-manager/pkg/webhook/handlers"
"github.com/jetstack/cert-manager/pkg/webhook/server"
"github.com/jetstack/cert-manager/pkg/webhook/server/tls"
) )
var validationHook handlers.ValidatingAdmissionHook = handlers.NewRegistryBackedValidator(logf.Log, webhook.Scheme, webhook.ValidationRegistry)
var mutationHook handlers.MutatingAdmissionHook = handlers.NewRegistryBackedMutator(logf.Log, webhook.Scheme, webhook.MutationRegistry)
var conversionHook handlers.ConversionHook = handlers.NewSchemeBackedConverter(logf.Log, webhook.Scheme)
type ServerOption func(*server.Server)
// WithConversionHandler allows you to override the handler for the `/convert`
// endpoint in tests.
func WithConversionHandler(handler handlers.ConversionHook) ServerOption {
return func(s *server.Server) {
s.ConversionWebhook = handler
}
}
func NewServerWithOptions(log logr.Logger, _ options.WebhookFlags, opts config.WebhookConfiguration, optionFunctions ...ServerOption) (*server.Server, error) {
restcfg, err := clientcmd.BuildConfigFromFlags(opts.APIServerHost, opts.KubeConfig)
if err != nil {
return nil, err
}
cl, err := kubernetes.NewForConfig(restcfg)
if err != nil {
return nil, fmt.Errorf("error creating kubernetes client: %s", err)
}
validationHook.InitPlugins(cl)
var source tls.CertificateSource
switch {
case opts.TLSConfig.FilesystemConfigProvided():
log.V(logf.InfoLevel).Info("using TLS certificate from local filesystem", "private_key_path", opts.TLSConfig.Filesystem.KeyFile, "certificate", opts.TLSConfig.Filesystem.CertFile)
source = &tls.FileCertificateSource{
CertPath: opts.TLSConfig.Filesystem.CertFile,
KeyPath: opts.TLSConfig.Filesystem.KeyFile,
}
case opts.TLSConfig.DynamicConfigProvided():
restcfg, err := clientcmd.BuildConfigFromFlags("", opts.KubeConfig)
if err != nil {
return nil, err
}
log.V(logf.InfoLevel).Info("using dynamic certificate generating using CA stored in Secret resource", "secret_namespace", opts.TLSConfig.Dynamic.SecretNamespace, "secret_name", opts.TLSConfig.Dynamic.SecretName)
source = &tls.DynamicSource{
DNSNames: opts.TLSConfig.Dynamic.DNSNames,
Authority: &authority.DynamicAuthority{
SecretNamespace: opts.TLSConfig.Dynamic.SecretNamespace,
SecretName: opts.TLSConfig.Dynamic.SecretName,
RESTConfig: restcfg,
},
}
default:
log.V(logf.WarnLevel).Info("serving insecurely as tls certificate data not provided")
}
s := &server.Server{
ListenAddr: fmt.Sprintf(":%d", *opts.SecurePort),
HealthzAddr: fmt.Sprintf(":%d", *opts.HealthzPort),
EnablePprof: opts.EnablePprof,
PprofAddr: opts.PprofAddress,
CertificateSource: source,
CipherSuites: opts.TLSConfig.CipherSuites,
MinTLSVersion: opts.TLSConfig.MinTLSVersion,
ValidationWebhook: validationHook,
MutationWebhook: mutationHook,
ConversionWebhook: conversionHook,
}
for _, f := range optionFunctions {
f(s)
}
return s, nil
}
const componentWebhook = "webhook" const componentWebhook = "webhook"
func NewServerCommand(stopCh <-chan struct{}) *cobra.Command { func NewServerCommand(stopCh <-chan struct{}) *cobra.Command {
ctx := cmdutil.ContextWithStopCh(context.Background(), stopCh) ctx := cmdutil.ContextWithStopCh(context.Background(), stopCh)
log := logf.Log log := logf.Log
ctx = logf.NewContext(ctx, log, "webhook") ctx = logf.NewContext(ctx, log, componentWebhook)
cleanFlagSet := pflag.NewFlagSet(componentWebhook, pflag.ContinueOnError) cleanFlagSet := pflag.NewFlagSet(componentWebhook, pflag.ContinueOnError)
// Replaces all instances of `_` in flag names with `-` // Replaces all instances of `_` in flag names with `-`
@ -195,7 +119,7 @@ func NewServerCommand(stopCh <-chan struct{}) *cobra.Command {
} }
} }
srv, err := NewServerWithOptions(log, *webhookFlags, *webhookConfig) srv, err := cmwebhook.NewCertManagerWebhookServer(log, *webhookFlags, *webhookConfig)
if err != nil { if err != nil {
log.Error(err, "Failed initialising server") log.Error(err, "Failed initialising server")
os.Exit(1) os.Exit(1)

View File

@ -14,6 +14,7 @@ go_library(
"//internal/apis/certmanager/install:go_default_library", "//internal/apis/certmanager/install:go_default_library",
"//internal/apis/config/webhook:go_default_library", "//internal/apis/config/webhook:go_default_library",
"//internal/apis/meta/install:go_default_library", "//internal/apis/meta/install:go_default_library",
"//internal/plugin:go_default_library",
"//pkg/logs:go_default_library", "//pkg/logs:go_default_library",
"//pkg/webhook/admission:go_default_library", "//pkg/webhook/admission:go_default_library",
"//pkg/webhook/admission/initializer:go_default_library", "//pkg/webhook/admission/initializer:go_default_library",

View File

@ -45,9 +45,19 @@ import (
var conversionHook handlers.ConversionHook = handlers.NewSchemeBackedConverter(logf.Log, Scheme) var conversionHook handlers.ConversionHook = handlers.NewSchemeBackedConverter(logf.Log, Scheme)
type ServerOption func(*server.Server)
// WithConversionHandler allows you to override the handler for the `/convert`
// endpoint in tests.
func WithConversionHandler(handler handlers.ConversionHook) ServerOption {
return func(s *server.Server) {
s.ConversionWebhook = handler
}
}
// NewCertManagerWebhookServer creates a new webhook server configured with all cert-manager // NewCertManagerWebhookServer creates a new webhook server configured with all cert-manager
// resource types, validation, defaulting and conversion functions. // resource types, validation, defaulting and conversion functions.
func NewCertManagerWebhookServer(log logr.Logger, _ options.WebhookFlags, opts config.WebhookConfiguration) (*server.Server, error) { func NewCertManagerWebhookServer(log logr.Logger, _ options.WebhookFlags, opts config.WebhookConfiguration, optionFunctions ...ServerOption) (*server.Server, error) {
restcfg, err := clientcmd.BuildConfigFromFlags(opts.APIServerHost, opts.KubeConfig) restcfg, err := clientcmd.BuildConfigFromFlags(opts.APIServerHost, opts.KubeConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -59,12 +69,12 @@ func NewCertManagerWebhookServer(log logr.Logger, _ options.WebhookFlags, opts c
} }
// Set up the admission chain // Set up the admission chain
admissionHandler, err := buildAdmissionChain(log, cl) admissionHandler, err := buildAdmissionChain(cl)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &server.Server{ s := &server.Server{
ListenAddr: fmt.Sprintf(":%d", *opts.SecurePort), ListenAddr: fmt.Sprintf(":%d", *opts.SecurePort),
HealthzAddr: fmt.Sprintf(":%d", *opts.HealthzPort), HealthzAddr: fmt.Sprintf(":%d", *opts.HealthzPort),
EnablePprof: opts.EnablePprof, EnablePprof: opts.EnablePprof,
@ -75,13 +85,16 @@ func NewCertManagerWebhookServer(log logr.Logger, _ options.WebhookFlags, opts c
ValidationWebhook: admissionHandler, ValidationWebhook: admissionHandler,
MutationWebhook: admissionHandler, MutationWebhook: admissionHandler,
ConversionWebhook: conversionHook, ConversionWebhook: conversionHook,
Log: log, }
}, nil for _, fn := range optionFunctions {
fn(s)
}
return s, nil
} }
func buildAdmissionChain(log logr.Logger, client kubernetes.Interface) (*admission.RequestHandler, error) { func buildAdmissionChain(client kubernetes.Interface) (*admission.RequestHandler, error) {
// Set up the admission chain // Set up the admission chain
pluginHandler := admission.NewPlugins(log, Scheme) pluginHandler := admission.NewPlugins(Scheme)
plugin.RegisterAllPlugins(pluginHandler) plugin.RegisterAllPlugins(pluginHandler)
authorizer, err := authorizerfactory.DelegatingAuthorizerConfig{ authorizer, err := authorizerfactory.DelegatingAuthorizerConfig{
SubjectAccessReviewClient: client.AuthorizationV1(), SubjectAccessReviewClient: client.AuthorizationV1(),
@ -115,7 +128,6 @@ func buildCertificateSource(log logr.Logger, tlsConfig config.TLSConfig, restCfg
return &tls.FileCertificateSource{ return &tls.FileCertificateSource{
CertPath: tlsConfig.Filesystem.CertFile, CertPath: tlsConfig.Filesystem.CertFile,
KeyPath: tlsConfig.Filesystem.KeyFile, KeyPath: tlsConfig.Filesystem.KeyFile,
Log: log,
} }
case tlsConfig.DynamicConfigProvided(): case tlsConfig.DynamicConfigProvided():
log.V(logf.InfoLevel).Info("using dynamic certificate generating using CA stored in Secret resource", "secret_namespace", tlsConfig.Dynamic.SecretNamespace, "secret_name", tlsConfig.Dynamic.SecretName) log.V(logf.InfoLevel).Info("using dynamic certificate generating using CA stored in Secret resource", "secret_namespace", tlsConfig.Dynamic.SecretNamespace, "secret_name", tlsConfig.Dynamic.SecretName)
@ -125,9 +137,7 @@ func buildCertificateSource(log logr.Logger, tlsConfig config.TLSConfig, restCfg
SecretNamespace: tlsConfig.Dynamic.SecretNamespace, SecretNamespace: tlsConfig.Dynamic.SecretNamespace,
SecretName: tlsConfig.Dynamic.SecretName, SecretName: tlsConfig.Dynamic.SecretName,
RESTConfig: restCfg, RESTConfig: restCfg,
Log: log,
}, },
Log: log,
} }
default: default:
log.V(logf.WarnLevel).Info("serving insecurely as tls certificate data not provided") log.V(logf.WarnLevel).Info("serving insecurely as tls certificate data not provided")

View File

@ -13,7 +13,6 @@ go_library(
visibility = ["//:__subpackages__"], visibility = ["//:__subpackages__"],
deps = [ deps = [
"//pkg/webhook/handlers:go_default_library", "//pkg/webhook/handlers:go_default_library",
"@com_github_go_logr_logr//:go_default_library",
"@io_k8s_api//admission/v1:go_default_library", "@io_k8s_api//admission/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_apimachinery//pkg/runtime:go_default_library", "@io_k8s_apimachinery//pkg/runtime:go_default_library",
@ -55,7 +54,6 @@ go_test(
], ],
embed = [":go_default_library"], embed = [":go_default_library"],
deps = [ deps = [
"//pkg/logs/testing:go_default_library",
"//pkg/webhook/admission/initializer:go_default_library", "//pkg/webhook/admission/initializer:go_default_library",
"//pkg/webhook/handlers/testdata/apis/testgroup:go_default_library", "//pkg/webhook/handlers/testdata/apis/testgroup:go_default_library",
"//pkg/webhook/handlers/testdata/apis/testgroup/install:go_default_library", "//pkg/webhook/handlers/testdata/apis/testgroup/install:go_default_library",

View File

@ -19,7 +19,6 @@ package admission
import ( import (
"fmt" "fmt"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
) )
@ -27,15 +26,13 @@ import (
// Plugins manages initialising, registering and executing admission plugins // Plugins manages initialising, registering and executing admission plugins
// for both validation and mutation. // for both validation and mutation.
type Plugins struct { type Plugins struct {
log logr.Logger
decoder runtime.Decoder decoder runtime.Decoder
pluginFactory map[string]Factory pluginFactory map[string]Factory
} }
func NewPlugins(log logr.Logger, scheme *runtime.Scheme) *Plugins { func NewPlugins(scheme *runtime.Scheme) *Plugins {
return &Plugins{ return &Plugins{
log: log,
decoder: serializer.NewCodecFactory(scheme).UniversalDecoder(), decoder: serializer.NewCodecFactory(scheme).UniversalDecoder(),
pluginFactory: make(map[string]Factory), pluginFactory: make(map[string]Factory),
} }

View File

@ -25,14 +25,13 @@ import (
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
log "github.com/jetstack/cert-manager/pkg/logs/testing"
"github.com/jetstack/cert-manager/pkg/webhook/admission" "github.com/jetstack/cert-manager/pkg/webhook/admission"
"github.com/jetstack/cert-manager/pkg/webhook/admission/initializer" "github.com/jetstack/cert-manager/pkg/webhook/admission/initializer"
) )
func TestPlugins_InitializesNamedOnly(t *testing.T) { func TestPlugins_InitializesNamedOnly(t *testing.T) {
scheme := runtime.NewScheme() scheme := runtime.NewScheme()
p := admission.NewPlugins(&log.TestLogger{T: t}, scheme) p := admission.NewPlugins(scheme)
testPlugin1 := &testPlugin{} testPlugin1 := &testPlugin{}
p.Register("TestPlugin1", func() (admission.Interface, error) { p.Register("TestPlugin1", func() (admission.Interface, error) {
@ -61,7 +60,7 @@ func TestPlugins_InitializesNamedOnly(t *testing.T) {
func TestPlugins_FailsIfAnyPluginFails(t *testing.T) { func TestPlugins_FailsIfAnyPluginFails(t *testing.T) {
scheme := runtime.NewScheme() scheme := runtime.NewScheme()
p := admission.NewPlugins(&log.TestLogger{T: t}, scheme) p := admission.NewPlugins(scheme)
testPlugin1 := &testPlugin{} testPlugin1 := &testPlugin{}
p.Register("TestPlugin1", func() (admission.Interface, error) { p.Register("TestPlugin1", func() (admission.Interface, error) {
@ -90,7 +89,7 @@ func TestPlugins_FailsIfAnyPluginFails(t *testing.T) {
func TestPlugins_FailsNonExistingPlugin(t *testing.T) { func TestPlugins_FailsNonExistingPlugin(t *testing.T) {
scheme := runtime.NewScheme() scheme := runtime.NewScheme()
p := admission.NewPlugins(&log.TestLogger{T: t}, scheme) p := admission.NewPlugins(scheme)
testPlugin1 := &testPlugin{} testPlugin1 := &testPlugin{}
p.Register("TestPlugin1", func() (admission.Interface, error) { p.Register("TestPlugin1", func() (admission.Interface, error) {
@ -109,7 +108,7 @@ func TestPlugins_FailsNonExistingPlugin(t *testing.T) {
func TestPlugins_FailsIfPluginFailsToBuild(t *testing.T) { func TestPlugins_FailsIfPluginFailsToBuild(t *testing.T) {
scheme := runtime.NewScheme() scheme := runtime.NewScheme()
p := admission.NewPlugins(&log.TestLogger{T: t}, scheme) p := admission.NewPlugins(scheme)
testPlugin1 := &testPlugin{} testPlugin1 := &testPlugin{}
p.Register("TestPlugin1", func() (admission.Interface, error) { p.Register("TestPlugin1", func() (admission.Interface, error) {

View File

@ -9,9 +9,9 @@ go_library(
importpath = "github.com/jetstack/cert-manager/test/integration/framework", importpath = "github.com/jetstack/cert-manager/test/integration/framework",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"//cmd/webhook/app:go_default_library",
"//cmd/webhook/app/testing:go_default_library", "//cmd/webhook/app/testing:go_default_library",
"//internal/test/paths:go_default_library", "//internal/test/paths:go_default_library",
"//internal/webhook:go_default_library",
"//pkg/api:go_default_library", "//pkg/api:go_default_library",
"//pkg/client/clientset/versioned:go_default_library", "//pkg/client/clientset/versioned:go_default_library",
"//pkg/client/informers/externalversions:go_default_library", "//pkg/client/informers/externalversions:go_default_library",

View File

@ -38,9 +38,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/jetstack/cert-manager/cmd/webhook/app"
webhooktesting "github.com/jetstack/cert-manager/cmd/webhook/app/testing" webhooktesting "github.com/jetstack/cert-manager/cmd/webhook/app/testing"
"github.com/jetstack/cert-manager/internal/test/paths" "github.com/jetstack/cert-manager/internal/test/paths"
"github.com/jetstack/cert-manager/internal/webhook"
"github.com/jetstack/cert-manager/pkg/api" "github.com/jetstack/cert-manager/pkg/api"
"github.com/jetstack/cert-manager/pkg/webhook/handlers" "github.com/jetstack/cert-manager/pkg/webhook/handlers"
"github.com/jetstack/cert-manager/test/internal/apiserver" "github.com/jetstack/cert-manager/test/internal/apiserver"
@ -87,7 +87,7 @@ func RunControlPlane(t *testing.T, ctx context.Context, optionFunctions ...RunCo
webhookOpts, stopWebhook := webhooktesting.StartWebhookServer( webhookOpts, stopWebhook := webhooktesting.StartWebhookServer(
t, ctx, []string{"--api-server-host=" + config.Host}, t, ctx, []string{"--api-server-host=" + config.Host},
app.WithConversionHandler(options.webhookConversionHandler), webhook.WithConversionHandler(options.webhookConversionHandler),
) )
crds := readCustomResourcesAtPath(t, *options.crdsDir) crds := readCustomResourcesAtPath(t, *options.crdsDir)