⚠️ webhook redesign for generic case#323
⚠️ webhook redesign for generic case#323k8s-ci-robot merged 19 commits intokubernetes-sigs:masterfrom
Conversation
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: DirectXMan12 The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
cc @mengqiy |
|
(built on top of #300 ) |
|
/assign @mengqiy |
droot
left a comment
There was a problem hiding this comment.
Love this change. I have couple of comments. I will take another look from the perspective of adding conversion-webhook-support tomorrow.
example/main.go
Outdated
| os.Exit(1) | ||
| } | ||
| hookServer.Register("/mutate-pods", webhook.Admission{&podAnnotator{}}) | ||
| hookServer.Register("/validate-pods", webhook.Admission{&podValidator{}}) |
There was a problem hiding this comment.
Looks very neat, easier to follow. This means path is now decoupled from the webhook definition.
|
|
||
| // podAnnotator adds an annotation to every incoming pods. | ||
| func (a *podAnnotator) Handle(ctx context.Context, req types.Request) types.Response { | ||
| func (a *podAnnotator) Handle(ctx context.Context, req admission.Request) admission.Response { |
There was a problem hiding this comment.
+1 on readability admission.{Request, Response} as http.{Request, Response}
|
|
||
| var admissionv1beta1scheme = runtime.NewScheme() | ||
| var admissionv1beta1schemecodecs = serializer.NewCodecFactory(admissionv1beta1scheme) | ||
| var admissionScheme = runtime.NewScheme() |
There was a problem hiding this comment.
Does this scheme needs user defined types ? (basically, should we be injected from the manager ?)
There was a problem hiding this comment.
This decoder only need to understand AdmissionReviewRequest. So IMO no need to be injected.
| func (hs multiMutating) Handle(ctx context.Context, req Request) Response { | ||
| patches := []jsonpatch.JsonPatchOperation{} | ||
| for _, handler := range hs { | ||
| resp := handler.Handle(ctx, req) |
There was a problem hiding this comment.
looks like these handlers are independent, so can probably be invoked concurrently ?
There was a problem hiding this comment.
maybe, but I almost want to deprecate this.
There was a problem hiding this comment.
maybe, but I almost want to deprecate this.
Reasons?
pkg/webhook/server.go
Outdated
| func instrumentedHook(path string, hookRaw http.Handler) http.Handler { | ||
| return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { | ||
| startTS := time.Now() | ||
| defer metrics.RequestLatency.WithLabelValues(path).Observe(time.Now().Sub(startTS).Seconds()) |
There was a problem hiding this comment.
We need to wrap it with func{} otherwise the args time.Now().Sub(startTS).Seconds() will be evaluated immediately reporting almost no latency :)
There was a problem hiding this comment.
Good catch!
The deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.
Ref: https://tour.golang.org/flowcontrol/12
mengqiy
left a comment
There was a problem hiding this comment.
A lot of improvement!
I have several comment inlined.
example/main.go
Outdated
| entryLog.Error(err, "unable to create a new webhook server") | ||
| os.Exit(1) | ||
| } | ||
| mgr.Add(as) |
There was a problem hiding this comment.
I don't think this will work, since mgr.Add() actually does the deps injection.
It should be done after registering all the handlers.
Thus, I proposed using Complete method to finish the setup.
There was a problem hiding this comment.
Start does the deps injection, really. Add just records the setFields method for later.
| type Server struct { | ||
| // Name is the name of server | ||
| Name string | ||
| // TODO(directxman12): should we make the mux configurable? |
There was a problem hiding this comment.
Is there a use case in your head that the user may want to configure mux?
There was a problem hiding this comment.
if you want to serve on an existing handler more easily. It's not clear that there's a concrete usecase, but I can imagine someone saying something at some points.
There was a problem hiding this comment.
(e.g. your comment below about Handle)
There was a problem hiding this comment.
(e.g. your comment below about
Handle)
We probably should make webhookMux field configurable.
pkg/webhook/server.go
Outdated
| registry map[string]Webhook | ||
| // webhookMux is the multiplexer that handles different webhooks. | ||
| webhookMux *http.ServeMux | ||
| // hooks keep track of all registered webhooks for dependency injection, |
There was a problem hiding this comment.
nit: do we want to use hooks or webhooks here?
There was a problem hiding this comment.
comment still doesn't match field name
| } | ||
|
|
||
| // Handle registers a http.Handler for the given pattern. | ||
| func (s *Server) Handle(pattern string, handler http.Handler) { |
There was a problem hiding this comment.
The original intend was to give the user flexibility to add additional handlers. e.g. statusz, healthz etc.
There was a problem hiding this comment.
right, but they shouldn't be doing it on the webhook server. They should do it elsewhere, or we should let people pass in their own mux.
pkg/webhook/admission/http.go
Outdated
| func (wh *Webhook) writeResponse(w io.Writer, response Response) { | ||
| if response.Result.Code != 0 { | ||
| if response.Result.Code == http.StatusOK { | ||
| metrics.TotalRequests.WithLabelValues(wh.Path, "true").Inc() |
There was a problem hiding this comment.
It seems metrics TotalRequests has been dropped.
Is it not useful?
There was a problem hiding this comment.
It's not clear how useful it is (accepting or rejecting is a normal occurrence -- you really probably want to see errors and error codes if you're trying to alert, etc), and it was difficult to work in the new code structure. There may be a way to implement it generically, but I figured it was fine for a follow-up PR + the scream test.
d4693e2 to
18b49ef
Compare
|
@mengqiy this is ready for more review. I'll write a better description tomorrow. Still want to follow it up with a better story around generating patches and returning errors, perhaps. |
a25f015 to
8623a9b
Compare
|
Looks good to me. Will defer it to @mengqiy for the final LGTM. |
mengqiy
left a comment
There was a problem hiding this comment.
Ha, 20 commits now.
Overall looks good.
move webhook self installer to CT as generator
The 1st commit should be gone after the rebase. Why it is still there?
|
|
||
| // Decode decodes the inlined object in the AdmissionRequest into the passed-in runtime.Object. | ||
| func (d *Decoder) Decode(req Request, into runtime.Object) error { | ||
| // NB(directxman12): there's a bug/weird interaction between decoders and |
There was a problem hiding this comment.
This looks like a workaround until apimachinery folks fix the bug.
We can probably link to the upstream issue.
There was a problem hiding this comment.
ack, need to file the issue
pkg/webhook/example_test.go
Outdated
| Handler: admission.HandlerFunc(func(ctx context.Context, req AdmissionRequest) AdmissionResponse { | ||
| return Patched("some changes", | ||
| JSONPatchOp{Operation: "add", Path: "/metadata/annotations/access", Value: "granted"}, | ||
| JSONPatchOp{Operation: "add", Path: "/metadata/annotations/access", Value: "granted"}, |
There was a problem hiding this comment.
It is strange to have a duplicate JSONPatch in the example, isn't it?
|
|
||
| // podAnnotator implements inject.Client. | ||
| // A client will be automatically injected. | ||
| var _ inject.Client = &podAnnotator{} |
There was a problem hiding this comment.
It doesn't really add much, and necessitates an extra import. I can add it back in if we really want it, but I think the comment suffices here.
| "k8s.io/apimachinery/pkg/api/errors" | ||
| "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| v1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| @@ -0,0 +1,105 @@ | |||
| /* | |||
There was a problem hiding this comment.
I believe everything under pkg/internal/webhookgenerator is rebase artifacts and should not exist.
| invoked bool | ||
| fn func(context.Context, Request) Response | ||
| decoder *Decoder | ||
| injectedString string |
There was a problem hiding this comment.
Is it actually tested anywhere?
There was a problem hiding this comment.
yep, pkg/webhook/admission/webhook_test.go
This makes sure to run go vet on the examples and the alias file, and turns goimports back on.
We had goimports turned off. When turned back on, it complained about a bunch of stuff. This fixes that.
The example was still using the old list options arguments style (second argument instead of last argument), which made it fail to compile. This also makes sure we run go test on the whole project, instead of just pkg/...
The webhook server logic was not handling injection correctly -- it implemented injection for a couple of specific fields, rather than receiving and dealing with an injectfunc. This fixes that. As a side effect, it also removes the handle to manager and client from the webhook server, since those aren't used by the server, and should thus be injected from the manager.
This flattens down the webhook package structure, removing dedicated `types` packages in favor of having things in the relevant places. To do this, the inject interface definitions for admission were also moved to the admission controller location, but they actually make some amount of sense living there. It also renames and restructures a couple of the types for code clarity (e.g. WebhookTypeMutating to MutatingWebhook) or to follow the convention of other types in CR (e.g. Making wrapped types from core Kubernetes nested fields).
We don't have anywhere that we actually make use of decoder as an interface, so switch to just have a concrete implementation.
This extracts out the multi-handler support into separate interfaces. That simplifies the code, makes it easier to test directly, and allows us to drop the now-extraneous Type field. This simultaneously removes the builder (for now) since it doesn't actually make anything simpler. Along the way, we also refactor some common functionality out into a helper on reponse itself as opposed to being run by different bits of the webhook.
This cleans up tests to more closely follow CR style, favoring behavior-style tests as opposed to pure unit tests.
5978194 to
be3d3ac
Compare
This renames a few ambigously named fields in the webhook server, removes some extraneous fields and methods, and removes the unnecessary constructor for Server.
It was only being used for registering metrics, and that can be done with the path instead.
This removes the path functionality from admission webhooks, since it's not strictly needed. Relevant metrics are moved up to the webhook server, and path is added to register. This also introduces a couple of helpers that should make code a bit clearer when writing admission webhooks (Allowed, Denied, Patched), and an alias file to avoid importing both webhook and webhook/admission.
This ensures that decoders (which are no longer constructed in the manager) are properly injected into webhook handlers. The webhook itself receives a scheme, which it uses to construct a decoder.
Due to a weird interaction between the unstructured decoder (which demands APIVersion and Kind fields) and the API server (which fails to set those fields on the embedded object in admission requests), we can't use the normal decoders, nor can we call json.Unmarshal directly on the unstructured (since it implements an unmarshaller that calls back into the decoder). This detects unstructured objects, and does the right thing for them.
This moves the examples to an actual Go example file, so that we can be sure that they actually compile.
It's not used anywhere any more.
It's the same name used in alias.go, and it produces less-verbose code without loss of readability.
The were incorrect (would not have compiled), are mostly duplicates of examples/*webhook.go, and can't be easily checked for being up-to-date.
This gives each webhook a log, so that serving errors can be tied to a particuluar webhook.
be3d3ac to
60b01e4
Compare
|
/lgtm |
Nothing was missing from the tree (due to transitive deps), just missing from being required by Gopkg.lock.
60b01e4 to
286b0b4
Compare
|
/lgtm |
While implementing a webhook, I found some tech debt and rough corners around the webhook code. This is an attempt to smooth some of that over.