Skip to content

Commit 0c80130

Browse files
author
Mengqi Yu
committed
admission webhook server
1 parent 2d4d049 commit 0c80130

39 files changed

+1797
-47
lines changed

example/main.go

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@ package main
1919
import (
2020
"context"
2121
"flag"
22+
"fmt"
23+
"net/http"
2224
"os"
2325

2426
"github.com/go-logr/logr"
27+
28+
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
2529
appsv1 "k8s.io/api/apps/v1"
2630
corev1 "k8s.io/api/core/v1"
2731
"k8s.io/apimachinery/pkg/api/errors"
32+
apitypes "k8s.io/apimachinery/pkg/types"
2833
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
2934
"sigs.k8s.io/controller-runtime/pkg/client"
3035
"sigs.k8s.io/controller-runtime/pkg/client/config"
@@ -35,11 +40,13 @@ import (
3540
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
3641
"sigs.k8s.io/controller-runtime/pkg/runtime/signals"
3742
"sigs.k8s.io/controller-runtime/pkg/source"
43+
"sigs.k8s.io/controller-runtime/pkg/webhook"
44+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
45+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission/builder"
46+
"sigs.k8s.io/controller-runtime/pkg/webhook/types"
3847
)
3948

40-
var (
41-
log = logf.Log.WithName("example-controller")
42-
)
49+
var log = logf.Log.WithName("example-controller")
4350

4451
func main() {
4552
flag.Parse()
@@ -75,6 +82,64 @@ func main() {
7582
os.Exit(1)
7683
}
7784

85+
// Setup webhooks
86+
mutatingWebhook, err := builder.NewWebhookBuilder().
87+
Name("mutating.k8s.io").
88+
Type(types.WebhookTypeMutating).
89+
Path("/mutating-pods").
90+
Operations(admissionregistrationv1beta1.Create, admissionregistrationv1beta1.Update).
91+
WithManager(mgr).
92+
ForType(&corev1.Pod{}).
93+
Build(&podAnnotator{client: mgr.GetClient(), decoder: mgr.GetAdmissionDecoder()})
94+
if err != nil {
95+
entryLog.Error(err, "unable to setup mutating webhook")
96+
os.Exit(1)
97+
}
98+
99+
validatingWebhook, err := builder.NewWebhookBuilder().
100+
Name("validating.k8s.io").
101+
Type(types.WebhookTypeValidating).
102+
Path("/validating-pods").
103+
Operations(admissionregistrationv1beta1.Create, admissionregistrationv1beta1.Update).
104+
WithManager(mgr).
105+
ForType(&corev1.Pod{}).
106+
Build(&podValidator{client: mgr.GetClient(), decoder: mgr.GetAdmissionDecoder()})
107+
if err != nil {
108+
entryLog.Error(err, "unable to setup validating webhook")
109+
os.Exit(1)
110+
}
111+
112+
as, err := webhook.NewServer("foo-admission-server", mgr, webhook.ServerOptions{
113+
Port: 443,
114+
CertDir: "/tmp/cert",
115+
Client: mgr.GetClient(),
116+
KVMap: map[string]interface{}{"foo": "bar"},
117+
BootstrapOptions: &webhook.BootstrapOptions{
118+
Secret: &apitypes.NamespacedName{
119+
Namespace: "default",
120+
Name: "foo-admission-server-secret",
121+
},
122+
123+
Service: &apitypes.NamespacedName{
124+
Namespace: "default",
125+
Name: "foo-admission-server-service",
126+
},
127+
// Labels should select the pods that runs this webhook server.
128+
Labels: map[string]string{
129+
"app": "foo-admission-server",
130+
},
131+
},
132+
})
133+
if err != nil {
134+
entryLog.Error(err, "unable to create a new webhook server")
135+
os.Exit(1)
136+
}
137+
err = as.Register(mutatingWebhook, validatingWebhook)
138+
if err != nil {
139+
entryLog.Error(err, "unable to register webhooks in the admission server")
140+
os.Exit(1)
141+
}
142+
78143
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
79144
entryLog.Error(err, "unable to run manager")
80145
os.Exit(1)
@@ -83,6 +148,7 @@ func main() {
83148

84149
// reconcileReplicaSet reconciles ReplicaSets
85150
type reconcileReplicaSet struct {
151+
// client can be used to retrieve objects from the APIServer.
86152
client client.Client
87153
log logr.Logger
88154
}
@@ -128,3 +194,73 @@ func (r *reconcileReplicaSet) Reconcile(request reconcile.Request) (reconcile.Re
128194

129195
return reconcile.Result{}, nil
130196
}
197+
198+
// podAnnotator annotates Pods
199+
type podAnnotator struct {
200+
client client.Client
201+
decoder admission.Decoder
202+
}
203+
204+
// Implement admission.Handler so the controller can handle admission request.
205+
var _ admission.Handler = &podAnnotator{}
206+
207+
// podAnnotator adds an annotation to every incoming pods.
208+
func (a *podAnnotator) Handle(_ context.Context, req admission.Request) admission.Response {
209+
pod := &corev1.Pod{}
210+
211+
err := a.decoder.Decode(req, pod)
212+
if err != nil {
213+
return admission.ErrorResponse(http.StatusBadRequest, err)
214+
}
215+
copy := pod.DeepCopy()
216+
217+
err = mutatePodsFn(copy)
218+
if err != nil {
219+
return admission.ErrorResponse(http.StatusInternalServerError, err)
220+
}
221+
return admission.PatchResponse(pod, copy)
222+
}
223+
224+
// mutatePodsFn add an annotation to the given pod
225+
func mutatePodsFn(pod *corev1.Pod) error {
226+
anno := pod.GetAnnotations()
227+
anno["example-mutating-admission-webhhok"] = "foo"
228+
pod.SetAnnotations(anno)
229+
return nil
230+
}
231+
232+
// podValidator validates Pods
233+
type podValidator struct {
234+
client client.Client
235+
decoder admission.Decoder
236+
}
237+
238+
// Implement admission.Handler so the controller can handle admission request.
239+
var _ admission.Handler = &podValidator{}
240+
241+
// podValidator admits a pod iff a specific annotation exists.
242+
func (v *podValidator) Handle(_ context.Context, req admission.Request) admission.Response {
243+
pod := &corev1.Pod{}
244+
245+
err := v.decoder.Decode(req, pod)
246+
if err != nil {
247+
return admission.ErrorResponse(http.StatusBadRequest, err)
248+
}
249+
250+
allowed, reason, err := validatePodsFn(pod)
251+
if err != nil {
252+
return admission.ErrorResponse(http.StatusInternalServerError, err)
253+
}
254+
return admission.ValidationResponse(allowed, reason)
255+
}
256+
257+
func validatePodsFn(pod *corev1.Pod) (bool, string, error) {
258+
anno := pod.GetAnnotations()
259+
key := "example-mutating-admission-webhhok"
260+
_, found := anno[key]
261+
if found {
262+
return found, "", nil
263+
} else {
264+
return found, fmt.Sprintf("failed to find annotation with key: %v", key), nil
265+
}
266+
}

pkg/client/interfaces.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package client
1919
import (
2020
"context"
2121

22+
"k8s.io/apimachinery/pkg/api/meta"
2223
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2324
"k8s.io/apimachinery/pkg/fields"
2425
"k8s.io/apimachinery/pkg/labels"
@@ -29,6 +30,15 @@ import (
2930
// ObjectKey identifies a Kubernetes Object.
3031
type ObjectKey = types.NamespacedName
3132

33+
// ObjectKeyFromObject returns the ObjectKey given a runtime.Object
34+
func ObjectKeyFromObject(obj runtime.Object) (ObjectKey, error) {
35+
accessor, err := meta.Accessor(obj)
36+
if err != nil {
37+
return ObjectKey{}, err
38+
}
39+
return ObjectKey{Namespace: accessor.GetNamespace(), Name: accessor.GetName()}, nil
40+
}
41+
3242
// TODO(directxman12): is there a sane way to deal with get/delete options?
3343

3444
// Reader knows how to read and list Kubernetes objects.

pkg/manager/internal.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package manager
1919
import (
2020
"sync"
2121

22+
"k8s.io/apimachinery/pkg/api/meta"
2223
"k8s.io/apimachinery/pkg/runtime"
2324
"k8s.io/client-go/rest"
2425
"k8s.io/client-go/tools/record"
@@ -27,6 +28,7 @@ import (
2728
"sigs.k8s.io/controller-runtime/pkg/recorder"
2829
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
2930
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
31+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3032
)
3133

3234
var log = logf.KBLog.WithName("manager")
@@ -38,6 +40,8 @@ type controllerManager struct {
3840
// scheme is the scheme injected into Controllers, EventHandlers, Sources and Predicates. Defaults
3941
// to scheme.scheme.
4042
scheme *runtime.Scheme
43+
// admissionDecoder is used to decode an admission.Request.
44+
admissionDecoder admission.Decoder
4145

4246
// runnables is the set of Controllers that the controllerManager injects deps into and Starts.
4347
runnables []Runnable
@@ -56,6 +60,9 @@ type controllerManager struct {
5660
// (and EventHandlers, Sources and Predicates).
5761
recorderProvider recorder.Provider
5862

63+
// mapper is used to map resources to kind, and map kind and version.
64+
mapper meta.RESTMapper
65+
5966
mu sync.Mutex
6067
started bool
6168
errChan chan error
@@ -120,6 +127,10 @@ func (cm *controllerManager) GetScheme() *runtime.Scheme {
120127
return cm.scheme
121128
}
122129

130+
func (cm *controllerManager) GetAdmissionDecoder() admission.Decoder {
131+
return cm.admissionDecoder
132+
}
133+
123134
func (cm *controllerManager) GetFieldIndexer() client.FieldIndexer {
124135
return cm.fieldIndexes
125136
}
@@ -132,6 +143,10 @@ func (cm *controllerManager) GetRecorder(name string) record.EventRecorder {
132143
return cm.recorderProvider.GetEventRecorderFor(name)
133144
}
134145

146+
func (cm *controllerManager) GetRESTMapper() meta.RESTMapper {
147+
return cm.mapper
148+
}
149+
135150
func (cm *controllerManager) Start(stop <-chan struct{}) error {
136151
func() {
137152
cm.mu.Lock()

pkg/manager/manager.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
3232
internalrecorder "sigs.k8s.io/controller-runtime/pkg/internal/recorder"
3333
"sigs.k8s.io/controller-runtime/pkg/recorder"
34+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3435
)
3536

3637
// Manager initializes shared dependencies such as Caches and Clients, and provides them to Runnables.
@@ -55,6 +56,9 @@ type Manager interface {
5556
// GetScheme returns and initialized Scheme
5657
GetScheme() *runtime.Scheme
5758

59+
// GetAdmissionDecoder returns the runtime.Decoder based on the scheme.
60+
GetAdmissionDecoder() admission.Decoder
61+
5862
// GetClient returns a client configured with the Config
5963
GetClient() client.Client
6064

@@ -66,6 +70,9 @@ type Manager interface {
6670

6771
// GetRecorder returns a new EventRecorder for the provided name
6872
GetRecorder(name string) record.EventRecorder
73+
74+
// GetRESTMapper returns a RESTMapper
75+
GetRESTMapper() meta.RESTMapper
6976
}
7077

7178
// Options are the arguments for creating a new Manager
@@ -87,6 +94,7 @@ type Options struct {
8794
newCache func(config *rest.Config, opts cache.Options) (cache.Cache, error)
8895
newClient func(config *rest.Config, options client.Options) (client.Client, error)
8996
newRecorderProvider func(config *rest.Config, scheme *runtime.Scheme, logger logr.Logger) (recorder.Provider, error)
97+
newAdmissionDecoder func(scheme *runtime.Scheme) (admission.Decoder, error)
9098
}
9199

92100
// Runnable allows a component to be started.
@@ -140,14 +148,21 @@ func New(config *rest.Config, options Options) (Manager, error) {
140148
return nil, err
141149
}
142150

151+
admissionDecoder, err := options.newAdmissionDecoder(options.Scheme)
152+
if err != nil {
153+
return nil, err
154+
}
155+
143156
return &controllerManager{
144157
config: config,
145158
scheme: options.Scheme,
159+
admissionDecoder: admissionDecoder,
146160
errChan: make(chan error),
147161
cache: cache,
148162
fieldIndexes: cache,
149163
client: client.DelegatingClient{Reader: cache, Writer: writeObj, StatusClient: writeObj},
150164
recorderProvider: recorderProvider,
165+
mapper: mapper,
151166
}, nil
152167
}
153168

@@ -177,5 +192,9 @@ func setOptionsDefaults(options Options) Options {
177192
options.newRecorderProvider = internalrecorder.NewProvider
178193
}
179194

195+
if options.newAdmissionDecoder == nil {
196+
options.newAdmissionDecoder = admission.NewDecoder
197+
}
198+
180199
return options
181200
}

pkg/patch/doc.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
/*
18+
Package patch provides method to calculate JSON patch between 2 k8s objects.
19+
20+
Calculate JSON patch
21+
22+
oldDeployment := appsv1.Deployment{
23+
// some fields
24+
}
25+
newDeployment := appsv1.Deployment{
26+
// some different fields
27+
}
28+
patch, err := NewJSONPatch(oldDeployment, newDeployment)
29+
if err != nil {
30+
// handle error
31+
}
32+
*/
33+
package patch

0 commit comments

Comments
 (0)