/
injector_webhook.go
323 lines (272 loc) · 10.3 KB
/
injector_webhook.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
/*
Copyright 2023 Lumigo.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package injector
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
admissionv1 "k8s.io/api/admission/v1"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/go-logr/logr"
operatorv1alpha1 "github.com/lumigo-io/lumigo-kubernetes-operator/api/v1alpha1"
"github.com/lumigo-io/lumigo-kubernetes-operator/controllers/conditions"
"github.com/lumigo-io/lumigo-kubernetes-operator/mutation"
)
var (
decoder = scheme.Codecs.UniversalDecoder()
)
type LumigoInjectorWebhookHandler struct {
client.Client
record.EventRecorder
*admission.Decoder
LumigoOperatorVersion string
LumigoInjectorImage string
TelemetryProxyOtlpServiceUrl string
Log logr.Logger
}
func (h *LumigoInjectorWebhookHandler) SetupWebhookWithManager(mgr ctrl.Manager) error {
webhook := &admission.Webhook{
Handler: h,
}
handler, err := admission.StandaloneWebhook(webhook, admission.StandaloneOptions{})
if err != nil {
return err
}
mgr.GetWebhookServer().Register("/v1alpha1/inject", handler)
return nil
}
// The client is automatically injected by the Webhook machinery
func (h *LumigoInjectorWebhookHandler) InjectClient(c client.Client) error {
h.Client = c
return nil
}
// The decoder is automatically injected by the Webhook machinery
func (h *LumigoInjectorWebhookHandler) InjectDecoder(d *admission.Decoder) error {
h.Decoder = d
return nil
}
func (h *LumigoInjectorWebhookHandler) Handle(ctx context.Context, request admission.Request) admission.Response {
log := logf.Log.WithName("lumigo-injector-webhook").WithValues("resource_gvk", request.Kind)
if request.Operation == admissionv1.Delete {
// Nothing to mutate on deletions
return admission.Allowed("Mutating webhooks have nothing to do on deletions")
}
resourceAdaper, err := newResourceAdatper(request.Kind, request.Object.Raw)
if err != nil {
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error while parsing the resource: %w", err))
}
if resourceAdaper == nil {
// Resource not supported
return admission.Allowed(fmt.Sprintf("The Lumigo Injector webhook does not mutate resources of type %s", request.Kind))
}
namespace := resourceAdaper.GetNamespace()
// Check if we have a Lumigo instance in the object's namespace
lumigos := &operatorv1alpha1.LumigoList{}
if err := h.Client.List(ctx, lumigos, &client.ListOptions{
Namespace: namespace,
}); err != nil {
if apierrors.IsNotFound(err) {
// TODO The Lumigo CRD is not register. Catastrophic error?
return admission.Allowed(fmt.Sprintf("No Lumigo configuration for the '%s' namespace; resource will not be mutated", namespace))
}
log.Error(err, "failed to retrieve Lumigo instance in namespace", "namespace", namespace)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("cannot retrieve Lumigo instances in namespace %s: %w", namespace, err))
}
if len(lumigos.Items) < 1 {
return admission.Allowed(fmt.Sprintf("No Lumigo configuration for the '%s' namespace; resource will not be mutated", namespace))
}
lumigo := lumigos.Items[0]
// Check if tracer injection is enabled (so the injection _should_ be performed)
enabled := lumigo.Spec.Tracing.Injection.Enabled
if enabled != nil && !*enabled {
return admission.Allowed(fmt.Sprintf("Tracing injection is disabled in the '%s' namespace; resource will not be mutated", namespace))
}
if !conditions.IsActive(&lumigo) {
return admission.Allowed(fmt.Sprintf("The Lumigo object in the '%s' namespace is not active; resource will not be mutated", namespace))
}
mutator, err := mutation.NewMutator(&log, &lumigo.Spec.LumigoToken, h.LumigoOperatorVersion, h.LumigoInjectorImage, h.TelemetryProxyOtlpServiceUrl)
if err != nil {
return admission.Allowed(fmt.Errorf("cannot instantiate mutator: %w", err).Error())
}
objectMeta := resourceAdaper.GetObjectMeta()
hadAlreadyInstrumentation := strings.HasPrefix(objectMeta.Labels[mutation.LumigoAutoTraceLabelKey], mutation.LumigoAutoTraceLabelVersionPrefixValue)
injectionOccurred := false
if objectMeta.Labels[mutation.LumigoAutoTraceLabelKey] == mutation.LumigoAutoTraceLabelSkipNextInjectorValue {
h.Log.Info(fmt.Sprintf("Skipping injection: '%s' label set to '%s'", mutation.LumigoAutoTraceLabelKey, mutation.LumigoAutoTraceLabelSkipNextInjectorValue))
delete(objectMeta.Labels, mutation.LumigoAutoTraceLabelKey)
} else if injectionOccurred, err = resourceAdaper.InjectLumigoInto(mutator); err != nil {
if !hadAlreadyInstrumentation {
operatorv1alpha1.RecordCannotAddInstrumentationEvent(h.EventRecorder, resourceAdaper.GetResource(), fmt.Sprintf("injector webhook, acting on behalf of the '%s/%s' Lumigo resource", lumigo.Namespace, lumigo.Name), err)
} else {
operatorv1alpha1.RecordCannotUpdateInstrumentationEvent(h.EventRecorder, resourceAdaper.GetResource(), fmt.Sprintf("injector webhook, acting on behalf of the '%s/%s' Lumigo resource", lumigo.Namespace, lumigo.Name), err)
}
return admission.Allowed(fmt.Errorf("cannot inject Lumigo tracing in the pod spec %w", err).Error())
}
marshalled, err := resourceAdaper.Marshal()
if err != nil {
return admission.Allowed(fmt.Errorf("cannot marshal object %w", err).Error())
}
if injectionOccurred {
if !hadAlreadyInstrumentation {
operatorv1alpha1.RecordAddedInstrumentationEvent(h.EventRecorder, resourceAdaper.GetResource(), fmt.Sprintf("injector webhook, acting on behalf of the '%s/%s' Lumigo resource", lumigo.Namespace, lumigo.Name))
} else {
operatorv1alpha1.RecordUpdatedInstrumentationEvent(h.EventRecorder, resourceAdaper.GetResource(), fmt.Sprintf("injector webhook, acting on behalf of the '%s/%s' Lumigo resource", lumigo.Namespace, lumigo.Name))
}
}
return admission.PatchResponseFromRaw(request.Object.Raw, marshalled)
}
type resourceAdapter interface {
GetResource() runtime.Object
GetObjectMeta() *metav1.ObjectMeta
GetNamespace() string
InjectLumigoInto(mutation.Mutator) (bool, error)
Marshal() ([]byte, error)
}
// Inline implementation of resourceAdapter inspired by https://stackoverflow.com/a/43420100/6188451
type resourceAdapterImpl struct {
resource runtime.Object
getNamespace func() string
getObjectMeta func() *metav1.ObjectMeta
}
func (r *resourceAdapterImpl) GetResource() runtime.Object {
return r.resource
}
func (r *resourceAdapterImpl) GetNamespace() string {
return r.getNamespace()
}
func (r *resourceAdapterImpl) GetObjectMeta() *metav1.ObjectMeta {
return r.getObjectMeta()
}
func (r *resourceAdapterImpl) InjectLumigoInto(mutator mutation.Mutator) (bool, error) {
return mutator.InjectLumigoInto(r.resource)
}
func (r *resourceAdapterImpl) Marshal() ([]byte, error) {
return json.Marshal(r.resource)
}
func newResourceAdatper(gvk metav1.GroupVersionKind, raw []byte) (resourceAdapter, error) {
sGVK := fmt.Sprintf("%s/%s.%s", gvk.Group, gvk.Version, gvk.Kind)
switch sGVK {
case "apps/v1.Daemonset":
{
resource := &appsv1.DaemonSet{}
if _, _, err := decoder.Decode(raw, nil, resource); err != nil {
return nil, fmt.Errorf("cannot parse resource into a %s: %w", sGVK, err)
}
return &resourceAdapterImpl{
resource: resource,
getNamespace: func() string {
return resource.Namespace
},
getObjectMeta: func() *metav1.ObjectMeta {
return &resource.ObjectMeta
},
}, nil
}
case "apps/v1.Deployment":
{
resource := &appsv1.Deployment{}
if _, _, err := decoder.Decode(raw, nil, resource); err != nil {
return nil, fmt.Errorf("cannot parse resource into a %s: %w", sGVK, err)
}
return &resourceAdapterImpl{
resource: resource,
getNamespace: func() string {
return resource.Namespace
},
getObjectMeta: func() *metav1.ObjectMeta {
return &resource.ObjectMeta
},
}, nil
}
case "apps/v1.ReplicaSet":
{
resource := &appsv1.ReplicaSet{}
if _, _, err := decoder.Decode(raw, nil, resource); err != nil {
return nil, fmt.Errorf("cannot parse resource into a %s: %w", sGVK, err)
}
return &resourceAdapterImpl{
resource: resource,
getNamespace: func() string {
return resource.Namespace
},
getObjectMeta: func() *metav1.ObjectMeta {
return &resource.ObjectMeta
},
}, nil
}
case "apps/v1.StatefulSet":
{
resource := &appsv1.StatefulSet{}
if _, _, err := decoder.Decode(raw, nil, resource); err != nil {
return nil, fmt.Errorf("cannot parse resource into a %s: %w", sGVK, err)
}
return &resourceAdapterImpl{
resource: resource,
getNamespace: func() string {
return resource.Namespace
},
getObjectMeta: func() *metav1.ObjectMeta {
return &resource.ObjectMeta
},
}, nil
}
case "batch/v1.CronJob":
{
resource := &batchv1.CronJob{}
if _, _, err := decoder.Decode(raw, nil, resource); err != nil {
return nil, fmt.Errorf("cannot parse resource into a %s: %w", sGVK, err)
}
return &resourceAdapterImpl{
resource: resource,
getNamespace: func() string {
return resource.Namespace
},
getObjectMeta: func() *metav1.ObjectMeta {
return &resource.ObjectMeta
},
}, nil
}
case "batch/v1.Job":
{
resource := &batchv1.Job{}
if _, _, err := decoder.Decode(raw, nil, resource); err != nil {
return nil, fmt.Errorf("cannot parse resource into a %s: %w", sGVK, err)
}
return &resourceAdapterImpl{
resource: resource,
getNamespace: func() string {
return resource.Namespace
},
getObjectMeta: func() *metav1.ObjectMeta {
return &resource.ObjectMeta
},
}, nil
}
default:
{
return nil, nil
}
}
}