-
Notifications
You must be signed in to change notification settings - Fork 24
/
webhooks.go
163 lines (146 loc) · 4.25 KB
/
webhooks.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
package webhooks
import (
"context"
"encoding/json"
"io"
"net/http"
"time"
"github.com/gorilla/mux"
"go.uber.org/fx"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxninja/aperture/v2/pkg/log"
)
// Module is a fx module that provides annotated http server using the default listener
// and kubernetes registry.
func Module() fx.Option {
return fx.Options(
fx.Provide(provideK8sRegistry),
)
}
// provideK8sRegistry provides k8s registry which allows serving k8s webhooks
// using given router.
func provideK8sRegistry(mux *mux.Router) *K8sRegistry {
return &K8sRegistry{
mux: mux,
handlers: make(map[string]*handler),
}
}
// K8sRegistry allows registering k8s webhooks as validators.
type K8sRegistry struct {
mux *mux.Router
handlers map[string]*handler
}
// RegisterValidator adds a validator to be handled on a given HTTP path
//
// This function should be only called before Start phase.
func (r *K8sRegistry) RegisterValidator(path string, validator Validator) {
oldHandler, exists := r.handlers[path]
if !exists {
newHandler := &handler{
validators: []Validator{validator},
path: path,
}
r.mux.Handle(path, newHandler)
// remember the handler so we can add more validators on the same path later
r.handlers[path] = newHandler
} else {
oldHandler.validators = append(oldHandler.validators, validator)
}
}
// FIXME make me configurable? (note: this should match the timeout on
// ValidatingWebhookConfiguration).
const requestTimeout = 5 * time.Second
// Validator is an interface for k8s validating webhooks.
type Validator interface {
// ValidateObject validates an object given in admission.Request
//
// returns:
// * true, "", nil if object passes validation
// * false, msg, nil if object fails validation
// * false, "", err if validator itself fails
//
// Note: if validator is "not interested" in a given object, it should
// return true.
ValidateObject(
ctx context.Context,
req *admissionv1.AdmissionRequest,
) (ok bool, msg string, err error)
}
// handler combines all the hooks to be served from concrete HTTP path.
type handler struct {
path string // for logging
validators []Validator
}
// ServeHTTP implements http.Handler interface and invokes the handler.
func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
log.Trace().Str("path", h.path).Msg("Webhooks HandleFunc() start")
if req.Method != http.MethodPost {
http.Error(w, "", http.StatusMethodNotAllowed)
return
}
if req.Header.Get("Content-Type") != "application/json" {
http.Error(w, "", http.StatusUnsupportedMediaType)
return
}
if req.Body == nil {
http.Error(w, "", http.StatusBadRequest)
return
}
body, err := io.ReadAll(req.Body)
if err != nil {
http.Error(w, "", http.StatusBadRequest)
return
}
var aReq admissionv1.AdmissionReview
err = json.Unmarshal(body, &aReq)
if err != nil {
http.Error(w, "", http.StatusBadRequest)
return
}
reqCtx, cancel := context.WithTimeout(req.Context(), requestTimeout)
defer cancel()
aResp := admissionv1.AdmissionReview{
TypeMeta: aReq.TypeMeta,
Response: &admissionv1.AdmissionResponse{
UID: aReq.Request.UID,
Allowed: true,
},
}
var ok bool
var msg string
for _, validator := range h.validators {
ok, msg, err = validator.ValidateObject(reqCtx, aReq.Request)
if err != nil {
log.Error().Err(err).Str("path", h.path).Msg("Validator failed to validate")
http.Error(
w, "internal error occurred in validator", http.StatusInternalServerError,
)
return
}
if !ok {
aResp.Response.Allowed = false
aResp.Response.Result = &metav1.Status{
Message: msg,
}
break
}
}
respBody, err := json.Marshal(aResp)
if err != nil {
log.Error().Err(err).Msg("Error marshaling webhook response body")
http.Error(w, "", http.StatusInternalServerError)
}
_, err = w.Write(respBody)
if err != nil {
log.Error().Err(err).Msg("Error writing webhook response body")
http.Error(w, "", http.StatusInternalServerError)
}
if aResp.Response.Allowed {
log.Trace().Str("path", h.path).Msg("Webhook accepted validation request")
} else {
log.Info().Str("path", h.path).Str("msg", aResp.Response.Result.Message).Msg(
"Validating webhook rejected validation request",
)
}
}