From ce26733562964cab0dad4ab8b1b5ed7f1f86cf9d Mon Sep 17 00:00:00 2001 From: David Kwon Date: Wed, 9 Sep 2026 16:24:08 -0400 Subject: [PATCH 1/2] Add watch permission for configmaps in webhook server ClusterRole The webhook server's httpfactory reads TLS certificate ConfigMaps through the cached client, which requires watch permission to set up an informer. Without it the informer never syncs and admission requests time out with EOF. Assisted-by: Claude Code Co-Authored-By: Claude Opus 4.6 Signed-off-by: David Kwon --- pkg/webhook/cluster_roles.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/webhook/cluster_roles.go b/pkg/webhook/cluster_roles.go index e23dee900..ba4ccf944 100755 --- a/pkg/webhook/cluster_roles.go +++ b/pkg/webhook/cluster_roles.go @@ -163,6 +163,7 @@ func getSpecClusterRole() (*v1.ClusterRole, error) { Verbs: []string{ "get", "list", + "watch", }, }, // Needed for pkg/config/sync.go:109 (SetupControllerConfig) From 4d228e053dd0616a5ac815e33188ec290f145dba Mon Sep 17 00:00:00 2001 From: David Kwon Date: Wed, 9 Sep 2026 16:26:49 -0400 Subject: [PATCH 2/2] Use non-caching client for DWT reads in webhook validation The webhook's resolveDevWorkspace used the manager's cached client to fetch DevWorkspaceTemplates, which created informers that cached all DWTs in memory. Co-Authored-By: Claude Opus 4.6 Signed-off-by: David Kwon --- webhook/main.go | 6 +++--- webhook/workspace/config.go | 6 +++--- webhook/workspace/handler/handler.go | 1 + webhook/workspace/handler/workspace.go | 2 +- webhook/workspace/handler/workspace_test.go | 5 +++-- webhook/workspace/mutate.go | 5 +++-- webhook/workspace/validate.go | 5 +++-- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/webhook/main.go b/webhook/main.go index 02c1bc720..e6d44d4df 100644 --- a/webhook/main.go +++ b/webhook/main.go @@ -170,7 +170,7 @@ func main() { os.Exit(1) } - err = createWebhooks(mgr) + err = createWebhooks(mgr, nonCachedClient) if err != nil { log.Error(err, "Failed to create webhooks") os.Exit(1) @@ -198,7 +198,7 @@ func main() { } } -func createWebhooks(mgr manager.Manager) error { +func createWebhooks(mgr manager.Manager, nonCachedClient client.Client) error { log.Info("Configuring Webhook Server") err := server.ConfigureWebhookServer(mgr) if err != nil { @@ -206,7 +206,7 @@ func createWebhooks(mgr manager.Manager) error { } log.Info("Configuring Webhooks") - if err := workspace.Configure(context.TODO(), mgr); err != nil { + if err := workspace.Configure(context.TODO(), mgr, nonCachedClient); err != nil { return err } return nil diff --git a/webhook/workspace/config.go b/webhook/workspace/config.go index b84eecc78..7ff2a0335 100644 --- a/webhook/workspace/config.go +++ b/webhook/workspace/config.go @@ -36,7 +36,7 @@ import ( ) // Configure configures mutate/validating webhooks that provides exec access into workspace for creator only -func Configure(ctx context.Context, mgr manager.Manager) error { +func Configure(ctx context.Context, mgr manager.Manager, nonCachingClient client.Client) error { log.Info("Configuring devworkspace webhooks") c, err := createClient() if err != nil { @@ -88,7 +88,7 @@ func Configure(ctx context.Context, mgr manager.Manager) error { log.Info("Created devworkspace mutating webhook configuration") } - server.GetWebhookServer().Register(mutateWebhookPath, &webhook.Admission{Handler: NewResourcesMutator(saUID, saName, mgr)}) + server.GetWebhookServer().Register(mutateWebhookPath, &webhook.Admission{Handler: NewResourcesMutator(saUID, saName, mgr, nonCachingClient)}) if err := c.Create(ctx, validateWebhookCfg); err != nil { if !apierrors.IsAlreadyExists(err) { @@ -111,7 +111,7 @@ func Configure(ctx context.Context, mgr manager.Manager) error { log.Info("Created devworkspace validating webhook configuration") } - server.GetWebhookServer().Register(validateWebhookPath, &webhook.Admission{Handler: NewResourcesValidator(saUID, saName, mgr)}) + server.GetWebhookServer().Register(validateWebhookPath, &webhook.Admission{Handler: NewResourcesValidator(saUID, saName, mgr, nonCachingClient)}) return nil } diff --git a/webhook/workspace/handler/handler.go b/webhook/workspace/handler/handler.go index a3b583b71..702f36faf 100644 --- a/webhook/workspace/handler/handler.go +++ b/webhook/workspace/handler/handler.go @@ -28,6 +28,7 @@ type WebhookHandler struct { ControllerUID string ControllerSAName string Client client.Client + NonCachingClient client.Client Decoder admission.Decoder } diff --git a/webhook/workspace/handler/workspace.go b/webhook/workspace/handler/workspace.go index 04a840dd1..833c14e89 100644 --- a/webhook/workspace/handler/workspace.go +++ b/webhook/workspace/handler/workspace.go @@ -294,7 +294,7 @@ func (h *WebhookHandler) resolveDevWorkspace( flattenHelpers := flatten.ResolverTools{ WorkspaceNamespace: workspace.Namespace, Context: ctx, - K8sClient: h.Client, + K8sClient: h.NonCachingClient, HttpClient: httpClient, DefaultResourceRequirements: workspace.Config.Workspace.DefaultContainerResources, } diff --git a/webhook/workspace/handler/workspace_test.go b/webhook/workspace/handler/workspace_test.go index 73ab7225f..6f7d3f8a0 100644 --- a/webhook/workspace/handler/workspace_test.go +++ b/webhook/workspace/handler/workspace_test.go @@ -88,8 +88,9 @@ func newTestWebhookHandler(t *testing.T) *WebhookHandler { WithScheme(s). Build() return &WebhookHandler{ - Client: fakeClient, - Decoder: admission.NewDecoder(s), + Client: fakeClient, + NonCachingClient: fakeClient, + Decoder: admission.NewDecoder(s), } } diff --git a/webhook/workspace/mutate.go b/webhook/workspace/mutate.go index d3cc4e9ce..2c18381ad 100644 --- a/webhook/workspace/mutate.go +++ b/webhook/workspace/mutate.go @@ -19,6 +19,7 @@ import ( "context" "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "github.com/devfile/devworkspace-operator/webhook/workspace/handler" @@ -33,8 +34,8 @@ type ResourcesMutator struct { *handler.WebhookHandler } -func NewResourcesMutator(controllerUID, controllerSAName string, mgr manager.Manager) *ResourcesMutator { - return &ResourcesMutator{&handler.WebhookHandler{ControllerUID: controllerUID, ControllerSAName: controllerSAName, Decoder: admission.NewDecoder(mgr.GetScheme()), Client: mgr.GetClient()}} +func NewResourcesMutator(controllerUID, controllerSAName string, mgr manager.Manager, nonCachingClient client.Client) *ResourcesMutator { + return &ResourcesMutator{&handler.WebhookHandler{ControllerUID: controllerUID, ControllerSAName: controllerSAName, Decoder: admission.NewDecoder(mgr.GetScheme()), Client: mgr.GetClient(), NonCachingClient: nonCachingClient}} } // ResourcesMutator verify if operation is a valid from Workspace controller perspective diff --git a/webhook/workspace/validate.go b/webhook/workspace/validate.go index 4a38cfb90..9fdf1304d 100644 --- a/webhook/workspace/validate.go +++ b/webhook/workspace/validate.go @@ -19,6 +19,7 @@ import ( "context" "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "github.com/devfile/devworkspace-operator/webhook/workspace/handler" @@ -33,8 +34,8 @@ type ResourcesValidator struct { *handler.WebhookHandler } -func NewResourcesValidator(controllerUID, controllerSAName string, mgr manager.Manager) *ResourcesValidator { - return &ResourcesValidator{&handler.WebhookHandler{ControllerUID: controllerUID, ControllerSAName: controllerSAName, Decoder: admission.NewDecoder(mgr.GetScheme()), Client: mgr.GetClient()}} +func NewResourcesValidator(controllerUID, controllerSAName string, mgr manager.Manager, nonCachingClient client.Client) *ResourcesValidator { + return &ResourcesValidator{&handler.WebhookHandler{ControllerUID: controllerUID, ControllerSAName: controllerSAName, Decoder: admission.NewDecoder(mgr.GetScheme()), Client: mgr.GetClient(), NonCachingClient: nonCachingClient}} } func (v *ResourcesValidator) Handle(ctx context.Context, req admission.Request) admission.Response {