Skip to content

Commit 0208f43

Browse files
author
Mengqi Yu
committed
⚠️ wire wehbooks if implements the interfaces
If user defines a CRD type and make it implement the Defaulter and (or) the Validator interface, it will automatically wire a webhook server for the admission webhooks.
1 parent 41ba537 commit 0208f43

File tree

10 files changed

+304
-10
lines changed

10 files changed

+304
-10
lines changed

Gopkg.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/builder/build.go

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"sigs.k8s.io/controller-runtime/pkg/predicate"
3131
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3232
"sigs.k8s.io/controller-runtime/pkg/source"
33+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3334
)
3435

3536
// Supporting mocking out functions for testing
@@ -65,6 +66,8 @@ func ControllerManagedBy(m manager.Manager) *Builder {
6566
// update events by *reconciling the object*.
6667
// This is the equivalent of calling
6768
// Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{})
69+
// If the passed in object has implemented the admission.Defaulter interface, a MutatingWebhook will be wired for this type.
70+
// If the passed in object has implemented the admission.Validator interface, a ValidatingWebhook will be wired for this type.
6871
//
6972
// Deprecated: Use For
7073
func (blder *Builder) ForType(apiType runtime.Object) *Builder {
@@ -75,6 +78,8 @@ func (blder *Builder) ForType(apiType runtime.Object) *Builder {
7578
// update events by *reconciling the object*.
7679
// This is the equivalent of calling
7780
// Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{})
81+
// If the passed in object has implemented the admission.Defaulter interface, a MutatingWebhook will be wired for this type.
82+
// If the passed in object has implemented the admission.Validator interface, a ValidatingWebhook will be wired for this type.
7883
func (blder *Builder) For(apiType runtime.Object) *Builder {
7984
blder.apiType = apiType
8085
return blder
@@ -124,7 +129,7 @@ func (blder *Builder) WithEventFilter(p predicate.Predicate) *Builder {
124129
return blder
125130
}
126131

127-
// Complete builds the Application ControllerManagedBy and returns the Manager used to start it.
132+
// Complete builds the Application ControllerManagedBy.
128133
func (blder *Builder) Complete(r reconcile.Reconciler) error {
129134
_, err := blder.Build(r)
130135
return err
@@ -135,7 +140,7 @@ func (blder *Builder) Complete(r reconcile.Reconciler) error {
135140
// Deprecated: Use Complete
136141
func (blder *Builder) Build(r reconcile.Reconciler) (manager.Manager, error) {
137142
if r == nil {
138-
return nil, fmt.Errorf("must call WithReconciler to set Reconciler")
143+
return nil, fmt.Errorf("must provide a non-nil Reconciler")
139144
}
140145

141146
// Set the Config
@@ -153,12 +158,26 @@ func (blder *Builder) Build(r reconcile.Reconciler) (manager.Manager, error) {
153158
return nil, err
154159
}
155160

161+
// Set the Webook if needed
162+
if err := blder.doWebhook(); err != nil {
163+
return nil, err
164+
}
165+
166+
// Set the Watch
167+
if err := blder.doWatch(); err != nil {
168+
return nil, err
169+
}
170+
171+
return blder.mgr, nil
172+
}
173+
174+
func (blder *Builder) doWatch() error {
156175
// Reconcile type
157176
src := &source.Kind{Type: blder.apiType}
158177
hdler := &handler.EnqueueRequestForObject{}
159178
err := blder.ctrl.Watch(src, hdler, blder.predicates...)
160179
if err != nil {
161-
return nil, err
180+
return err
162181
}
163182

164183
// Watches the managed types
@@ -169,19 +188,18 @@ func (blder *Builder) Build(r reconcile.Reconciler) (manager.Manager, error) {
169188
IsController: true,
170189
}
171190
if err := blder.ctrl.Watch(src, hdler, blder.predicates...); err != nil {
172-
return nil, err
191+
return err
173192
}
174193
}
175194

176195
// Do the watch requests
177196
for _, w := range blder.watchRequest {
178197
if err := blder.ctrl.Watch(w.src, w.eventhandler, blder.predicates...); err != nil {
179-
return nil, err
198+
return err
180199
}
181200

182201
}
183-
184-
return blder.mgr, nil
202+
return nil
185203
}
186204

187205
func (blder *Builder) doConfig() error {
@@ -223,3 +241,51 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
223241
blder.ctrl, err = newController(name, blder.mgr, controller.Options{Reconciler: r})
224242
return err
225243
}
244+
245+
func (blder *Builder) doWebhook() error {
246+
// Create a webhook for each type
247+
gvk, err := apiutil.GVKForObject(blder.apiType, blder.mgr.GetScheme())
248+
if err != nil {
249+
return err
250+
}
251+
252+
partialPath := strings.Replace(gvk.Group, ".", "-", -1) + "-" +
253+
gvk.Version + "-" + strings.ToLower(gvk.Kind)
254+
255+
// TODO: When the conversion webhook lands, we need to handle all registered versions of a given group-kind.
256+
// A potential workflow for defaulting webhook
257+
// 1) a bespoke (non-hub) version comes in
258+
// 2) convert it to the hub version
259+
// 3) do defaulting
260+
// 4) convert it back to the same bespoke version
261+
// 5) calculate the JSON patch
262+
//
263+
// A potential workflow for validating webhook
264+
// 1) a bespoke (non-hub) version comes in
265+
// 2) convert it to the hub version
266+
// 3) do validation
267+
if defaulter, isDefaulter := blder.apiType.(admission.Defaulter); isDefaulter {
268+
mwh := admission.DefaultingWebhookFor(defaulter)
269+
if mwh != nil {
270+
path := "/mutate-" + partialPath
271+
log.Info("Registering a mutating webhook",
272+
"GVK", gvk,
273+
"path", path)
274+
275+
blder.mgr.GetWebhookServer().Register(path, mwh)
276+
}
277+
}
278+
279+
if validator, isValidator := blder.apiType.(admission.Validator); isValidator {
280+
vwh := admission.ValidatingWebhookFor(validator)
281+
if vwh != nil {
282+
path := "/validate-" + partialPath
283+
log.Info("Registering a validating webhook",
284+
"GVK", gvk,
285+
"path", path)
286+
blder.mgr.GetWebhookServer().Register(path, vwh)
287+
}
288+
}
289+
290+
return err
291+
}

pkg/builder/builder_suite_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@ import (
2121

2222
. "github.com/onsi/ginkgo"
2323
. "github.com/onsi/gomega"
24+
2425
"k8s.io/client-go/rest"
2526
"sigs.k8s.io/controller-runtime/pkg/envtest"
2627
logf "sigs.k8s.io/controller-runtime/pkg/log"
2728
"sigs.k8s.io/controller-runtime/pkg/log/zap"
2829
"sigs.k8s.io/controller-runtime/pkg/metrics"
30+
"sigs.k8s.io/controller-runtime/pkg/webhook"
31+
"sigs.k8s.io/testing_frameworks/integration/addr"
2932
)
3033

31-
func TestSource(t *testing.T) {
34+
func TestBuilder(t *testing.T) {
3235
RegisterFailHandler(Fail)
3336
RunSpecsWithDefaultAndCustomReporters(t, "application Suite", []Reporter{envtest.NewlineReporter{}})
3437
}
@@ -48,6 +51,9 @@ var _ = BeforeSuite(func(done Done) {
4851
// Prevent the metrics listener being created
4952
metrics.DefaultBindAddress = "0"
5053

54+
webhook.DefaultPort, _, err = addr.Suggest()
55+
Expect(err).NotTo(HaveOccurred())
56+
5157
close(done)
5258
}, 60)
5359

@@ -56,4 +62,7 @@ var _ = AfterSuite(func() {
5662

5763
// Put the DefaultBindAddress back
5864
metrics.DefaultBindAddress = ":8080"
65+
66+
// Change the webhook.DefaultPort back to the original default.
67+
webhook.DefaultPort = 443
5968
})

pkg/builder/doc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,9 @@ limitations under the License.
2020
// Projects built with the builder package can trivially be rebased on top of the underlying
2121
// packages if the project requires more customized behavior in the future.
2222
package builder
23+
24+
import (
25+
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
26+
)
27+
28+
var log = logf.RuntimeLog.WithName("builder")

pkg/manager/internal.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"sigs.k8s.io/controller-runtime/pkg/metrics"
3838
"sigs.k8s.io/controller-runtime/pkg/recorder"
3939
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
40+
"sigs.k8s.io/controller-runtime/pkg/webhook"
4041
)
4142

4243
var log = logf.RuntimeLog.WithName("manager")
@@ -93,6 +94,13 @@ type controllerManager struct {
9394
internalStopper chan<- struct{}
9495

9596
startCache func(stop <-chan struct{}) error
97+
98+
// port is the port that the webhook server serves at.
99+
port int
100+
// host is the hostname that the webhook server binds to.
101+
host string
102+
103+
webhookServer *webhook.Server
96104
}
97105

98106
// Add sets dependencies on i, and adds it to the list of runnables to start.
@@ -177,6 +185,19 @@ func (cm *controllerManager) GetAPIReader() client.Reader {
177185
return cm.apiReader
178186
}
179187

188+
func (cm *controllerManager) GetWebhookServer() *webhook.Server {
189+
if cm.webhookServer == nil {
190+
cm.webhookServer = &webhook.Server{
191+
Port: cm.port,
192+
Host: cm.host,
193+
}
194+
if err := cm.Add(cm.webhookServer); err != nil {
195+
panic("unable to add webhookServer to the controller manager")
196+
}
197+
}
198+
return cm.webhookServer
199+
}
200+
180201
func (cm *controllerManager) serveMetrics(stop <-chan struct{}) {
181202
handler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{
182203
ErrorHandling: promhttp.HTTPErrorOnError,

pkg/manager/manager.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"sigs.k8s.io/controller-runtime/pkg/leaderelection"
3737
"sigs.k8s.io/controller-runtime/pkg/metrics"
3838
"sigs.k8s.io/controller-runtime/pkg/recorder"
39+
"sigs.k8s.io/controller-runtime/pkg/webhook"
3940
)
4041

4142
// Manager initializes shared dependencies such as Caches and Clients, and provides them to Runnables.
@@ -79,6 +80,9 @@ type Manager interface {
7980
// This should be used sparingly and only when the client does not fit your
8081
// use case.
8182
GetAPIReader() client.Reader
83+
84+
// GetWebhookServer returns a webhook.Server
85+
GetWebhookServer() *webhook.Server
8286
}
8387

8488
// Options are the arguments for creating a new Manager
@@ -121,6 +125,13 @@ type Options struct {
121125
// for serving prometheus metrics
122126
MetricsBindAddress string
123127

128+
// Port is the port that the webhook server serves at.
129+
// It is used to set webhook.Server.Port.
130+
Port int
131+
// Host is the hostname that the webhook server binds to.
132+
// It is used to set webhook.Server.Host.
133+
Host string
134+
124135
// Functions to all for a user to customize the values that will be injected.
125136

126137
// NewCache is the function that will create the cache to be used
@@ -234,6 +245,8 @@ func New(config *rest.Config, options Options) (Manager, error) {
234245
metricsListener: metricsListener,
235246
internalStop: stop,
236247
internalStopper: stop,
248+
port: options.Port,
249+
host: options.Host,
237250
}, nil
238251
}
239252

pkg/webhook/admission/defaulter.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package admission
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"net/http"
23+
24+
"k8s.io/apimachinery/pkg/runtime"
25+
)
26+
27+
// Defaulter defines functions for setting defaults on resources
28+
type Defaulter interface {
29+
runtime.Object
30+
Default()
31+
}
32+
33+
// DefaultingWebhookFor creates a new Webhook for Defaulting the provided type.
34+
func DefaultingWebhookFor(defaulter Defaulter) *Webhook {
35+
return &Webhook{
36+
Handler: &mutatingHandler{defaulter: defaulter},
37+
}
38+
}
39+
40+
type mutatingHandler struct {
41+
defaulter Defaulter
42+
decoder *Decoder
43+
}
44+
45+
var _ DecoderInjector = &mutatingHandler{}
46+
47+
// InjectDecoder injects the decoder into a mutatingHandler.
48+
func (h *mutatingHandler) InjectDecoder(d *Decoder) error {
49+
h.decoder = d
50+
return nil
51+
}
52+
53+
// Handle handles admission requests.
54+
func (h *mutatingHandler) Handle(ctx context.Context, req Request) Response {
55+
if h.defaulter == nil {
56+
panic("defaulter should never be nil")
57+
}
58+
59+
// Get the object in the request
60+
obj := h.defaulter.DeepCopyObject().(Defaulter)
61+
err := h.decoder.Decode(req, obj)
62+
if err != nil {
63+
return Errored(http.StatusBadRequest, err)
64+
}
65+
66+
// Default the object
67+
obj.Default()
68+
marshalled, err := json.Marshal(obj)
69+
if err != nil {
70+
return Errored(http.StatusInternalServerError, err)
71+
}
72+
73+
// Create the patch
74+
return PatchResponseFromRaw(req.Object.Raw, marshalled)
75+
}

0 commit comments

Comments
 (0)