From eeb6dfad1bf50ca12649c3b03212022be8829e78 Mon Sep 17 00:00:00 2001 From: Radostin Stoyanov Date: Wed, 21 Oct 2026 21:27:43 +0100 Subject: [PATCH] fix(provenance): start the managed webhook server Signed-off-by: Radostin Stoyanov --- docs/operations/liqo-control-plane.md | 5 ++ operator/cmd/provenance-webhook/main.go | 13 +++- operator/cmd/provenance-webhook/main_test.go | 81 ++++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 operator/cmd/provenance-webhook/main_test.go diff --git a/docs/operations/liqo-control-plane.md b/docs/operations/liqo-control-plane.md index 524af1327..d7b184783 100644 --- a/docs/operations/liqo-control-plane.md +++ b/docs/operations/liqo-control-plane.md @@ -98,6 +98,11 @@ labels are not signatures against an administrator of that API server. Use the same reviewed Snapshot build for the consumer operator, provider agent and PageBroker. The operator image also contains `/provenance-webhook`, deployed as a separate provider process without Snapshot reconciliation or storage access. +Its readiness check succeeds only after the manager starts the TLS webhook +server. If readiness reports `webhook server has not been started yet` while +liveness succeeds, check that the selected build registers the server through +`mgr.GetWebhookServer()` before starting the manager. + The provider's ordinary in-cluster configuration must continue to point to the provider. Never replace it with the consumer kubeconfig. diff --git a/operator/cmd/provenance-webhook/main.go b/operator/cmd/provenance-webhook/main.go index a632e15d5..2d865fb5e 100644 --- a/operator/cmd/provenance-webhook/main.go +++ b/operator/cmd/provenance-webhook/main.go @@ -71,7 +71,6 @@ func run(bindingPath, certDir, reflector string) error { return err } server := webhook.NewServer(webhook.Options{Port: 9443, CertDir: certDir}) - server.Register("/validate-shadowpod", &admission.Webhook{Handler: validator}) mgr, err := ctrl.NewManager(localConfig, ctrl.Options{ Scheme: scheme, WebhookServer: server, HealthProbeBindAddress: ":8081", Metrics: metricsserver.Options{BindAddress: "0"}, @@ -79,11 +78,21 @@ func run(bindingPath, certDir, reflector string) error { if err != nil { return err } + if err := setupWebhook(mgr, validator); err != nil { + return err + } + return mgr.Start(ctrl.SetupSignalHandler()) +} + +func setupWebhook(mgr ctrl.Manager, validator admission.Handler) error { + // GetWebhookServer also adds the configured server to the manager lifecycle. + server := mgr.GetWebhookServer() + server.Register("/validate-shadowpod", &admission.Webhook{Handler: validator}) if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { return err } if err := mgr.AddReadyzCheck("readyz", server.StartedChecker()); err != nil { return err } - return mgr.Start(ctrl.SetupSignalHandler()) + return nil } diff --git a/operator/cmd/provenance-webhook/main_test.go b/operator/cmd/provenance-webhook/main_test.go new file mode 100644 index 000000000..fcff904dc --- /dev/null +++ b/operator/cmd/provenance-webhook/main_test.go @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "crypto/tls" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func TestManagerStartsProvenanceWebhook(t *testing.T) { + // Reuse a loopback test certificate and port without cluster credentials. + tlsServer := httptest.NewTLSServer(http.NotFoundHandler()) + endpoint, err := url.Parse(tlsServer.URL) + require.NoError(t, err) + port, err := strconv.Atoi(endpoint.Port()) + require.NoError(t, err) + certificate := tlsServer.TLS.Certificates[0] + client := tlsServer.Client() + tlsServer.Close() + + server := webhook.NewServer(webhook.Options{ + Host: endpoint.Hostname(), Port: port, + TLSOpts: []func(*tls.Config){func(config *tls.Config) { + config.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return &certificate, nil + } + }}, + }) + mgr, err := ctrl.NewManager(&rest.Config{Host: "http://127.0.0.1:1"}, ctrl.Options{ + WebhookServer: server, HealthProbeBindAddress: "0", + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + require.NoError(t, err) + require.NoError(t, setupWebhook(mgr, admission.HandlerFunc(func(context.Context, admission.Request) admission.Response { + return admission.Allowed("test request") + }))) + checker := server.StartedChecker() + require.Error(t, checker(nil), "readiness must fail before manager startup") + + ctx, cancel := context.WithCancel(context.Background()) + finished := make(chan error, 1) + go func() { finished <- mgr.Start(ctx) }() + t.Cleanup(func() { + cancel() + select { + case err := <-finished: + require.NoError(t, err) + case <-time.After(5 * time.Second): + t.Error("manager did not stop after cancellation") + } + }) + require.Eventually(t, func() bool { return checker(nil) == nil }, 5*time.Second, 10*time.Millisecond) + client.Timeout = time.Second + response, err := client.Get(tlsServer.URL + "/validate-shadowpod") + require.NoError(t, err) + defer response.Body.Close() + // An empty request must reach admission validation, rather than a missing route. + require.Equal(t, http.StatusOK, response.StatusCode) + var review admissionv1.AdmissionReview + require.NoError(t, json.NewDecoder(response.Body).Decode(&review)) + require.NotNil(t, review.Response) + require.False(t, review.Response.Allowed) + require.NotNil(t, review.Response.Result) + require.EqualValues(t, http.StatusBadRequest, review.Response.Result.Code) +}