Skip to content

Commit 2c24442

Browse files
committed
StandaloneWebhook can run on any arbitrary mux
1 parent 83846f5 commit 2c24442

File tree

3 files changed

+122
-26
lines changed

3 files changed

+122
-26
lines changed

pkg/webhook/admission/webhook.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@ import (
2222
"net/http"
2323

2424
"github.com/go-logr/logr"
25+
"github.com/prometheus/client_golang/prometheus"
26+
"github.com/prometheus/client_golang/prometheus/promhttp"
2527
jsonpatch "gomodules.xyz/jsonpatch/v2"
2628
admissionv1 "k8s.io/api/admission/v1"
2729
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2830
"k8s.io/apimachinery/pkg/runtime"
2931
"k8s.io/apimachinery/pkg/util/json"
32+
"k8s.io/client-go/kubernetes/scheme"
3033

34+
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
3135
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
36+
"sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics"
3237
)
3338

3439
var (
@@ -203,3 +208,62 @@ func (w *Webhook) InjectFunc(f inject.Func) error {
203208

204209
return setFields(w.Handler)
205210
}
211+
212+
// InstrumentedHook adds some instrumentation on top of the given webhook.
213+
func InstrumentedHook(path string, hookRaw http.Handler) http.Handler {
214+
lbl := prometheus.Labels{"webhook": path}
215+
216+
lat := metrics.RequestLatency.MustCurryWith(lbl)
217+
cnt := metrics.RequestTotal.MustCurryWith(lbl)
218+
gge := metrics.RequestInFlight.With(lbl)
219+
220+
// Initialize the most likely HTTP status codes.
221+
cnt.WithLabelValues("200")
222+
cnt.WithLabelValues("500")
223+
224+
return promhttp.InstrumentHandlerDuration(
225+
lat,
226+
promhttp.InstrumentHandlerCounter(
227+
cnt,
228+
promhttp.InstrumentHandlerInFlight(gge, hookRaw),
229+
),
230+
)
231+
}
232+
233+
// StandaloneOptions let you configure a StandaloneWebhook.
234+
type StandaloneOptions struct {
235+
// Scheme is the scheme used to resolve runtime.Objects to GroupVersionKinds / Resources
236+
// Defaults to the kubernetes/client-go scheme.Scheme, but it's almost always better
237+
// idea to pass your own scheme in. See the documentation in pkg/scheme for more information.
238+
Scheme *runtime.Scheme
239+
// Logger to be used by the webhook.
240+
// If none is set, it defaults to log.Log global logger.
241+
Logger logr.Logger
242+
// Path the webhook will be served at.
243+
// Used for labelling prometheus metrics.
244+
Path string
245+
}
246+
247+
// StandaloneWebhook transforms a Webhook that needs to be registered
248+
// on a webhook.Server into one that can be ran on any arbitrary mux.
249+
func StandaloneWebhook(hook *Webhook, opts StandaloneOptions) (http.Handler, error) {
250+
if opts.Scheme == nil {
251+
opts.Scheme = scheme.Scheme
252+
}
253+
254+
var err error
255+
hook.decoder, err = NewDecoder(opts.Scheme)
256+
if err != nil {
257+
return nil, err
258+
}
259+
260+
if opts.Logger == nil {
261+
opts.Logger = logf.RuntimeLog.WithName("webhook")
262+
}
263+
hook.log = opts.Logger
264+
265+
if opts.Path == "" {
266+
return hook, nil
267+
}
268+
return InstrumentedHook(opts.Path, hook), nil
269+
}

pkg/webhook/server.go

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,11 @@ import (
2929
"strconv"
3030
"sync"
3131

32-
"github.com/prometheus/client_golang/prometheus"
33-
"github.com/prometheus/client_golang/prometheus/promhttp"
3432
"k8s.io/apimachinery/pkg/runtime"
3533
"k8s.io/client-go/kubernetes/scheme"
3634
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
3735
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
38-
"sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics"
36+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3937
)
4038

4139
// DefaultPort is the default port that the webhook server serves.
@@ -187,7 +185,7 @@ func (s *Server) Register(path string, hook http.Handler) {
187185
}
188186
// TODO(directxman12): call setfields if we've already started the server
189187
s.webhooks[path] = hook
190-
s.WebhookMux.Handle(path, instrumentedHook(path, hook))
188+
s.WebhookMux.Handle(path, admission.InstrumentedHook(path, hook))
191189

192190
regLog := log.WithValues("path", path)
193191
regLog.Info("registering webhook")
@@ -212,27 +210,6 @@ func (s *Server) Register(path string, hook http.Handler) {
212210
}
213211
}
214212

215-
// instrumentedHook adds some instrumentation on top of the given webhook.
216-
func instrumentedHook(path string, hookRaw http.Handler) http.Handler {
217-
lbl := prometheus.Labels{"webhook": path}
218-
219-
lat := metrics.RequestLatency.MustCurryWith(lbl)
220-
cnt := metrics.RequestTotal.MustCurryWith(lbl)
221-
gge := metrics.RequestInFlight.With(lbl)
222-
223-
// Initialize the most likely HTTP status codes.
224-
cnt.WithLabelValues("200")
225-
cnt.WithLabelValues("500")
226-
227-
return promhttp.InstrumentHandlerDuration(
228-
lat,
229-
promhttp.InstrumentHandlerCounter(
230-
cnt,
231-
promhttp.InstrumentHandlerInFlight(gge, hookRaw),
232-
),
233-
)
234-
}
235-
236213
// Start runs the server.
237214
// It will install the webhook related resources depend on the server configuration.
238215
func (s *Server) Start(ctx context.Context) error {

pkg/webhook/webhook_integration_test.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ package webhook_test
22

33
import (
44
"context"
5+
"crypto/tls"
56
"fmt"
7+
"net"
8+
"net/http"
9+
"path/filepath"
10+
"strconv"
611
"time"
712

813
. "github.com/onsi/ginkgo"
@@ -11,6 +16,7 @@ import (
1116
corev1 "k8s.io/api/core/v1"
1217
"k8s.io/apimachinery/pkg/api/errors"
1318
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
1420
"sigs.k8s.io/controller-runtime/pkg/client"
1521
"sigs.k8s.io/controller-runtime/pkg/manager"
1622
"sigs.k8s.io/controller-runtime/pkg/webhook"
@@ -78,7 +84,7 @@ var _ = Describe("Webhook", func() {
7884
close(done)
7985
})
8086
})
81-
Context("when running a webhook server without a manager ", func() {
87+
Context("when running a webhook server without a manager", func() {
8288
It("should reject create request for webhook that rejects all requests", func(done Done) {
8389
opts := webhook.Options{
8490
Port: testenv.WebhookInstallOptions.LocalServingPort,
@@ -99,6 +105,55 @@ var _ = Describe("Webhook", func() {
99105
return errors.ReasonForError(err) == metav1.StatusReason("Always denied")
100106
}, 1*time.Second).Should(BeTrue())
101107

108+
cancel()
109+
close(done)
110+
})
111+
})
112+
Context("when running a standalone webhook", func() {
113+
It("should reject create request for webhook that rejects all requests", func(done Done) {
114+
ctx, cancel := context.WithCancel(context.Background())
115+
// generate tls cfg
116+
certPath := filepath.Join(testenv.WebhookInstallOptions.LocalServingCertDir, "tls.crt")
117+
keyPath := filepath.Join(testenv.WebhookInstallOptions.LocalServingCertDir, "tls.key")
118+
119+
certWatcher, err := certwatcher.New(certPath, keyPath)
120+
Expect(err).NotTo(HaveOccurred())
121+
go func() {
122+
Expect(certWatcher.Start(ctx)).NotTo(HaveOccurred())
123+
}()
124+
125+
cfg := &tls.Config{
126+
NextProtos: []string{"h2"},
127+
GetCertificate: certWatcher.GetCertificate,
128+
}
129+
130+
// generate listener
131+
listener, err := tls.Listen("tcp", net.JoinHostPort(testenv.WebhookInstallOptions.LocalServingHost, strconv.Itoa(int(testenv.WebhookInstallOptions.LocalServingPort))), cfg)
132+
Expect(err).NotTo(HaveOccurred())
133+
134+
// create and register the standalone webhook
135+
hook, err := admission.StandaloneWebhook(&webhook.Admission{Handler: &rejectingValidator{}}, admission.StandaloneOptions{})
136+
Expect(err).NotTo(HaveOccurred())
137+
http.Handle("/failing", hook)
138+
139+
// run the http server
140+
srv := &http.Server{}
141+
go func() {
142+
idleConnsClosed := make(chan struct{})
143+
go func() {
144+
<-ctx.Done()
145+
Expect(srv.Shutdown(context.Background())).NotTo(HaveOccurred())
146+
close(idleConnsClosed)
147+
}()
148+
srv.Serve(listener)
149+
<-idleConnsClosed
150+
}()
151+
152+
Eventually(func() bool {
153+
err = c.Create(context.TODO(), obj)
154+
return errors.ReasonForError(err) == metav1.StatusReason("Always denied")
155+
}, 1*time.Second).Should(BeTrue())
156+
102157
cancel()
103158
close(done)
104159
})

0 commit comments

Comments
 (0)