Skip to content

Commit f2ab43e

Browse files
author
Mengqi Yu
committed
admission webhook server
1 parent 419794c commit f2ab43e

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: &webhook.Service{
124+
Namespace: "default",
125+
Name: "foo-admission-server-service",
126+
// Selectors should select the pods that runs this webhook server.
127+
Selectors: map[string]string{
128+
"app": "foo-admission-server",
129+
},
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
@@ -21,6 +21,7 @@ import (
2121
"sync"
2222
"time"
2323

24+
"k8s.io/apimachinery/pkg/api/meta"
2425
"k8s.io/apimachinery/pkg/runtime"
2526
"k8s.io/client-go/rest"
2627
"k8s.io/client-go/tools/leaderelection"
@@ -31,6 +32,7 @@ import (
3132
"sigs.k8s.io/controller-runtime/pkg/recorder"
3233
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
3334
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
35+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3436
)
3537

3638
var log = logf.KBLog.WithName("manager")
@@ -42,6 +44,8 @@ type controllerManager struct {
4244
// scheme is the scheme injected into Controllers, EventHandlers, Sources and Predicates. Defaults
4345
// to scheme.scheme.
4446
scheme *runtime.Scheme
47+
// admissionDecoder is used to decode an admission.Request.
48+
admissionDecoder admission.Decoder
4549

4650
// runnables is the set of Controllers that the controllerManager injects deps into and Starts.
4751
runnables []Runnable
@@ -63,6 +67,9 @@ type controllerManager struct {
6367
// resourceLock
6468
resourceLock resourcelock.Interface
6569

70+
// mapper is used to map resources to kind, and map kind and version.
71+
mapper meta.RESTMapper
72+
6673
mu sync.Mutex
6774
started bool
6875
errChan chan error
@@ -127,6 +134,10 @@ func (cm *controllerManager) GetScheme() *runtime.Scheme {
127134
return cm.scheme
128135
}
129136

137+
func (cm *controllerManager) GetAdmissionDecoder() admission.Decoder {
138+
return cm.admissionDecoder
139+
}
140+
130141
func (cm *controllerManager) GetFieldIndexer() client.FieldIndexer {
131142
return cm.fieldIndexes
132143
}
@@ -139,6 +150,10 @@ func (cm *controllerManager) GetRecorder(name string) record.EventRecorder {
139150
return cm.recorderProvider.GetEventRecorderFor(name)
140151
}
141152

153+
func (cm *controllerManager) GetRESTMapper() meta.RESTMapper {
154+
return cm.mapper
155+
}
156+
142157
func (cm *controllerManager) Start(stop <-chan struct{}) error {
143158
if cm.resourceLock == nil {
144159
go cm.start(stop)

pkg/manager/manager.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
internalrecorder "sigs.k8s.io/controller-runtime/pkg/internal/recorder"
3434
"sigs.k8s.io/controller-runtime/pkg/leaderelection"
3535
"sigs.k8s.io/controller-runtime/pkg/recorder"
36+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3637
)
3738

3839
// Manager initializes shared dependencies such as Caches and Clients, and provides them to Runnables.
@@ -57,6 +58,9 @@ type Manager interface {
5758
// GetScheme returns and initialized Scheme
5859
GetScheme() *runtime.Scheme
5960

61+
// GetAdmissionDecoder returns the runtime.Decoder based on the scheme.
62+
GetAdmissionDecoder() admission.Decoder
63+
6064
// GetClient returns a client configured with the Config
6165
GetClient() client.Client
6266

@@ -68,6 +72,9 @@ type Manager interface {
6872

6973
// GetRecorder returns a new EventRecorder for the provided name
7074
GetRecorder(name string) record.EventRecorder
75+
76+
// GetRESTMapper returns a RESTMapper
77+
GetRESTMapper() meta.RESTMapper
7178
}
7279

7380
// Options are the arguments for creating a new Manager
@@ -102,6 +109,7 @@ type Options struct {
102109
newClient func(config *rest.Config, options client.Options) (client.Client, error)
103110
newRecorderProvider func(config *rest.Config, scheme *runtime.Scheme, logger logr.Logger) (recorder.Provider, error)
104111
newResourceLock func(config *rest.Config, recorderProvider recorder.Provider, options leaderelection.Options) (resourcelock.Interface, error)
112+
newAdmissionDecoder func(scheme *runtime.Scheme) (admission.Decoder, error)
105113
}
106114

107115
// Runnable allows a component to be started.
@@ -165,15 +173,22 @@ func New(config *rest.Config, options Options) (Manager, error) {
165173
return nil, err
166174
}
167175

176+
admissionDecoder, err := options.newAdmissionDecoder(options.Scheme)
177+
if err != nil {
178+
return nil, err
179+
}
180+
168181
return &controllerManager{
169182
config: config,
170183
scheme: options.Scheme,
184+
admissionDecoder: admissionDecoder,
171185
errChan: make(chan error),
172186
cache: cache,
173187
fieldIndexes: cache,
174188
client: client.DelegatingClient{Reader: cache, Writer: writeObj, StatusClient: writeObj},
175189
recorderProvider: recorderProvider,
176190
resourceLock: resourceLock,
191+
mapper: mapper,
177192
}, nil
178193
}
179194

@@ -208,5 +223,9 @@ func setOptionsDefaults(options Options) Options {
208223
options.newResourceLock = leaderelection.NewResourceLock
209224
}
210225

226+
if options.newAdmissionDecoder == nil {
227+
options.newAdmissionDecoder = admission.NewDecoder
228+
}
229+
211230
return options
212231
}

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)