From 69a2f406152d3339c3a9a2582e13b4e770175590 Mon Sep 17 00:00:00 2001 From: Casey Davenport Date: Wed, 20 May 2026 13:29:33 -0700 Subject: [PATCH 1/8] Bump k8s deps to v0.36.1 and controller-runtime to v0.24.1 --- go.mod | 29 +++++++++---------- go.sum | 90 ++++++++++++++++++++++++++++------------------------------ 2 files changed, 58 insertions(+), 61 deletions(-) diff --git a/go.mod b/go.mod index 654bc4a99c..ddb100bac1 100644 --- a/go.mod +++ b/go.mod @@ -45,14 +45,14 @@ require ( gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.20.2 - k8s.io/api v0.35.4 - k8s.io/apiextensions-apiserver v0.35.4 - k8s.io/apimachinery v0.35.4 - k8s.io/apiserver v0.35.4 - k8s.io/client-go v0.35.4 - k8s.io/kube-aggregator v0.35.4 + k8s.io/api v0.36.1 + k8s.io/apiextensions-apiserver v0.36.1 + k8s.io/apimachinery v0.36.1 + k8s.io/apiserver v0.36.1 + k8s.io/client-go v0.36.1 + k8s.io/kube-aggregator v0.36.1 k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 - sigs.k8s.io/controller-runtime v0.23.3 + sigs.k8s.io/controller-runtime v0.24.1 sigs.k8s.io/gateway-api v1.4.1 sigs.k8s.io/kind v0.31.0 // Do not remove, not used by code but used by build sigs.k8s.io/secrets-store-csi-driver v1.6.0 @@ -120,7 +120,6 @@ require ( github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gosuri/uitable v0.0.4 // indirect - github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect @@ -186,22 +185,22 @@ require ( golang.org/x/tools v0.44.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/grpc v1.79.3 // indirect - google.golang.org/protobuf v1.36.11 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect howett.net/plist v1.0.1 // indirect - k8s.io/cli-runtime v0.35.3 // indirect - k8s.io/component-base v0.35.4 // indirect + k8s.io/cli-runtime v0.36.1 // indirect + k8s.io/component-base v0.36.1 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect - k8s.io/kubectl v0.35.3 // indirect + k8s.io/kubectl v0.36.1 // indirect oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect - sigs.k8s.io/kustomize/api v0.20.1 // indirect - sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect + sigs.k8s.io/kustomize/api v0.21.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) diff --git a/go.sum b/go.sum index 0609643885..bfc19f0bc1 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,8 @@ github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEm github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= github.com/corazawaf/coraza-coreruleset/v4 v4.25.0 h1:tqFO1lfVpTiyWtlN618OXpZMfw+nnN0Q4///W5W+/HM= github.com/corazawaf/coraza-coreruleset/v4 v4.25.0/go.mod h1:nRuGXITxOPvsLF2VxaTB7pYok8QB8BitX3ZenXcUryY= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -231,10 +231,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -384,8 +382,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= @@ -448,8 +446,8 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGh go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= @@ -460,10 +458,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= go.opentelemetry.io/otel/exporters/prometheus v0.61.0 h1:cCyZS4dr67d30uDyh8etKM2QyDsQ4zC9ds3bdbrVoD0= @@ -502,8 +500,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= -golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -543,14 +541,14 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -578,44 +576,44 @@ helm.sh/helm/v3 v3.20.2 h1:binM4rvPx5DcNsa1sIt7UZi55lRbu3pZUFmQkSoRh48= helm.sh/helm/v3 v3.20.2/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -k8s.io/api v0.35.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988= -k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU= -k8s.io/apiextensions-apiserver v0.35.4 h1:HeP+Upp7ItdvnyGmub0yoix+2z5+ev4M5cE5TCgtOUU= -k8s.io/apiextensions-apiserver v0.35.4/go.mod h1:ogQlk+stIE8mnoRthSYCwlOS12fVqgWFiErMwPaXA7c= -k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds= -k8s.io/apimachinery v0.35.4/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc= -k8s.io/apiserver v0.35.4 h1:vtuFqNFmF9bPRdHDL2lpK6qCTPWDreZJL4LRPwVM6ho= -k8s.io/apiserver v0.35.4/go.mod h1:JnBcb+J8kFXKpZkgcbcUnPBBHi4qgBii1I7dLxFY/oo= -k8s.io/cli-runtime v0.35.3 h1:UZq4ipNimtzBmhN7PPKbfAdqo8quK0H0UdGl6qAQnqI= -k8s.io/cli-runtime v0.35.3/go.mod h1:O7MUmCqcKSd5xI+O5X7/pRkB5l0O2NIhOdUVwbHLXu4= -k8s.io/client-go v0.35.4 h1:DN6fyaGuzK64UvnKO5fOA6ymSjvfGAnCAHAR0C66kD8= -k8s.io/client-go v0.35.4/go.mod h1:2Pg9WpsS4NeOpoYTfHHfMxBG8zFMSAUi4O/qoiJC3nY= -k8s.io/component-base v0.35.4 h1:6n1tNJ87johN0Hif0Fs8K2GMthsaUwMqCebUDLYyv7U= -k8s.io/component-base v0.35.4/go.mod h1:qaDJgz5c1KYKla9occFmlJEfPpkuA55s90G509R+PeY= +k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= +k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks= +k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/apiserver v0.36.1 h1:iMS5V+rPUertv5P9RaqJgmHHTuh4quWpoxchvMUY+JY= +k8s.io/apiserver v0.36.1/go.mod h1:Cby1PbLWztu0GDOxoO6iFOyyqIsziHNEW+w9zVQ22Kw= +k8s.io/cli-runtime v0.36.1 h1:yuC/BGnnj1YYPh6D1P+pZnzinCs6DvMq86yAeNqoqzM= +k8s.io/cli-runtime v0.36.1/go.mod h1:ZQWHGt8xAF7KnviB79vX0lYNyUUqKIpU+LQg7exuFAw= +k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= +k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= +k8s.io/component-base v0.36.1 h1:iG6GsELftXqTNG9HG6kiVjatSgAw1sf5pJ6R5a6N0kA= +k8s.io/component-base v0.36.1/go.mod h1:nf9XPlntRdqO6WMeEWAA5F93Y4ICZQdeT9GeqLDB3JI= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kube-aggregator v0.35.4 h1:6eR50WHwqSYJQTR6QxEG5fRW2vBA6Yoqzp72hw76koE= -k8s.io/kube-aggregator v0.35.4/go.mod h1:13mmXpCW9sReIQR8yLvApbKphZfoGnK39UJ8u1opT9g= +k8s.io/kube-aggregator v0.36.1 h1:IzNeRsJcTtgsiCyTgCR1pSwWCrXC1QZQWMTcBw18cFQ= +k8s.io/kube-aggregator v0.36.1/go.mod h1:ROrIm5irUhVUJsKVCgBAAcXpK5IiqpdCn0Ka7LYMGs4= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= -k8s.io/kubectl v0.35.3 h1:1KqSYXk/sodU7VeDvK6atX2kAGUZd2QTeR5K7Hb9r9w= -k8s.io/kubectl v0.35.3/go.mod h1:GPHxZqRe+u/i3gTBoVQHeIyq2NilfNPj9hDWeuN3x5s= +k8s.io/kubectl v0.36.1 h1:96HqS9twIdHM0MlJLTwbo14b9kUKPkOzZ4tlRDLv4qI= +k8s.io/kubectl v0.36.1/go.mod h1:/DGPAIewKsFWF9VFgGvkPhao2Ev4SNuE3BioZo8yPbk= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= -sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/gateway-api v1.4.1 h1:NPxFutNkKNa8UfLd2CMlEuhIPMQgDQ6DXNKG9sHbJU8= sigs.k8s.io/gateway-api v1.4.1/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kind v0.31.0 h1:UcT4nzm+YM7YEbqiAKECk+b6dsvc/HRZZu9U0FolL1g= sigs.k8s.io/kind v0.31.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= -sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= -sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= -sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= -sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= +sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= +sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= +sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/secrets-store-csi-driver v1.6.0 h1:YpKG/2hJkp3EkRGpH5SPxg1/5AkmeD5pwHNKIlE90FU= From 43b3e015d0575191a05b0d4d16f93858651f6e98 Mon Sep 17 00:00:00 2001 From: Casey Davenport Date: Wed, 20 May 2026 13:53:18 -0700 Subject: [PATCH 2/8] Discover served MutatingAdmissionPolicy API version at runtime K8s 1.36 promotes MutatingAdmissionPolicy to v1; v1beta1 is scheduled for removal in 1.37. Hardcoding v1beta1 broke operator reconciles on clusters that only serve v1. Use the RESTMapper to pick the served version (prefer v1), and parse/list/sync at that version. --- pkg/common/kubernetes_version.go | 6 - .../installation/core_controller.go | 47 ++-- .../installation/core_controller_test.go | 231 +++++++++--------- pkg/imports/admission/admission.go | 169 +++++++++++-- pkg/imports/admission/admission_test.go | 170 +++++++++---- 5 files changed, 408 insertions(+), 215 deletions(-) diff --git a/pkg/common/kubernetes_version.go b/pkg/common/kubernetes_version.go index 4f597b5995..2240f3c4d4 100644 --- a/pkg/common/kubernetes_version.go +++ b/pkg/common/kubernetes_version.go @@ -59,9 +59,3 @@ func (v *VersionInfo) ProvidesCertV1API() bool { } return false } - -// ProvidesMutatingAdmissionPolicyV1Beta1 returns if admissionregistration.k8s.io/v1beta1 MutatingAdmissionPolicy -// is supported given the current k8s version (introduced in k8s 1.32). -func (v *VersionInfo) ProvidesMutatingAdmissionPolicyV1Beta1() bool { - return v != nil && (v.Major > 1 || (v.Major == 1 && v.Minor >= 32)) -} diff --git a/pkg/controller/installation/core_controller.go b/pkg/controller/installation/core_controller.go index ebce8b40f4..7e62e7141a 100644 --- a/pkg/controller/installation/core_controller.go +++ b/pkg/controller/installation/core_controller.go @@ -31,7 +31,6 @@ import ( "github.com/go-logr/logr" configv1 "github.com/openshift/api/config/v1" - admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" @@ -2269,47 +2268,45 @@ func (r *ReconcileInstallation) updateMutatingAdmissionPolicies(ctx context.Cont if !r.manageCRDs || !r.v3CRDs { return nil } - if !r.kubernetesVersion.ProvidesMutatingAdmissionPolicyV1Beta1() { - r.status.SetDegraded(operatorv1.ResourceNotReady, "Kubernetes version does not support MutatingAdmissionPolicy v1beta1 (requires v1.32+); policy defaulting will not be available", nil, log) + + // Discover which served version of MutatingAdmissionPolicy is available. v1 was promoted to GA + // in k8s 1.36 and v1beta1 (introduced in 1.32) is scheduled for removal in 1.37, so the cluster + // may serve either or both. + apiVersion := admission.DiscoverAPIVersion(r.client.RESTMapper()) + if apiVersion == "" { + r.status.SetDegraded(operatorv1.ResourceNotReady, "Kubernetes cluster does not serve MutatingAdmissionPolicy (requires v1.32+); policy defaulting will not be available", nil, log) return nil } - desired := admission.GetMutatingAdmissionPolicies(install.Spec.Variant, r.v3CRDs) + desired := admission.GetMutatingAdmissionPolicies(install.Spec.Variant, r.v3CRDs, apiVersion) - // Build a set of desired resource names for comparison. + // Build sets of desired resource names for comparison. desiredMAPs := map[string]bool{} desiredMAPBs := map[string]bool{} for _, obj := range desired { - switch obj.(type) { - case *admissionv1beta1.MutatingAdmissionPolicy: + switch { + case admission.IsPolicyKind(obj): desiredMAPs[obj.GetName()] = true - case *admissionv1beta1.MutatingAdmissionPolicyBinding: + case admission.IsBindingKind(obj): desiredMAPBs[obj.GetName()] = true } } - // Find stale MAPs that are labeled as managed but no longer desired. - existingMAPs := &admissionv1beta1.MutatingAdmissionPolicyList{} - if err := r.client.List(ctx, existingMAPs, client.MatchingLabels{admission.ManagedMAPLabel: admission.ManagedMAPLabelValue}); err != nil { - r.status.SetDegraded(operatorv1.ResourceReadError, "Error listing MutatingAdmissionPolicies", err, log) + // Find stale managed resources at the discovered API version. + existingMAPs, existingMAPBs, err := admission.ListManaged(ctx, r.client, apiVersion) + if err != nil { + r.status.SetDegraded(operatorv1.ResourceReadError, "Error listing managed MutatingAdmissionPolicy resources", err, log) return err } var toDelete []client.Object - for i := range existingMAPs.Items { - if !desiredMAPs[existingMAPs.Items[i].Name] { - toDelete = append(toDelete, &existingMAPs.Items[i]) + for _, obj := range existingMAPs { + if !desiredMAPs[obj.GetName()] { + toDelete = append(toDelete, obj) } } - - // Find stale MAPBs that are labeled as managed but no longer desired. - existingMAPBs := &admissionv1beta1.MutatingAdmissionPolicyBindingList{} - if err := r.client.List(ctx, existingMAPBs, client.MatchingLabels{admission.ManagedMAPLabel: admission.ManagedMAPLabelValue}); err != nil { - r.status.SetDegraded(operatorv1.ResourceReadError, "Error listing MutatingAdmissionPolicyBindings", err, log) - return err - } - for i := range existingMAPBs.Items { - if !desiredMAPBs[existingMAPBs.Items[i].Name] { - toDelete = append(toDelete, &existingMAPBs.Items[i]) + for _, obj := range existingMAPBs { + if !desiredMAPBs[obj.GetName()] { + toDelete = append(toDelete, obj) } } diff --git a/pkg/controller/installation/core_controller_test.go b/pkg/controller/installation/core_controller_test.go index aa113b34e3..fb4eecf02b 100644 --- a/pkg/controller/installation/core_controller_test.go +++ b/pkg/controller/installation/core_controller_test.go @@ -27,6 +27,7 @@ import ( "github.com/go-logr/logr" "github.com/stretchr/testify/mock" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -34,8 +35,10 @@ import ( rbacv1 "k8s.io/api/rbac/v1" schedv1 "k8s.io/api/scheduling/v1" storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" kfake "k8s.io/client-go/kubernetes/fake" @@ -2559,7 +2562,6 @@ func (f *fakeComponentHandler) CreateOrUpdateOrDelete(ctx context.Context, compo var _ = Describe("updateMutatingAdmissionPolicies", func() { var ( - c client.Client ctx context.Context cancel context.CancelFunc r ReconcileInstallation @@ -2570,6 +2572,27 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { installation *operator.Installation ) + // mapMapper builds a RESTMapper advertising the requested served versions of + // admissionregistration.k8s.io MutatingAdmissionPolicy / MutatingAdmissionPolicyBinding. + mapMapper := func(versions ...string) meta.RESTMapper { + gv := []schema.GroupVersion{} + mapper := meta.NewDefaultRESTMapper(gv) + for _, v := range versions { + gvk := schema.GroupVersionKind{Group: admission.APIGroup, Version: v, Kind: "MutatingAdmissionPolicy"} + gvkB := schema.GroupVersionKind{Group: admission.APIGroup, Version: v, Kind: "MutatingAdmissionPolicyBinding"} + mapper.Add(gvk, meta.RESTScopeRoot) + mapper.Add(gvkB, meta.RESTScopeRoot) + } + return mapper + } + + clientFor := func(mapper meta.RESTMapper, initial ...client.Object) client.Client { + return ctrlrfake.DefaultFakeClientBuilder(scheme). + WithRESTMapper(mapper). + WithObjects(initial...). + Build() + } + BeforeEach(func() { log = logr.Discard() ctx, cancel = context.WithCancel(context.Background()) @@ -2577,10 +2600,9 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { scheme = runtime.NewScheme() Expect(apis.AddToScheme(scheme, false)).NotTo(HaveOccurred()) Expect(operator.SchemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred()) + Expect(admissionregistrationv1.SchemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred()) Expect(admissionv1beta1.SchemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred()) - c = ctrlrfake.DefaultFakeClientBuilder(scheme).Build() - mockStatus = &status.MockStatus{} mockStatus.On("SetDegraded", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return() @@ -2597,31 +2619,28 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { cancel() }) - It("should create MAPs when manageCRDs=true, v3CRDs=true, k8s>=1.32", func() { + It("should create v1 MAPs when v1 is served", func() { r = ReconcileInstallation{ - client: c, - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - kubernetesVersion: &common.VersionInfo{Major: 1, Minor: 32}, + client: clientFor(mapMapper(admission.VersionV1)), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, } - err := r.updateMutatingAdmissionPolicies(ctx, installation, log) - Expect(err).NotTo(HaveOccurred()) + Expect(r.updateMutatingAdmissionPolicies(ctx, installation, log)).NotTo(HaveOccurred()) Expect(componentHandler.objectsToCreate).To(HaveLen(4)) - // Verify we got two MAPs and two MAPBs. var mapCount, mapbCount int for _, obj := range componentHandler.objectsToCreate { switch obj.(type) { - case *admissionv1beta1.MutatingAdmissionPolicy: + case *admissionregistrationv1.MutatingAdmissionPolicy: mapCount++ Expect(obj.GetLabels()).To(HaveKeyWithValue(admission.ManagedMAPLabel, admission.ManagedMAPLabelValue)) - case *admissionv1beta1.MutatingAdmissionPolicyBinding: + case *admissionregistrationv1.MutatingAdmissionPolicyBinding: mapbCount++ Expect(obj.GetLabels()).To(HaveKeyWithValue(admission.ManagedMAPLabel, admission.ManagedMAPLabelValue)) } @@ -2630,116 +2649,110 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { Expect(mapbCount).To(Equal(2)) }) - It("should not create MAPs when k8s<1.32 and should set degraded", func() { + It("should create v1beta1 MAPs when only v1beta1 is served", func() { r = ReconcileInstallation{ - client: c, - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - kubernetesVersion: &common.VersionInfo{Major: 1, Minor: 31}, + client: clientFor(mapMapper(admission.VersionV1Beta1)), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, } - err := r.updateMutatingAdmissionPolicies(ctx, installation, log) - Expect(err).NotTo(HaveOccurred()) - Expect(componentHandler.objectsToCreate).To(BeEmpty()) - mockStatus.AssertCalled(GinkgoT(), "SetDegraded", operator.ResourceNotReady, mock.Anything, mock.Anything, mock.Anything) + Expect(r.updateMutatingAdmissionPolicies(ctx, installation, log)).NotTo(HaveOccurred()) + Expect(componentHandler.objectsToCreate).To(HaveLen(4)) + + var mapCount, mapbCount int + for _, obj := range componentHandler.objectsToCreate { + switch obj.(type) { + case *admissionv1beta1.MutatingAdmissionPolicy: + mapCount++ + case *admissionv1beta1.MutatingAdmissionPolicyBinding: + mapbCount++ + } + } + Expect(mapCount).To(Equal(2)) + Expect(mapbCount).To(Equal(2)) }) - It("should not create MAPs when v3CRDs=false", func() { + It("should not create MAPs when no served version exists and should set degraded", func() { r = ReconcileInstallation{ - client: c, - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: false, - kubernetesVersion: &common.VersionInfo{Major: 1, Minor: 32}, + client: clientFor(mapMapper()), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, } - err := r.updateMutatingAdmissionPolicies(ctx, installation, log) - Expect(err).NotTo(HaveOccurred()) + Expect(r.updateMutatingAdmissionPolicies(ctx, installation, log)).NotTo(HaveOccurred()) Expect(componentHandler.objectsToCreate).To(BeEmpty()) + mockStatus.AssertCalled(GinkgoT(), "SetDegraded", operator.ResourceNotReady, mock.Anything, mock.Anything, mock.Anything) }) - It("should not create MAPs when manageCRDs=false", func() { + It("should not create MAPs when v3CRDs=false", func() { r = ReconcileInstallation{ - client: c, - scheme: scheme, - status: mockStatus, - manageCRDs: false, - v3CRDs: true, - kubernetesVersion: &common.VersionInfo{Major: 1, Minor: 32}, + client: clientFor(mapMapper(admission.VersionV1)), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: false, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, } - err := r.updateMutatingAdmissionPolicies(ctx, installation, log) - Expect(err).NotTo(HaveOccurred()) + Expect(r.updateMutatingAdmissionPolicies(ctx, installation, log)).NotTo(HaveOccurred()) Expect(componentHandler.objectsToCreate).To(BeEmpty()) }) - It("should not create MAPs when kubernetesVersion is nil and should set degraded", func() { + It("should not create MAPs when manageCRDs=false", func() { r = ReconcileInstallation{ - client: c, - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - kubernetesVersion: nil, + client: clientFor(mapMapper(admission.VersionV1)), + scheme: scheme, + status: mockStatus, + manageCRDs: false, + v3CRDs: true, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, } - err := r.updateMutatingAdmissionPolicies(ctx, installation, log) - Expect(err).NotTo(HaveOccurred()) + Expect(r.updateMutatingAdmissionPolicies(ctx, installation, log)).NotTo(HaveOccurred()) Expect(componentHandler.objectsToCreate).To(BeEmpty()) - mockStatus.AssertCalled(GinkgoT(), "SetDegraded", operator.ResourceNotReady, mock.Anything, mock.Anything, mock.Anything) }) - It("should delete stale MAPs with managed label", func() { - // Pre-create a stale MAP with the managed label. - staleMAP := &admissionv1beta1.MutatingAdmissionPolicy{ + It("should delete stale v1 MAPs with managed label", func() { + staleMAP := &admissionregistrationv1.MutatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "stale-policy", Labels: map[string]string{admission.ManagedMAPLabel: admission.ManagedMAPLabelValue}, }, } - staleMAPB := &admissionv1beta1.MutatingAdmissionPolicyBinding{ + staleMAPB := &admissionregistrationv1.MutatingAdmissionPolicyBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "stale-binding", Labels: map[string]string{admission.ManagedMAPLabel: admission.ManagedMAPLabelValue}, }, } - Expect(c.Create(ctx, staleMAP)).NotTo(HaveOccurred()) - Expect(c.Create(ctx, staleMAPB)).NotTo(HaveOccurred()) r = ReconcileInstallation{ - client: c, - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - kubernetesVersion: &common.VersionInfo{Major: 1, Minor: 32}, + client: clientFor(mapMapper(admission.VersionV1), staleMAP, staleMAPB), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, } - err := r.updateMutatingAdmissionPolicies(ctx, installation, log) - Expect(err).NotTo(HaveOccurred()) - - // Should have created the desired resources. + Expect(r.updateMutatingAdmissionPolicies(ctx, installation, log)).NotTo(HaveOccurred()) Expect(componentHandler.objectsToCreate).To(HaveLen(4)) - - // Should have marked the stale resources for deletion. Expect(componentHandler.objectsToDelete).To(HaveLen(2)) deletedNames := map[string]bool{} for _, obj := range componentHandler.objectsToDelete { @@ -2750,66 +2763,47 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { }) It("should not delete MAPs that are in the desired set", func() { - // Pre-create the desired MAPs with the managed label (simulating a previous reconcile). - desiredMAP1 := &admissionv1beta1.MutatingAdmissionPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "policytypes.policy.projectcalico.org", - Labels: map[string]string{admission.ManagedMAPLabel: admission.ManagedMAPLabelValue}, - }, - } - desiredMAP2 := &admissionv1beta1.MutatingAdmissionPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "tierlabel.policy.projectcalico.org", - Labels: map[string]string{admission.ManagedMAPLabel: admission.ManagedMAPLabelValue}, - }, - } - desiredMAPB1 := &admissionv1beta1.MutatingAdmissionPolicyBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "set-policytypes-binding", - Labels: map[string]string{admission.ManagedMAPLabel: admission.ManagedMAPLabelValue}, - }, + var initial []client.Object + for _, n := range []string{"policytypes.policy.projectcalico.org", "tierlabel.policy.projectcalico.org"} { + initial = append(initial, &admissionregistrationv1.MutatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: n, + Labels: map[string]string{admission.ManagedMAPLabel: admission.ManagedMAPLabelValue}, + }, + }) } - desiredMAPB2 := &admissionv1beta1.MutatingAdmissionPolicyBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "set-tier-label-binding", - Labels: map[string]string{admission.ManagedMAPLabel: admission.ManagedMAPLabelValue}, - }, + for _, n := range []string{"set-policytypes-binding", "set-tier-label-binding"} { + initial = append(initial, &admissionregistrationv1.MutatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: n, + Labels: map[string]string{admission.ManagedMAPLabel: admission.ManagedMAPLabelValue}, + }, + }) } - Expect(c.Create(ctx, desiredMAP1)).NotTo(HaveOccurred()) - Expect(c.Create(ctx, desiredMAP2)).NotTo(HaveOccurred()) - Expect(c.Create(ctx, desiredMAPB1)).NotTo(HaveOccurred()) - Expect(c.Create(ctx, desiredMAPB2)).NotTo(HaveOccurred()) r = ReconcileInstallation{ - client: c, - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - kubernetesVersion: &common.VersionInfo{Major: 1, Minor: 32}, + client: clientFor(mapMapper(admission.VersionV1), initial...), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, } - err := r.updateMutatingAdmissionPolicies(ctx, installation, log) - Expect(err).NotTo(HaveOccurred()) - - // Should have created the desired resources (update via passthrough). + Expect(r.updateMutatingAdmissionPolicies(ctx, installation, log)).NotTo(HaveOccurred()) Expect(componentHandler.objectsToCreate).To(HaveLen(4)) - - // Should NOT have deleted anything since existing resources match desired set. Expect(componentHandler.objectsToDelete).To(BeEmpty()) }) It("should work with Enterprise variant", func() { r = ReconcileInstallation{ - client: c, - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - kubernetesVersion: &common.VersionInfo{Major: 1, Minor: 32}, + client: clientFor(mapMapper(admission.VersionV1)), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2817,8 +2811,7 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { installation.Spec.Variant = operator.CalicoEnterprise - err := r.updateMutatingAdmissionPolicies(ctx, installation, log) - Expect(err).NotTo(HaveOccurred()) + Expect(r.updateMutatingAdmissionPolicies(ctx, installation, log)).NotTo(HaveOccurred()) Expect(componentHandler.objectsToCreate).To(HaveLen(4)) }) }) diff --git a/pkg/imports/admission/admission.go b/pkg/imports/admission/admission.go index 5731149b9b..5bb9a46368 100644 --- a/pkg/imports/admission/admission.go +++ b/pkg/imports/admission/admission.go @@ -23,8 +23,12 @@ import ( "time" "github.com/go-logr/logr" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" @@ -37,6 +41,13 @@ const ( ManagedMAPLabel = "operator.tigera.io/mutating-admission-policy" // ManagedMAPLabelValue is the label value for operator-managed MAP resources. ManagedMAPLabelValue = "managed" + + // APIGroup is the API group for MutatingAdmissionPolicy resources. + APIGroup = "admissionregistration.k8s.io" + // VersionV1 is the GA API version (k8s 1.36+). + VersionV1 = "v1" + // VersionV1Beta1 is the beta API version (k8s 1.32-1.36). + VersionV1Beta1 = "v1beta1" ) var ( @@ -46,11 +57,26 @@ var ( enterpriseAdmissionFiles embed.FS ) +// DiscoverAPIVersion returns which served version of MutatingAdmissionPolicy is available on the +// cluster: "v1" (preferred), "v1beta1", or "" if neither is served. The K8s version varies which +// version is registered: v1beta1 was introduced in 1.32 and v1 graduated in 1.36, with v1beta1 +// scheduled for removal in 1.37. +func DiscoverAPIVersion(mapper meta.RESTMapper) string { + gk := schema.GroupKind{Group: APIGroup, Kind: "MutatingAdmissionPolicy"} + for _, v := range []string{VersionV1, VersionV1Beta1} { + if _, err := mapper.RESTMapping(gk, v); err == nil { + return v + } + } + return "" +} + // GetMutatingAdmissionPolicies returns MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding -// objects for the given variant. These are only applicable when v3 CRDs are enabled. +// objects for the given variant, typed at the requested API version. These are only applicable +// when v3 CRDs are enabled. // Each returned object is labeled with ManagedMAPLabel to enable stale resource cleanup. -func GetMutatingAdmissionPolicies(variant opv1.ProductVariant, v3 bool) []client.Object { - if !v3 { +func GetMutatingAdmissionPolicies(variant opv1.ProductVariant, v3 bool, apiVersion string) []client.Object { + if !v3 || apiVersion == "" { return nil } @@ -83,7 +109,7 @@ func GetMutatingAdmissionPolicies(variant opv1.ProductVariant, v3 bool) []client continue } - obj, err := parseAdmissionPolicyYAML(doc, entry.Name()) + obj, err := parseAdmissionPolicyYAML(doc, entry.Name(), apiVersion) if err != nil { panic(fmt.Sprintf("Failed to parse admission policy %s: %v", entry.Name(), err)) } @@ -104,14 +130,22 @@ func GetMutatingAdmissionPolicies(variant opv1.ProductVariant, v3 bool) []client } // Ensure ensures that MutatingAdmissionPolicies necessary for bootstrapping exist in the cluster. -// Further reconciliation is handled by the core controller. If the API is not available (K8s < 1.32), -// a warning is logged and the function returns nil. MAPs are only installed when v3 CRDs are enabled. +// Further reconciliation is handled by the core controller. If the API is not available (no served +// version of MutatingAdmissionPolicy), a warning is logged and the function returns nil. MAPs are +// only installed when v3 CRDs are enabled. func Ensure(c client.Client, variant string, v3 bool, log logr.Logger) error { if !v3 { return nil } - objs := GetMutatingAdmissionPolicies(opv1.ProductVariant(variant), v3) + apiVersion := DiscoverAPIVersion(c.RESTMapper()) + if apiVersion == "" { + log.Info("MutatingAdmissionPolicy API not available on cluster, skipping bootstrap") + return nil + } + log.Info("Using discovered MutatingAdmissionPolicy API version", "version", apiVersion) + + objs := GetMutatingAdmissionPolicies(opv1.ProductVariant(variant), v3, apiVersion) for _, obj := range objs { log.Info("ensuring MutatingAdmissionPolicy resource exists", "name", obj.GetName(), "kind", obj.GetObjectKind().GroupVersionKind().Kind) @@ -124,7 +158,7 @@ func Ensure(c client.Client, variant string, v3 bool, log logr.Logger) error { continue } - // If the API is not available (K8s < 1.32), log a warning and skip. + // If the API is not available, log a warning and skip. if errors.IsNotFound(err) || errors.IsForbidden(err) { log.Info("MutatingAdmissionPolicy API not available, skipping", "error", err) return nil @@ -140,9 +174,10 @@ func Ensure(c client.Client, variant string, v3 bool, log logr.Logger) error { } // parseAdmissionPolicyYAML parses a YAML document into either a MutatingAdmissionPolicy -// or MutatingAdmissionPolicyBinding based on its kind field. -func parseAdmissionPolicyYAML(doc []byte, filename string) (client.Object, error) { - // First, determine the kind. +// or MutatingAdmissionPolicyBinding at the requested API version. The MAP types are identical +// in shape between v1beta1 and v1, so we deserialize the same YAML into the requested target type +// and overwrite TypeMeta to reflect the chosen GroupVersion. +func parseAdmissionPolicyYAML(doc []byte, filename, apiVersion string) (client.Object, error) { var meta struct { Kind string `json:"kind"` } @@ -150,20 +185,108 @@ func parseAdmissionPolicyYAML(doc []byte, filename string) (client.Object, error return nil, fmt.Errorf("unable to determine kind from %s: %v", filename, err) } - switch meta.Kind { - case "MutatingAdmissionPolicy": - obj := &admissionv1beta1.MutatingAdmissionPolicy{} - if err := yaml.Unmarshal(doc, obj); err != nil { - return nil, fmt.Errorf("unable to parse MutatingAdmissionPolicy from %s: %v", filename, err) + gv := APIGroup + "/" + apiVersion + + switch apiVersion { + case VersionV1: + switch meta.Kind { + case "MutatingAdmissionPolicy": + obj := &admissionregistrationv1.MutatingAdmissionPolicy{} + if err := yaml.Unmarshal(doc, obj); err != nil { + return nil, fmt.Errorf("unable to parse MutatingAdmissionPolicy from %s: %v", filename, err) + } + obj.TypeMeta = metav1.TypeMeta{Kind: meta.Kind, APIVersion: gv} + return obj, nil + case "MutatingAdmissionPolicyBinding": + obj := &admissionregistrationv1.MutatingAdmissionPolicyBinding{} + if err := yaml.Unmarshal(doc, obj); err != nil { + return nil, fmt.Errorf("unable to parse MutatingAdmissionPolicyBinding from %s: %v", filename, err) + } + obj.TypeMeta = metav1.TypeMeta{Kind: meta.Kind, APIVersion: gv} + return obj, nil } - return obj, nil - case "MutatingAdmissionPolicyBinding": - obj := &admissionv1beta1.MutatingAdmissionPolicyBinding{} - if err := yaml.Unmarshal(doc, obj); err != nil { - return nil, fmt.Errorf("unable to parse MutatingAdmissionPolicyBinding from %s: %v", filename, err) + case VersionV1Beta1: + switch meta.Kind { + case "MutatingAdmissionPolicy": + obj := &admissionv1beta1.MutatingAdmissionPolicy{} + if err := yaml.Unmarshal(doc, obj); err != nil { + return nil, fmt.Errorf("unable to parse MutatingAdmissionPolicy from %s: %v", filename, err) + } + obj.TypeMeta = metav1.TypeMeta{Kind: meta.Kind, APIVersion: gv} + return obj, nil + case "MutatingAdmissionPolicyBinding": + obj := &admissionv1beta1.MutatingAdmissionPolicyBinding{} + if err := yaml.Unmarshal(doc, obj); err != nil { + return nil, fmt.Errorf("unable to parse MutatingAdmissionPolicyBinding from %s: %v", filename, err) + } + obj.TypeMeta = metav1.TypeMeta{Kind: meta.Kind, APIVersion: gv} + return obj, nil } - return obj, nil default: - return nil, fmt.Errorf("unexpected kind %q in %s", meta.Kind, filename) + return nil, fmt.Errorf("unsupported MutatingAdmissionPolicy API version %q", apiVersion) + } + return nil, fmt.Errorf("unexpected kind %q in %s", meta.Kind, filename) +} + +// ListManaged returns the operator-managed MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding +// objects currently present on the cluster at the given API version. Returns nil if apiVersion is empty. +func ListManaged(ctx context.Context, c client.Client, apiVersion string) (policies, bindings []client.Object, err error) { + if apiVersion == "" { + return nil, nil, nil + } + switch apiVersion { + case VersionV1: + mapList := &admissionregistrationv1.MutatingAdmissionPolicyList{} + if err := c.List(ctx, mapList, client.MatchingLabels{ManagedMAPLabel: ManagedMAPLabelValue}); err != nil { + return nil, nil, fmt.Errorf("listing MutatingAdmissionPolicies: %w", err) + } + for i := range mapList.Items { + policies = append(policies, &mapList.Items[i]) + } + + bindList := &admissionregistrationv1.MutatingAdmissionPolicyBindingList{} + if err := c.List(ctx, bindList, client.MatchingLabels{ManagedMAPLabel: ManagedMAPLabelValue}); err != nil { + return nil, nil, fmt.Errorf("listing MutatingAdmissionPolicyBindings: %w", err) + } + for i := range bindList.Items { + bindings = append(bindings, &bindList.Items[i]) + } + case VersionV1Beta1: + mapList := &admissionv1beta1.MutatingAdmissionPolicyList{} + if err := c.List(ctx, mapList, client.MatchingLabels{ManagedMAPLabel: ManagedMAPLabelValue}); err != nil { + return nil, nil, fmt.Errorf("listing MutatingAdmissionPolicies: %w", err) + } + for i := range mapList.Items { + policies = append(policies, &mapList.Items[i]) + } + + bindList := &admissionv1beta1.MutatingAdmissionPolicyBindingList{} + if err := c.List(ctx, bindList, client.MatchingLabels{ManagedMAPLabel: ManagedMAPLabelValue}); err != nil { + return nil, nil, fmt.Errorf("listing MutatingAdmissionPolicyBindings: %w", err) + } + for i := range bindList.Items { + bindings = append(bindings, &bindList.Items[i]) + } + default: + return nil, nil, fmt.Errorf("unsupported MutatingAdmissionPolicy API version %q", apiVersion) + } + return policies, bindings, nil +} + +// IsPolicyKind returns whether obj is a MutatingAdmissionPolicy (any served version). +func IsPolicyKind(obj client.Object) bool { + switch obj.(type) { + case *admissionregistrationv1.MutatingAdmissionPolicy, *admissionv1beta1.MutatingAdmissionPolicy: + return true + } + return false +} + +// IsBindingKind returns whether obj is a MutatingAdmissionPolicyBinding (any served version). +func IsBindingKind(obj client.Object) bool { + switch obj.(type) { + case *admissionregistrationv1.MutatingAdmissionPolicyBinding, *admissionv1beta1.MutatingAdmissionPolicyBinding: + return true } + return false } diff --git a/pkg/imports/admission/admission_test.go b/pkg/imports/admission/admission_test.go index 31cc893de7..d31362ca77 100644 --- a/pkg/imports/admission/admission_test.go +++ b/pkg/imports/admission/admission_test.go @@ -18,59 +18,145 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/restmapper" opv1 "github.com/tigera/operator/api/v1" ) -var _ = Describe("MutatingAdmissionPolicies", func() { - It("returns Calico MAPs when v3=true", func() { - objs := GetMutatingAdmissionPolicies(opv1.Calico, true) - Expect(objs).To(HaveLen(4), "Expected 4 admission objects, got %d", len(objs)) - - // Verify we get two MAPs and two MAPBs. - var mapCount, mapbCount int - for _, obj := range objs { - switch obj.(type) { - case *admissionv1beta1.MutatingAdmissionPolicy: - mapCount++ - case *admissionv1beta1.MutatingAdmissionPolicyBinding: - mapbCount++ +// fakeMapper builds a RESTMapper that only knows the given GroupVersionResources. +func fakeMapper(gvrs ...schema.GroupVersionResource) meta.RESTMapper { + apiGroupRes := []*restmapper.APIGroupResources{ + { + Group: metav1.APIGroup{ + Name: APIGroup, + }, + VersionedResources: map[string][]metav1.APIResource{}, + }, + } + for _, gvr := range gvrs { + kind := "MutatingAdmissionPolicy" + if gvr.Resource == "mutatingadmissionpolicybindings" { + kind = "MutatingAdmissionPolicyBinding" + } + apiGroupRes[0].VersionedResources[gvr.Version] = append( + apiGroupRes[0].VersionedResources[gvr.Version], + metav1.APIResource{Name: gvr.Resource, Namespaced: false, Kind: kind}, + ) + // Group versions need to be advertised too. + seen := false + for _, v := range apiGroupRes[0].Group.Versions { + if v.Version == gvr.Version { + seen = true + break } - // Verify the managed label is set. - Expect(obj.GetLabels()).To(HaveKeyWithValue(ManagedMAPLabel, ManagedMAPLabelValue), "Expected MAP object to have label %s=%s", ManagedMAPLabel, ManagedMAPLabelValue) } - Expect(mapCount).To(Equal(2), "Expected 2 MutatingAdmissionPolicy, got %d", mapCount) - Expect(mapbCount).To(Equal(2), "Expected 2 MutatingAdmissionPolicyBinding, got %d", mapbCount) + if !seen { + apiGroupRes[0].Group.Versions = append(apiGroupRes[0].Group.Versions, metav1.GroupVersionForDiscovery{ + GroupVersion: gvr.GroupVersion().String(), + Version: gvr.Version, + }) + } + } + return restmapper.NewDiscoveryRESTMapper(apiGroupRes) +} + +var _ = Describe("MutatingAdmissionPolicies", func() { + Describe("DiscoverAPIVersion", func() { + It("prefers v1 when both are served", func() { + mapper := fakeMapper( + schema.GroupVersionResource{Group: APIGroup, Version: "v1", Resource: "mutatingadmissionpolicies"}, + schema.GroupVersionResource{Group: APIGroup, Version: "v1beta1", Resource: "mutatingadmissionpolicies"}, + ) + Expect(DiscoverAPIVersion(mapper)).To(Equal(VersionV1)) + }) + + It("falls back to v1beta1", func() { + mapper := fakeMapper( + schema.GroupVersionResource{Group: APIGroup, Version: "v1beta1", Resource: "mutatingadmissionpolicies"}, + ) + Expect(DiscoverAPIVersion(mapper)).To(Equal(VersionV1Beta1)) + }) + + It("returns empty when nothing is served", func() { + Expect(DiscoverAPIVersion(fakeMapper())).To(BeEmpty()) + }) }) - It("returns Enterprise MAPs when v3=true", func() { - objs := GetMutatingAdmissionPolicies(opv1.CalicoEnterprise, true) - Expect(objs).To(HaveLen(4), "Expected 4 admission objects, got %d", len(objs)) - - var mapCount, mapbCount int - for _, obj := range objs { - switch obj.(type) { - case *admissionv1beta1.MutatingAdmissionPolicy: - mapCount++ - case *admissionv1beta1.MutatingAdmissionPolicyBinding: - mapbCount++ + Describe("GetMutatingAdmissionPolicies", func() { + It("returns Calico v1beta1 MAPs when v3=true", func() { + objs := GetMutatingAdmissionPolicies(opv1.Calico, true, VersionV1Beta1) + Expect(objs).To(HaveLen(4)) + + var mapCount, mapbCount int + for _, obj := range objs { + switch obj.(type) { + case *admissionv1beta1.MutatingAdmissionPolicy: + mapCount++ + case *admissionv1beta1.MutatingAdmissionPolicyBinding: + mapbCount++ + } + Expect(obj.GetLabels()).To(HaveKeyWithValue(ManagedMAPLabel, ManagedMAPLabelValue)) } - Expect(obj.GetLabels()).To(HaveKeyWithValue(ManagedMAPLabel, ManagedMAPLabelValue), "Expected MAP object to have label %s=%s", ManagedMAPLabel, ManagedMAPLabelValue) - } - Expect(mapCount).To(Equal(2), "Expected 2 MutatingAdmissionPolicy, got %d", mapCount) - Expect(mapbCount).To(Equal(2), "Expected 2 MutatingAdmissionPolicyBinding, got %d", mapbCount) - }) + Expect(mapCount).To(Equal(2)) + Expect(mapbCount).To(Equal(2)) + }) - It("returns empty when v3=false", func() { - Expect(GetMutatingAdmissionPolicies(opv1.Calico, false)).To(BeEmpty(), "Expected no admission objects when v3=false") - Expect(GetMutatingAdmissionPolicies(opv1.CalicoEnterprise, false)).To(BeEmpty(), "Expected no admission objects when v3=false") - }) + It("returns Calico v1 MAPs when discovered version is v1", func() { + objs := GetMutatingAdmissionPolicies(opv1.Calico, true, VersionV1) + Expect(objs).To(HaveLen(4)) - It("parses MAP names correctly", func() { - objs := GetMutatingAdmissionPolicies(opv1.Calico, true) - for _, obj := range objs { - Expect(obj.GetName()).ToNot(BeEmpty(), "Expected MAP object to have a name") - } + var mapCount, mapbCount int + for _, obj := range objs { + switch o := obj.(type) { + case *admissionregistrationv1.MutatingAdmissionPolicy: + mapCount++ + Expect(o.APIVersion).To(Equal(APIGroup + "/" + VersionV1)) + case *admissionregistrationv1.MutatingAdmissionPolicyBinding: + mapbCount++ + Expect(o.APIVersion).To(Equal(APIGroup + "/" + VersionV1)) + } + Expect(obj.GetLabels()).To(HaveKeyWithValue(ManagedMAPLabel, ManagedMAPLabelValue)) + } + Expect(mapCount).To(Equal(2)) + Expect(mapbCount).To(Equal(2)) + }) + + It("returns Enterprise MAPs at the chosen version", func() { + objs := GetMutatingAdmissionPolicies(opv1.CalicoEnterprise, true, VersionV1) + Expect(objs).To(HaveLen(4)) + + var mapCount, mapbCount int + for _, obj := range objs { + switch obj.(type) { + case *admissionregistrationv1.MutatingAdmissionPolicy: + mapCount++ + case *admissionregistrationv1.MutatingAdmissionPolicyBinding: + mapbCount++ + } + } + Expect(mapCount).To(Equal(2)) + Expect(mapbCount).To(Equal(2)) + }) + + It("returns empty when v3=false", func() { + Expect(GetMutatingAdmissionPolicies(opv1.Calico, false, VersionV1)).To(BeEmpty()) + Expect(GetMutatingAdmissionPolicies(opv1.CalicoEnterprise, false, VersionV1)).To(BeEmpty()) + }) + + It("returns empty when apiVersion is empty", func() { + Expect(GetMutatingAdmissionPolicies(opv1.Calico, true, "")).To(BeEmpty()) + }) + + It("parses MAP names correctly", func() { + objs := GetMutatingAdmissionPolicies(opv1.Calico, true, VersionV1) + for _, obj := range objs { + Expect(obj.GetName()).ToNot(BeEmpty()) + } + }) }) }) From 644c0b08143423727c6e248b1bf6dd7d6edd5b0b Mon Sep 17 00:00:00 2001 From: Casey Davenport Date: Wed, 20 May 2026 14:06:32 -0700 Subject: [PATCH 3/8] Detect MutatingAdmissionPolicy API version once at startup Avoid a per-reconcile RESTMapping call by discovering the served version in main() and threading it through ControllerOptions to the installation controller. --- cmd/main.go | 13 +- .../installation/core_controller.go | 14 +-- .../installation/core_controller_test.go | 111 ++++++++---------- pkg/controller/options/options.go | 6 + pkg/imports/admission/admission.go | 10 +- 5 files changed, 79 insertions(+), 75 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 7b1117b355..de3642eacd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -335,6 +335,15 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe os.Exit(1) } + // Detect which served version of MutatingAdmissionPolicy is available on the cluster (if any). + // We discover once at startup so reconcile loops don't repeatedly hit the discovery API. + mapAPIVersion := admission.DiscoverAPIVersion(mgr.GetRESTMapper()) + if mapAPIVersion != "" { + setupLog.WithValues("version", mapAPIVersion).Info("Detected MutatingAdmissionPolicy API version") + } else { + setupLog.Info("MutatingAdmissionPolicy API is not served by this cluster") + } + // If configured to manage CRDs, do a preliminary install of them here. The Installation controller // will reconcile them as well, but we need to make sure they are installed before we start the rest of the controllers. if bootstrapCRDs || manageCRDs { @@ -345,7 +354,7 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe os.Exit(1) } - if err := admission.Ensure(mgr.GetClient(), variant, v3CRDs, setupLog); err != nil { + if err := admission.Ensure(mgr.GetClient(), variant, v3CRDs, mapAPIVersion, setupLog); err != nil { setupLog.Error(err, "Failed to ensure MutatingAdmissionPolicies are created") os.Exit(1) } @@ -509,6 +518,8 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe MultiTenant: multiTenant, ElasticExternal: utils.UseExternalElastic(bootConfig), UseV3CRDs: v3CRDs, + + MutatingAdmissionPolicyAPIVersion: mapAPIVersion, } // Before we start any controllers, make sure our options are valid. diff --git a/pkg/controller/installation/core_controller.go b/pkg/controller/installation/core_controller.go index 7e62e7141a..01135b8dbb 100644 --- a/pkg/controller/installation/core_controller.go +++ b/pkg/controller/installation/core_controller.go @@ -344,6 +344,7 @@ func newReconciler(mgr manager.Manager, opts options.ControllerOptions) (*Reconc newComponentHandler: utils.NewComponentHandler, v3CRDs: opts.UseV3CRDs, kubernetesVersion: opts.KubernetesVersion, + mapAPIVersion: opts.MutatingAdmissionPolicyAPIVersion, } r.status.Run(opts.ShutdownContext) r.typhaAutoscaler.start(opts.ShutdownContext) @@ -402,6 +403,7 @@ type ReconcileInstallation struct { migrationWatchReady *utils.ReadyFlag v3CRDs bool kubernetesVersion *common.VersionInfo + mapAPIVersion string // newComponentHandler returns a new component handler. Useful stub for unit testing. newComponentHandler func(log logr.Logger, client client.Client, scheme *runtime.Scheme, cr metav1.Object) utils.ComponentHandler @@ -2269,16 +2271,14 @@ func (r *ReconcileInstallation) updateMutatingAdmissionPolicies(ctx context.Cont return nil } - // Discover which served version of MutatingAdmissionPolicy is available. v1 was promoted to GA - // in k8s 1.36 and v1beta1 (introduced in 1.32) is scheduled for removal in 1.37, so the cluster - // may serve either or both. - apiVersion := admission.DiscoverAPIVersion(r.client.RESTMapper()) - if apiVersion == "" { + // MutatingAdmissionPolicy served version was discovered once at startup (v1 was promoted to GA + // in k8s 1.36 and v1beta1 (introduced in 1.32) is scheduled for removal in 1.37). + if r.mapAPIVersion == "" { r.status.SetDegraded(operatorv1.ResourceNotReady, "Kubernetes cluster does not serve MutatingAdmissionPolicy (requires v1.32+); policy defaulting will not be available", nil, log) return nil } - desired := admission.GetMutatingAdmissionPolicies(install.Spec.Variant, r.v3CRDs, apiVersion) + desired := admission.GetMutatingAdmissionPolicies(install.Spec.Variant, r.v3CRDs, r.mapAPIVersion) // Build sets of desired resource names for comparison. desiredMAPs := map[string]bool{} @@ -2293,7 +2293,7 @@ func (r *ReconcileInstallation) updateMutatingAdmissionPolicies(ctx context.Cont } // Find stale managed resources at the discovered API version. - existingMAPs, existingMAPBs, err := admission.ListManaged(ctx, r.client, apiVersion) + existingMAPs, existingMAPBs, err := admission.ListManaged(ctx, r.client, r.mapAPIVersion) if err != nil { r.status.SetDegraded(operatorv1.ResourceReadError, "Error listing managed MutatingAdmissionPolicy resources", err, log) return err diff --git a/pkg/controller/installation/core_controller_test.go b/pkg/controller/installation/core_controller_test.go index fb4eecf02b..f3d10365a9 100644 --- a/pkg/controller/installation/core_controller_test.go +++ b/pkg/controller/installation/core_controller_test.go @@ -35,10 +35,8 @@ import ( rbacv1 "k8s.io/api/rbac/v1" schedv1 "k8s.io/api/scheduling/v1" storagev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" kfake "k8s.io/client-go/kubernetes/fake" @@ -2572,25 +2570,8 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { installation *operator.Installation ) - // mapMapper builds a RESTMapper advertising the requested served versions of - // admissionregistration.k8s.io MutatingAdmissionPolicy / MutatingAdmissionPolicyBinding. - mapMapper := func(versions ...string) meta.RESTMapper { - gv := []schema.GroupVersion{} - mapper := meta.NewDefaultRESTMapper(gv) - for _, v := range versions { - gvk := schema.GroupVersionKind{Group: admission.APIGroup, Version: v, Kind: "MutatingAdmissionPolicy"} - gvkB := schema.GroupVersionKind{Group: admission.APIGroup, Version: v, Kind: "MutatingAdmissionPolicyBinding"} - mapper.Add(gvk, meta.RESTScopeRoot) - mapper.Add(gvkB, meta.RESTScopeRoot) - } - return mapper - } - - clientFor := func(mapper meta.RESTMapper, initial ...client.Object) client.Client { - return ctrlrfake.DefaultFakeClientBuilder(scheme). - WithRESTMapper(mapper). - WithObjects(initial...). - Build() + clientFor := func(initial ...client.Object) client.Client { + return ctrlrfake.DefaultFakeClientBuilder(scheme).WithObjects(initial...).Build() } BeforeEach(func() { @@ -2621,11 +2602,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should create v1 MAPs when v1 is served", func() { r = ReconcileInstallation{ - client: clientFor(mapMapper(admission.VersionV1)), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + mapAPIVersion: admission.VersionV1, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2651,11 +2633,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should create v1beta1 MAPs when only v1beta1 is served", func() { r = ReconcileInstallation{ - client: clientFor(mapMapper(admission.VersionV1Beta1)), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + mapAPIVersion: admission.VersionV1Beta1, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2679,11 +2662,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should not create MAPs when no served version exists and should set degraded", func() { r = ReconcileInstallation{ - client: clientFor(mapMapper()), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + mapAPIVersion: "", newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2696,11 +2680,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should not create MAPs when v3CRDs=false", func() { r = ReconcileInstallation{ - client: clientFor(mapMapper(admission.VersionV1)), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: false, + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: false, + mapAPIVersion: admission.VersionV1, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2712,11 +2697,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should not create MAPs when manageCRDs=false", func() { r = ReconcileInstallation{ - client: clientFor(mapMapper(admission.VersionV1)), - scheme: scheme, - status: mockStatus, - manageCRDs: false, - v3CRDs: true, + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: false, + v3CRDs: true, + mapAPIVersion: admission.VersionV1, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2741,11 +2727,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { } r = ReconcileInstallation{ - client: clientFor(mapMapper(admission.VersionV1), staleMAP, staleMAPB), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, + client: clientFor(staleMAP, staleMAPB), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + mapAPIVersion: admission.VersionV1, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2782,11 +2769,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { } r = ReconcileInstallation{ - client: clientFor(mapMapper(admission.VersionV1), initial...), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, + client: clientFor(initial...), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + mapAPIVersion: admission.VersionV1, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2799,11 +2787,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should work with Enterprise variant", func() { r = ReconcileInstallation{ - client: clientFor(mapMapper(admission.VersionV1)), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + mapAPIVersion: admission.VersionV1, newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, diff --git a/pkg/controller/options/options.go b/pkg/controller/options/options.go index e7e10e1112..c4805c85d0 100644 --- a/pkg/controller/options/options.go +++ b/pkg/controller/options/options.go @@ -49,4 +49,10 @@ type ControllerOptions struct { // Whether or not to use crd.projectcalico.org/v1 or projectcalico.org/v3 for Calico CRDs. UseV3CRDs bool + + // MutatingAdmissionPolicyAPIVersion is the served API version of + // admissionregistration.k8s.io MutatingAdmissionPolicy (e.g. "v1", "v1beta1") detected once at + // startup via the RESTMapper, or empty if neither is served. Discovered up front to avoid + // repeated discovery on every reconcile. + MutatingAdmissionPolicyAPIVersion string } diff --git a/pkg/imports/admission/admission.go b/pkg/imports/admission/admission.go index 5bb9a46368..a6b8ee4162 100644 --- a/pkg/imports/admission/admission.go +++ b/pkg/imports/admission/admission.go @@ -130,20 +130,18 @@ func GetMutatingAdmissionPolicies(variant opv1.ProductVariant, v3 bool, apiVersi } // Ensure ensures that MutatingAdmissionPolicies necessary for bootstrapping exist in the cluster. -// Further reconciliation is handled by the core controller. If the API is not available (no served -// version of MutatingAdmissionPolicy), a warning is logged and the function returns nil. MAPs are -// only installed when v3 CRDs are enabled. -func Ensure(c client.Client, variant string, v3 bool, log logr.Logger) error { +// Further reconciliation is handled by the core controller. If apiVersion is empty (no served +// version of MutatingAdmissionPolicy on the cluster), a warning is logged and the function returns +// nil. MAPs are only installed when v3 CRDs are enabled. +func Ensure(c client.Client, variant string, v3 bool, apiVersion string, log logr.Logger) error { if !v3 { return nil } - apiVersion := DiscoverAPIVersion(c.RESTMapper()) if apiVersion == "" { log.Info("MutatingAdmissionPolicy API not available on cluster, skipping bootstrap") return nil } - log.Info("Using discovered MutatingAdmissionPolicy API version", "version", apiVersion) objs := GetMutatingAdmissionPolicies(opv1.ProductVariant(variant), v3, apiVersion) From 79b3f4443eb0294b994f1115c5fcca24e5baec5f Mon Sep 17 00:00:00 2001 From: Casey Davenport Date: Wed, 20 May 2026 14:22:00 -0700 Subject: [PATCH 4/8] Add pkg/common/apidiscovery for served-version lookups Generalize one-shot API discovery into a small package that controllers can query without hitting the cluster. cmd/main.go pre-resolves the set of GroupKinds we care about and passes a Discovery snapshot through ControllerOptions; lookups are plain map reads. --- cmd/main.go | 18 +-- pkg/common/apidiscovery/discovery.go | 67 +++++++++++ pkg/common/apidiscovery/discovery_test.go | 93 +++++++++++++++ .../installation/core_controller.go | 12 +- .../installation/core_controller_test.go | 106 ++++++++++-------- pkg/controller/options/options.go | 10 +- pkg/imports/admission/admission.go | 24 ++-- pkg/imports/admission/admission_test.go | 62 ---------- 8 files changed, 250 insertions(+), 142 deletions(-) create mode 100644 pkg/common/apidiscovery/discovery.go create mode 100644 pkg/common/apidiscovery/discovery_test.go diff --git a/cmd/main.go b/cmd/main.go index de3642eacd..0239cdff51 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,6 +35,7 @@ import ( "github.com/tigera/operator/pkg/apis" "github.com/tigera/operator/pkg/awssgsetup" "github.com/tigera/operator/pkg/common" + "github.com/tigera/operator/pkg/common/apidiscovery" "github.com/tigera/operator/pkg/components" "github.com/tigera/operator/pkg/controller/metrics" "github.com/tigera/operator/pkg/controller/migration/datastoremigration" @@ -56,6 +57,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -335,11 +337,13 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe os.Exit(1) } - // Detect which served version of MutatingAdmissionPolicy is available on the cluster (if any). - // We discover once at startup so reconcile loops don't repeatedly hit the discovery API. - mapAPIVersion := admission.DiscoverAPIVersion(mgr.GetRESTMapper()) - if mapAPIVersion != "" { - setupLog.WithValues("version", mapAPIVersion).Info("Detected MutatingAdmissionPolicy API version") + // Snapshot the served versions for the Kubernetes APIs the operator branches on. Discovery + // happens once here so reconcile loops can do plain map lookups. + apiDiscovery := apidiscovery.New(mgr.GetRESTMapper(), []schema.GroupKind{ + admission.PolicyGroupKind, + }) + if v := apiDiscovery.ServedVersion(admission.APIGroup, admission.KindPolicy); v != "" { + setupLog.WithValues("version", v).Info("Detected MutatingAdmissionPolicy API version") } else { setupLog.Info("MutatingAdmissionPolicy API is not served by this cluster") } @@ -354,7 +358,7 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe os.Exit(1) } - if err := admission.Ensure(mgr.GetClient(), variant, v3CRDs, mapAPIVersion, setupLog); err != nil { + if err := admission.Ensure(mgr.GetClient(), variant, v3CRDs, apiDiscovery.ServedVersion(admission.APIGroup, admission.KindPolicy), setupLog); err != nil { setupLog.Error(err, "Failed to ensure MutatingAdmissionPolicies are created") os.Exit(1) } @@ -519,7 +523,7 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe ElasticExternal: utils.UseExternalElastic(bootConfig), UseV3CRDs: v3CRDs, - MutatingAdmissionPolicyAPIVersion: mapAPIVersion, + APIDiscovery: apiDiscovery, } // Before we start any controllers, make sure our options are valid. diff --git a/pkg/common/apidiscovery/discovery.go b/pkg/common/apidiscovery/discovery.go new file mode 100644 index 0000000000..0417e14aeb --- /dev/null +++ b/pkg/common/apidiscovery/discovery.go @@ -0,0 +1,67 @@ +// Copyright (c) 2026 Tigera, Inc. All rights reserved. + +// 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 apidiscovery exposes the served versions of API kinds that the operator cares about. +// A snapshot is taken once at startup so controllers can ask which version of an API is available +// (and use that to choose between typed Go imports) without issuing further discovery requests at +// reconcile time. +package apidiscovery + +import ( + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Discovery is a snapshot of which API versions a cluster serves for a pre-registered set of +// GroupKinds. Lookups are pure map reads; no API calls are made after construction. +type Discovery struct { + versions map[schema.GroupKind]string +} + +// New consults the supplied RESTMapper for each tracked GroupKind and records the preferred +// served version. GroupKinds not served by the cluster (or unknown to the RESTMapper) map to the +// empty string. Callers should pass every GroupKind that any controller will later look up; only +// pre-registered kinds will return a non-empty version, so this list is the single place to add a +// new API the operator wants to discover. +func New(mapper meta.RESTMapper, tracked []schema.GroupKind) *Discovery { + d := &Discovery{versions: map[schema.GroupKind]string{}} + for _, gk := range tracked { + // RESTMappings returns mappings sorted by the group's preferred-version order from + // discovery — for a GA API like v1+v1beta1, v1 comes first. + mappings, err := mapper.RESTMappings(gk) + if err != nil || len(mappings) == 0 { + continue + } + d.versions[gk] = mappings[0].GroupVersionKind.Version + } + return d +} + +// ServedVersion returns the preferred served version for the given GroupKind, or "" if the kind +// is not served by the cluster (or was not pre-registered in the tracked list passed to New). +func (d *Discovery) ServedVersion(group, kind string) string { + if d == nil { + return "" + } + return d.versions[schema.GroupKind{Group: group, Kind: kind}] +} + +// NewStatic constructs a Discovery from an explicit version map. Intended for tests. +func NewStatic(versions map[schema.GroupKind]string) *Discovery { + cp := make(map[schema.GroupKind]string, len(versions)) + for k, v := range versions { + cp[k] = v + } + return &Discovery{versions: cp} +} diff --git a/pkg/common/apidiscovery/discovery_test.go b/pkg/common/apidiscovery/discovery_test.go new file mode 100644 index 0000000000..13c7ce7c3b --- /dev/null +++ b/pkg/common/apidiscovery/discovery_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2026 Tigera, Inc. All rights reserved. + +// 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 apidiscovery + +import ( + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// fakeMapper returns the registered versions for the given GroupKind. Other RESTMapper methods +// are not implemented because Discovery only uses RESTMappings. +type fakeMapper struct { + meta.RESTMapper + served map[schema.GroupKind][]string + calls int +} + +func (f *fakeMapper) RESTMappings(gk schema.GroupKind, _ ...string) ([]*meta.RESTMapping, error) { + f.calls++ + versions, ok := f.served[gk] + if !ok { + return nil, &meta.NoKindMatchError{GroupKind: gk} + } + out := make([]*meta.RESTMapping, 0, len(versions)) + for _, v := range versions { + out = append(out, &meta.RESTMapping{GroupVersionKind: gk.WithVersion(v)}) + } + return out, nil +} + +func TestDiscoveryRecordsPreferredVersion(t *testing.T) { + mapper := &fakeMapper{served: map[schema.GroupKind][]string{ + {Group: "admissionregistration.k8s.io", Kind: "MutatingAdmissionPolicy"}: {"v1", "v1beta1"}, + {Group: "certificates.k8s.io", Kind: "CertificateSigningRequest"}: {"v1"}, + }} + tracked := []schema.GroupKind{ + {Group: "admissionregistration.k8s.io", Kind: "MutatingAdmissionPolicy"}, + {Group: "certificates.k8s.io", Kind: "CertificateSigningRequest"}, + {Group: "made.up.example.com", Kind: "Nope"}, + } + + d := New(mapper, tracked) + + if got := d.ServedVersion("admissionregistration.k8s.io", "MutatingAdmissionPolicy"); got != "v1" { + t.Errorf("MutatingAdmissionPolicy: got %q, want v1", got) + } + if got := d.ServedVersion("certificates.k8s.io", "CertificateSigningRequest"); got != "v1" { + t.Errorf("CertificateSigningRequest: got %q, want v1", got) + } + if got := d.ServedVersion("made.up.example.com", "Nope"); got != "" { + t.Errorf("Nope: got %q, want empty", got) + } + if got := d.ServedVersion("never.registered.com", "Other"); got != "" { + t.Errorf("untracked GroupKind: got %q, want empty", got) + } +} + +func TestDiscoveryNoCallsAfterConstruction(t *testing.T) { + mapper := &fakeMapper{served: map[schema.GroupKind][]string{ + {Group: "g", Kind: "K"}: {"v1"}, + }} + d := New(mapper, []schema.GroupKind{{Group: "g", Kind: "K"}}) + calls := mapper.calls + + for i := 0; i < 100; i++ { + _ = d.ServedVersion("g", "K") + _ = d.ServedVersion("nope", "Other") + } + if mapper.calls != calls { + t.Errorf("expected zero additional mapper calls after construction, got %d (was %d)", mapper.calls, calls) + } +} + +func TestNilDiscoverySafe(t *testing.T) { + var d *Discovery + if got := d.ServedVersion("g", "K"); got != "" { + t.Errorf("nil Discovery: got %q, want empty", got) + } +} diff --git a/pkg/controller/installation/core_controller.go b/pkg/controller/installation/core_controller.go index 01135b8dbb..d1b7a7d0e8 100644 --- a/pkg/controller/installation/core_controller.go +++ b/pkg/controller/installation/core_controller.go @@ -61,6 +61,7 @@ import ( operatorv1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/active" "github.com/tigera/operator/pkg/common" + "github.com/tigera/operator/pkg/common/apidiscovery" "github.com/tigera/operator/pkg/components" "github.com/tigera/operator/pkg/controller/certificatemanager" "github.com/tigera/operator/pkg/controller/ippool" @@ -344,7 +345,7 @@ func newReconciler(mgr manager.Manager, opts options.ControllerOptions) (*Reconc newComponentHandler: utils.NewComponentHandler, v3CRDs: opts.UseV3CRDs, kubernetesVersion: opts.KubernetesVersion, - mapAPIVersion: opts.MutatingAdmissionPolicyAPIVersion, + apiDiscovery: opts.APIDiscovery, } r.status.Run(opts.ShutdownContext) r.typhaAutoscaler.start(opts.ShutdownContext) @@ -403,7 +404,7 @@ type ReconcileInstallation struct { migrationWatchReady *utils.ReadyFlag v3CRDs bool kubernetesVersion *common.VersionInfo - mapAPIVersion string + apiDiscovery *apidiscovery.Discovery // newComponentHandler returns a new component handler. Useful stub for unit testing. newComponentHandler func(log logr.Logger, client client.Client, scheme *runtime.Scheme, cr metav1.Object) utils.ComponentHandler @@ -2273,12 +2274,13 @@ func (r *ReconcileInstallation) updateMutatingAdmissionPolicies(ctx context.Cont // MutatingAdmissionPolicy served version was discovered once at startup (v1 was promoted to GA // in k8s 1.36 and v1beta1 (introduced in 1.32) is scheduled for removal in 1.37). - if r.mapAPIVersion == "" { + mapAPIVersion := r.apiDiscovery.ServedVersion(admission.APIGroup, admission.KindPolicy) + if mapAPIVersion == "" { r.status.SetDegraded(operatorv1.ResourceNotReady, "Kubernetes cluster does not serve MutatingAdmissionPolicy (requires v1.32+); policy defaulting will not be available", nil, log) return nil } - desired := admission.GetMutatingAdmissionPolicies(install.Spec.Variant, r.v3CRDs, r.mapAPIVersion) + desired := admission.GetMutatingAdmissionPolicies(install.Spec.Variant, r.v3CRDs, mapAPIVersion) // Build sets of desired resource names for comparison. desiredMAPs := map[string]bool{} @@ -2293,7 +2295,7 @@ func (r *ReconcileInstallation) updateMutatingAdmissionPolicies(ctx context.Cont } // Find stale managed resources at the discovered API version. - existingMAPs, existingMAPBs, err := admission.ListManaged(ctx, r.client, r.mapAPIVersion) + existingMAPs, existingMAPBs, err := admission.ListManaged(ctx, r.client, mapAPIVersion) if err != nil { r.status.SetDegraded(operatorv1.ResourceReadError, "Error listing managed MutatingAdmissionPolicy resources", err, log) return err diff --git a/pkg/controller/installation/core_controller_test.go b/pkg/controller/installation/core_controller_test.go index f3d10365a9..6465153b52 100644 --- a/pkg/controller/installation/core_controller_test.go +++ b/pkg/controller/installation/core_controller_test.go @@ -37,6 +37,7 @@ import ( storagev1 "k8s.io/api/storage/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" kfake "k8s.io/client-go/kubernetes/fake" @@ -51,6 +52,7 @@ import ( operator "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/apis" "github.com/tigera/operator/pkg/common" + "github.com/tigera/operator/pkg/common/apidiscovery" "github.com/tigera/operator/pkg/components" "github.com/tigera/operator/pkg/controller/certificatemanager" "github.com/tigera/operator/pkg/controller/status" @@ -2574,6 +2576,14 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { return ctrlrfake.DefaultFakeClientBuilder(scheme).WithObjects(initial...).Build() } + discoveryFor := func(mapVersion string) *apidiscovery.Discovery { + m := map[schema.GroupKind]string{} + if mapVersion != "" { + m[admission.PolicyGroupKind] = mapVersion + } + return apidiscovery.NewStatic(m) + } + BeforeEach(func() { log = logr.Discard() ctx, cancel = context.WithCancel(context.Background()) @@ -2602,12 +2612,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should create v1 MAPs when v1 is served", func() { r = ReconcileInstallation{ - client: clientFor(), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - mapAPIVersion: admission.VersionV1, + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + apiDiscovery: discoveryFor(admission.VersionV1), newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2633,12 +2643,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should create v1beta1 MAPs when only v1beta1 is served", func() { r = ReconcileInstallation{ - client: clientFor(), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - mapAPIVersion: admission.VersionV1Beta1, + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + apiDiscovery: discoveryFor(admission.VersionV1Beta1), newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2662,12 +2672,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should not create MAPs when no served version exists and should set degraded", func() { r = ReconcileInstallation{ - client: clientFor(), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - mapAPIVersion: "", + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + apiDiscovery: discoveryFor(""), newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2680,12 +2690,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should not create MAPs when v3CRDs=false", func() { r = ReconcileInstallation{ - client: clientFor(), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: false, - mapAPIVersion: admission.VersionV1, + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: false, + apiDiscovery: discoveryFor(admission.VersionV1), newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2697,12 +2707,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should not create MAPs when manageCRDs=false", func() { r = ReconcileInstallation{ - client: clientFor(), - scheme: scheme, - status: mockStatus, - manageCRDs: false, - v3CRDs: true, - mapAPIVersion: admission.VersionV1, + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: false, + v3CRDs: true, + apiDiscovery: discoveryFor(admission.VersionV1), newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2727,12 +2737,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { } r = ReconcileInstallation{ - client: clientFor(staleMAP, staleMAPB), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - mapAPIVersion: admission.VersionV1, + client: clientFor(staleMAP, staleMAPB), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + apiDiscovery: discoveryFor(admission.VersionV1), newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2769,12 +2779,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { } r = ReconcileInstallation{ - client: clientFor(initial...), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - mapAPIVersion: admission.VersionV1, + client: clientFor(initial...), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + apiDiscovery: discoveryFor(admission.VersionV1), newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, @@ -2787,12 +2797,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { It("should work with Enterprise variant", func() { r = ReconcileInstallation{ - client: clientFor(), - scheme: scheme, - status: mockStatus, - manageCRDs: true, - v3CRDs: true, - mapAPIVersion: admission.VersionV1, + client: clientFor(), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + apiDiscovery: discoveryFor(admission.VersionV1), newComponentHandler: func(logr.Logger, client.Client, *runtime.Scheme, metav1.Object) utils.ComponentHandler { return componentHandler }, diff --git a/pkg/controller/options/options.go b/pkg/controller/options/options.go index c4805c85d0..26b0aa0c80 100644 --- a/pkg/controller/options/options.go +++ b/pkg/controller/options/options.go @@ -19,6 +19,7 @@ import ( v1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/common" + "github.com/tigera/operator/pkg/common/apidiscovery" "k8s.io/client-go/kubernetes" ) @@ -50,9 +51,8 @@ type ControllerOptions struct { // Whether or not to use crd.projectcalico.org/v1 or projectcalico.org/v3 for Calico CRDs. UseV3CRDs bool - // MutatingAdmissionPolicyAPIVersion is the served API version of - // admissionregistration.k8s.io MutatingAdmissionPolicy (e.g. "v1", "v1beta1") detected once at - // startup via the RESTMapper, or empty if neither is served. Discovered up front to avoid - // repeated discovery on every reconcile. - MutatingAdmissionPolicyAPIVersion string + // APIDiscovery is a snapshot of which Kubernetes API versions the cluster serves for the kinds + // the operator cares about. Populated once at startup so controllers can branch on API + // availability without issuing further discovery requests at reconcile time. + APIDiscovery *apidiscovery.Discovery } diff --git a/pkg/imports/admission/admission.go b/pkg/imports/admission/admission.go index a6b8ee4162..52369050ec 100644 --- a/pkg/imports/admission/admission.go +++ b/pkg/imports/admission/admission.go @@ -26,7 +26,6 @@ import ( admissionregistrationv1 "k8s.io/api/admissionregistration/v1" admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" @@ -48,8 +47,17 @@ const ( VersionV1 = "v1" // VersionV1Beta1 is the beta API version (k8s 1.32-1.36). VersionV1Beta1 = "v1beta1" + + // KindPolicy is the MutatingAdmissionPolicy kind. + KindPolicy = "MutatingAdmissionPolicy" + // KindBinding is the MutatingAdmissionPolicyBinding kind. + KindBinding = "MutatingAdmissionPolicyBinding" ) +// PolicyGroupKind is the GroupKind for MutatingAdmissionPolicy. Exposed so the API discovery +// registry in cmd/main.go can pre-resolve its served version at startup. +var PolicyGroupKind = schema.GroupKind{Group: APIGroup, Kind: KindPolicy} + var ( //go:embed calico calicoAdmissionFiles embed.FS @@ -57,20 +65,6 @@ var ( enterpriseAdmissionFiles embed.FS ) -// DiscoverAPIVersion returns which served version of MutatingAdmissionPolicy is available on the -// cluster: "v1" (preferred), "v1beta1", or "" if neither is served. The K8s version varies which -// version is registered: v1beta1 was introduced in 1.32 and v1 graduated in 1.36, with v1beta1 -// scheduled for removal in 1.37. -func DiscoverAPIVersion(mapper meta.RESTMapper) string { - gk := schema.GroupKind{Group: APIGroup, Kind: "MutatingAdmissionPolicy"} - for _, v := range []string{VersionV1, VersionV1Beta1} { - if _, err := mapper.RESTMapping(gk, v); err == nil { - return v - } - } - return "" -} - // GetMutatingAdmissionPolicies returns MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding // objects for the given variant, typed at the requested API version. These are only applicable // when v3 CRDs are enabled. diff --git a/pkg/imports/admission/admission_test.go b/pkg/imports/admission/admission_test.go index d31362ca77..2a747b4e26 100644 --- a/pkg/imports/admission/admission_test.go +++ b/pkg/imports/admission/admission_test.go @@ -20,73 +20,11 @@ import ( admissionregistrationv1 "k8s.io/api/admissionregistration/v1" admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/restmapper" opv1 "github.com/tigera/operator/api/v1" ) -// fakeMapper builds a RESTMapper that only knows the given GroupVersionResources. -func fakeMapper(gvrs ...schema.GroupVersionResource) meta.RESTMapper { - apiGroupRes := []*restmapper.APIGroupResources{ - { - Group: metav1.APIGroup{ - Name: APIGroup, - }, - VersionedResources: map[string][]metav1.APIResource{}, - }, - } - for _, gvr := range gvrs { - kind := "MutatingAdmissionPolicy" - if gvr.Resource == "mutatingadmissionpolicybindings" { - kind = "MutatingAdmissionPolicyBinding" - } - apiGroupRes[0].VersionedResources[gvr.Version] = append( - apiGroupRes[0].VersionedResources[gvr.Version], - metav1.APIResource{Name: gvr.Resource, Namespaced: false, Kind: kind}, - ) - // Group versions need to be advertised too. - seen := false - for _, v := range apiGroupRes[0].Group.Versions { - if v.Version == gvr.Version { - seen = true - break - } - } - if !seen { - apiGroupRes[0].Group.Versions = append(apiGroupRes[0].Group.Versions, metav1.GroupVersionForDiscovery{ - GroupVersion: gvr.GroupVersion().String(), - Version: gvr.Version, - }) - } - } - return restmapper.NewDiscoveryRESTMapper(apiGroupRes) -} - var _ = Describe("MutatingAdmissionPolicies", func() { - Describe("DiscoverAPIVersion", func() { - It("prefers v1 when both are served", func() { - mapper := fakeMapper( - schema.GroupVersionResource{Group: APIGroup, Version: "v1", Resource: "mutatingadmissionpolicies"}, - schema.GroupVersionResource{Group: APIGroup, Version: "v1beta1", Resource: "mutatingadmissionpolicies"}, - ) - Expect(DiscoverAPIVersion(mapper)).To(Equal(VersionV1)) - }) - - It("falls back to v1beta1", func() { - mapper := fakeMapper( - schema.GroupVersionResource{Group: APIGroup, Version: "v1beta1", Resource: "mutatingadmissionpolicies"}, - ) - Expect(DiscoverAPIVersion(mapper)).To(Equal(VersionV1Beta1)) - }) - - It("returns empty when nothing is served", func() { - Expect(DiscoverAPIVersion(fakeMapper())).To(BeEmpty()) - }) - }) - Describe("GetMutatingAdmissionPolicies", func() { It("returns Calico v1beta1 MAPs when v3=true", func() { objs := GetMutatingAdmissionPolicies(opv1.Calico, true, VersionV1Beta1) From 85af4b000ea7d9933821d8d5b78e5fdf7a1dc450 Mon Sep 17 00:00:00 2001 From: Casey Davenport Date: Wed, 20 May 2026 14:30:28 -0700 Subject: [PATCH 5/8] Move tracked GroupKinds inside apidiscovery.New, drop main.go logging --- cmd/main.go | 10 +---- pkg/common/apidiscovery/discovery.go | 19 ++++++--- pkg/common/apidiscovery/discovery_test.go | 52 +++++++++++++---------- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 0239cdff51..67307ba2ab 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -57,7 +57,6 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -339,14 +338,7 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe // Snapshot the served versions for the Kubernetes APIs the operator branches on. Discovery // happens once here so reconcile loops can do plain map lookups. - apiDiscovery := apidiscovery.New(mgr.GetRESTMapper(), []schema.GroupKind{ - admission.PolicyGroupKind, - }) - if v := apiDiscovery.ServedVersion(admission.APIGroup, admission.KindPolicy); v != "" { - setupLog.WithValues("version", v).Info("Detected MutatingAdmissionPolicy API version") - } else { - setupLog.Info("MutatingAdmissionPolicy API is not served by this cluster") - } + apiDiscovery := apidiscovery.New(mgr.GetRESTMapper()) // If configured to manage CRDs, do a preliminary install of them here. The Installation controller // will reconcile them as well, but we need to make sure they are installed before we start the rest of the controllers. diff --git a/pkg/common/apidiscovery/discovery.go b/pkg/common/apidiscovery/discovery.go index 0417e14aeb..1ab578cee4 100644 --- a/pkg/common/apidiscovery/discovery.go +++ b/pkg/common/apidiscovery/discovery.go @@ -23,20 +23,25 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) -// Discovery is a snapshot of which API versions a cluster serves for a pre-registered set of -// GroupKinds. Lookups are pure map reads; no API calls are made after construction. +// trackedGroupKinds enumerates the Kubernetes API kinds the operator wants to know served +// versions for. Add an entry here when a controller needs to branch on whether (or which version +// of) an API is available. +var trackedGroupKinds = []schema.GroupKind{ + {Group: "admissionregistration.k8s.io", Kind: "MutatingAdmissionPolicy"}, +} + +// Discovery is a snapshot of which API versions a cluster serves for the set of GroupKinds the +// operator cares about. Lookups are pure map reads; no API calls are made after construction. type Discovery struct { versions map[schema.GroupKind]string } // New consults the supplied RESTMapper for each tracked GroupKind and records the preferred // served version. GroupKinds not served by the cluster (or unknown to the RESTMapper) map to the -// empty string. Callers should pass every GroupKind that any controller will later look up; only -// pre-registered kinds will return a non-empty version, so this list is the single place to add a -// new API the operator wants to discover. -func New(mapper meta.RESTMapper, tracked []schema.GroupKind) *Discovery { +// empty string. Only GroupKinds listed in trackedGroupKinds will return a non-empty version. +func New(mapper meta.RESTMapper) *Discovery { d := &Discovery{versions: map[schema.GroupKind]string{}} - for _, gk := range tracked { + for _, gk := range trackedGroupKinds { // RESTMappings returns mappings sorted by the group's preferred-version order from // discovery — for a GA API like v1+v1beta1, v1 comes first. mappings, err := mapper.RESTMappings(gk) diff --git a/pkg/common/apidiscovery/discovery_test.go b/pkg/common/apidiscovery/discovery_test.go index 13c7ce7c3b..c45eec72a0 100644 --- a/pkg/common/apidiscovery/discovery_test.go +++ b/pkg/common/apidiscovery/discovery_test.go @@ -43,41 +43,49 @@ func (f *fakeMapper) RESTMappings(gk schema.GroupKind, _ ...string) ([]*meta.RES } func TestDiscoveryRecordsPreferredVersion(t *testing.T) { - mapper := &fakeMapper{served: map[schema.GroupKind][]string{ - {Group: "admissionregistration.k8s.io", Kind: "MutatingAdmissionPolicy"}: {"v1", "v1beta1"}, - {Group: "certificates.k8s.io", Kind: "CertificateSigningRequest"}: {"v1"}, - }} - tracked := []schema.GroupKind{ - {Group: "admissionregistration.k8s.io", Kind: "MutatingAdmissionPolicy"}, - {Group: "certificates.k8s.io", Kind: "CertificateSigningRequest"}, - {Group: "made.up.example.com", Kind: "Nope"}, + // Serve every tracked GroupKind at v1 (preferred over v1beta1 where applicable). + served := map[schema.GroupKind][]string{} + for _, gk := range trackedGroupKinds { + served[gk] = []string{"v1", "v1beta1"} } + mapper := &fakeMapper{served: served} - d := New(mapper, tracked) + d := New(mapper) - if got := d.ServedVersion("admissionregistration.k8s.io", "MutatingAdmissionPolicy"); got != "v1" { - t.Errorf("MutatingAdmissionPolicy: got %q, want v1", got) - } - if got := d.ServedVersion("certificates.k8s.io", "CertificateSigningRequest"); got != "v1" { - t.Errorf("CertificateSigningRequest: got %q, want v1", got) - } - if got := d.ServedVersion("made.up.example.com", "Nope"); got != "" { - t.Errorf("Nope: got %q, want empty", got) + for _, gk := range trackedGroupKinds { + if got := d.ServedVersion(gk.Group, gk.Kind); got != "v1" { + t.Errorf("%s: got %q, want v1", gk, got) + } } if got := d.ServedVersion("never.registered.com", "Other"); got != "" { t.Errorf("untracked GroupKind: got %q, want empty", got) } } +func TestDiscoveryUnservedGroupKind(t *testing.T) { + // Mapper returns NoKindMatchError for everything. + mapper := &fakeMapper{served: map[schema.GroupKind][]string{}} + d := New(mapper) + for _, gk := range trackedGroupKinds { + if got := d.ServedVersion(gk.Group, gk.Kind); got != "" { + t.Errorf("%s served unexpectedly: %q", gk, got) + } + } +} + func TestDiscoveryNoCallsAfterConstruction(t *testing.T) { - mapper := &fakeMapper{served: map[schema.GroupKind][]string{ - {Group: "g", Kind: "K"}: {"v1"}, - }} - d := New(mapper, []schema.GroupKind{{Group: "g", Kind: "K"}}) + served := map[schema.GroupKind][]string{} + for _, gk := range trackedGroupKinds { + served[gk] = []string{"v1"} + } + mapper := &fakeMapper{served: served} + d := New(mapper) calls := mapper.calls for i := 0; i < 100; i++ { - _ = d.ServedVersion("g", "K") + for _, gk := range trackedGroupKinds { + _ = d.ServedVersion(gk.Group, gk.Kind) + } _ = d.ServedVersion("nope", "Other") } if mapper.calls != calls { From 5d2b43605885a0eb723ce468840b9227776a910b Mon Sep 17 00:00:00 2001 From: Casey Davenport Date: Wed, 20 May 2026 14:49:30 -0700 Subject: [PATCH 6/8] Consolidate discovery helpers under pkg/common/discovery Fold the new APIDiscovery type into the existing discovery file rather than adding a new package next to it. Cluster-shape helpers move from pkg/controller/utils into pkg/common/discovery alongside the served-API snapshot. --- cmd/main.go | 12 +++---- .../discovery.go => discovery/api.go} | 30 +++++++--------- .../api_test.go} | 36 +++++++++---------- .../utils => common/discovery}/discovery.go | 4 ++- .../discovery}/discovery_test.go | 2 +- .../installation/core_controller.go | 6 ++-- .../installation/core_controller_test.go | 6 ++-- pkg/controller/options/options.go | 4 +-- pkg/controller/utils/utils.go | 3 ++ 9 files changed, 51 insertions(+), 52 deletions(-) rename pkg/common/{apidiscovery/discovery.go => discovery/api.go} (60%) rename pkg/common/{apidiscovery/discovery_test.go => discovery/api_test.go} (70%) rename pkg/{controller/utils => common/discovery}/discovery.go (98%) rename pkg/{controller/utils => common/discovery}/discovery_test.go (99%) diff --git a/cmd/main.go b/cmd/main.go index 67307ba2ab..f9c8d81f37 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,7 +35,7 @@ import ( "github.com/tigera/operator/pkg/apis" "github.com/tigera/operator/pkg/awssgsetup" "github.com/tigera/operator/pkg/common" - "github.com/tigera/operator/pkg/common/apidiscovery" + "github.com/tigera/operator/pkg/common/discovery" "github.com/tigera/operator/pkg/components" "github.com/tigera/operator/pkg/controller/metrics" "github.com/tigera/operator/pkg/controller/migration/datastoremigration" @@ -338,7 +338,7 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe // Snapshot the served versions for the Kubernetes APIs the operator branches on. Discovery // happens once here so reconcile loops can do plain map lookups. - apiDiscovery := apidiscovery.New(mgr.GetRESTMapper()) + apiDiscovery := discovery.DiscoverAPIs(mgr.GetRESTMapper()) // If configured to manage CRDs, do a preliminary install of them here. The Installation controller // will reconcile them as well, but we need to make sure they are installed before we start the rest of the controllers. @@ -430,7 +430,7 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe } // Attempt to auto discover the provider - provider, err := utils.AutoDiscoverProvider(ctx, clientset) + provider, err := discovery.AutoDiscoverProvider(ctx, clientset) if err != nil { setupLog.Error(err, "Auto discovery of Provider failed") os.Exit(1) @@ -438,7 +438,7 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe setupLog.WithValues("provider", provider).Info("Checking type of cluster") // Determine if we're running in single or multi-tenant mode. - multiTenant, err := utils.MultiTenant(ctx, clientset) + multiTenant, err := discovery.MultiTenant(ctx, clientset) if err != nil { log.Error(err, "Failed to discovery tenancy mode") os.Exit(1) @@ -446,7 +446,7 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe setupLog.WithValues("tenancy", multiTenant).Info("Checking tenancy mode") // Determine if we need to start the Enterprise specific controllers. - enterpriseCRDExists, err := utils.RequiresTigeraSecure(clientset) + enterpriseCRDExists, err := discovery.RequiresTigeraSecure(clientset) if err != nil { setupLog.Error(err, "Failed to determine if Enterprise controllers are required") os.Exit(1) @@ -512,7 +512,7 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe ShutdownContext: ctx, K8sClientset: clientset, MultiTenant: multiTenant, - ElasticExternal: utils.UseExternalElastic(bootConfig), + ElasticExternal: discovery.UseExternalElastic(bootConfig), UseV3CRDs: v3CRDs, APIDiscovery: apiDiscovery, diff --git a/pkg/common/apidiscovery/discovery.go b/pkg/common/discovery/api.go similarity index 60% rename from pkg/common/apidiscovery/discovery.go rename to pkg/common/discovery/api.go index 1ab578cee4..35eea22185 100644 --- a/pkg/common/apidiscovery/discovery.go +++ b/pkg/common/discovery/api.go @@ -12,11 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package apidiscovery exposes the served versions of API kinds that the operator cares about. -// A snapshot is taken once at startup so controllers can ask which version of an API is available -// (and use that to choose between typed Go imports) without issuing further discovery requests at -// reconcile time. -package apidiscovery +package discovery import ( "k8s.io/apimachinery/pkg/api/meta" @@ -30,17 +26,17 @@ var trackedGroupKinds = []schema.GroupKind{ {Group: "admissionregistration.k8s.io", Kind: "MutatingAdmissionPolicy"}, } -// Discovery is a snapshot of which API versions a cluster serves for the set of GroupKinds the +// APIDiscovery is a snapshot of which API versions a cluster serves for the set of GroupKinds the // operator cares about. Lookups are pure map reads; no API calls are made after construction. -type Discovery struct { +type APIDiscovery struct { versions map[schema.GroupKind]string } -// New consults the supplied RESTMapper for each tracked GroupKind and records the preferred -// served version. GroupKinds not served by the cluster (or unknown to the RESTMapper) map to the -// empty string. Only GroupKinds listed in trackedGroupKinds will return a non-empty version. -func New(mapper meta.RESTMapper) *Discovery { - d := &Discovery{versions: map[schema.GroupKind]string{}} +// DiscoverAPIs consults the supplied RESTMapper for each tracked GroupKind and records the +// preferred served version. GroupKinds not served by the cluster (or unknown to the RESTMapper) +// map to the empty string. Only GroupKinds listed in trackedGroupKinds are queried. +func DiscoverAPIs(mapper meta.RESTMapper) *APIDiscovery { + d := &APIDiscovery{versions: map[schema.GroupKind]string{}} for _, gk := range trackedGroupKinds { // RESTMappings returns mappings sorted by the group's preferred-version order from // discovery — for a GA API like v1+v1beta1, v1 comes first. @@ -54,19 +50,19 @@ func New(mapper meta.RESTMapper) *Discovery { } // ServedVersion returns the preferred served version for the given GroupKind, or "" if the kind -// is not served by the cluster (or was not pre-registered in the tracked list passed to New). -func (d *Discovery) ServedVersion(group, kind string) string { +// is not served by the cluster (or is not in the tracked set). +func (d *APIDiscovery) ServedVersion(group, kind string) string { if d == nil { return "" } return d.versions[schema.GroupKind{Group: group, Kind: kind}] } -// NewStatic constructs a Discovery from an explicit version map. Intended for tests. -func NewStatic(versions map[schema.GroupKind]string) *Discovery { +// NewStaticAPIDiscovery constructs an APIDiscovery from an explicit version map. Intended for tests. +func NewStaticAPIDiscovery(versions map[schema.GroupKind]string) *APIDiscovery { cp := make(map[schema.GroupKind]string, len(versions)) for k, v := range versions { cp[k] = v } - return &Discovery{versions: cp} + return &APIDiscovery{versions: cp} } diff --git a/pkg/common/apidiscovery/discovery_test.go b/pkg/common/discovery/api_test.go similarity index 70% rename from pkg/common/apidiscovery/discovery_test.go rename to pkg/common/discovery/api_test.go index c45eec72a0..36d460e1b4 100644 --- a/pkg/common/apidiscovery/discovery_test.go +++ b/pkg/common/discovery/api_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package apidiscovery +package discovery import ( "testing" @@ -21,15 +21,15 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) -// fakeMapper returns the registered versions for the given GroupKind. Other RESTMapper methods -// are not implemented because Discovery only uses RESTMappings. -type fakeMapper struct { +// fakeAPIMapper returns the registered versions for the given GroupKind. Other RESTMapper methods +// are not implemented because APIDiscovery only uses RESTMappings. +type fakeAPIMapper struct { meta.RESTMapper served map[schema.GroupKind][]string calls int } -func (f *fakeMapper) RESTMappings(gk schema.GroupKind, _ ...string) ([]*meta.RESTMapping, error) { +func (f *fakeAPIMapper) RESTMappings(gk schema.GroupKind, _ ...string) ([]*meta.RESTMapping, error) { f.calls++ versions, ok := f.served[gk] if !ok { @@ -42,15 +42,14 @@ func (f *fakeMapper) RESTMappings(gk schema.GroupKind, _ ...string) ([]*meta.RES return out, nil } -func TestDiscoveryRecordsPreferredVersion(t *testing.T) { - // Serve every tracked GroupKind at v1 (preferred over v1beta1 where applicable). +func TestAPIDiscoveryRecordsPreferredVersion(t *testing.T) { served := map[schema.GroupKind][]string{} for _, gk := range trackedGroupKinds { served[gk] = []string{"v1", "v1beta1"} } - mapper := &fakeMapper{served: served} + mapper := &fakeAPIMapper{served: served} - d := New(mapper) + d := DiscoverAPIs(mapper) for _, gk := range trackedGroupKinds { if got := d.ServedVersion(gk.Group, gk.Kind); got != "v1" { @@ -62,10 +61,9 @@ func TestDiscoveryRecordsPreferredVersion(t *testing.T) { } } -func TestDiscoveryUnservedGroupKind(t *testing.T) { - // Mapper returns NoKindMatchError for everything. - mapper := &fakeMapper{served: map[schema.GroupKind][]string{}} - d := New(mapper) +func TestAPIDiscoveryUnservedGroupKind(t *testing.T) { + mapper := &fakeAPIMapper{served: map[schema.GroupKind][]string{}} + d := DiscoverAPIs(mapper) for _, gk := range trackedGroupKinds { if got := d.ServedVersion(gk.Group, gk.Kind); got != "" { t.Errorf("%s served unexpectedly: %q", gk, got) @@ -73,13 +71,13 @@ func TestDiscoveryUnservedGroupKind(t *testing.T) { } } -func TestDiscoveryNoCallsAfterConstruction(t *testing.T) { +func TestAPIDiscoveryNoCallsAfterConstruction(t *testing.T) { served := map[schema.GroupKind][]string{} for _, gk := range trackedGroupKinds { served[gk] = []string{"v1"} } - mapper := &fakeMapper{served: served} - d := New(mapper) + mapper := &fakeAPIMapper{served: served} + d := DiscoverAPIs(mapper) calls := mapper.calls for i := 0; i < 100; i++ { @@ -93,9 +91,9 @@ func TestDiscoveryNoCallsAfterConstruction(t *testing.T) { } } -func TestNilDiscoverySafe(t *testing.T) { - var d *Discovery +func TestNilAPIDiscoverySafe(t *testing.T) { + var d *APIDiscovery if got := d.ServedVersion("g", "K"); got != "" { - t.Errorf("nil Discovery: got %q, want empty", got) + t.Errorf("nil APIDiscovery: got %q, want empty", got) } } diff --git a/pkg/controller/utils/discovery.go b/pkg/common/discovery/discovery.go similarity index 98% rename from pkg/controller/utils/discovery.go rename to pkg/common/discovery/discovery.go index f80638ea5d..fc2e75f4ec 100644 --- a/pkg/controller/utils/discovery.go +++ b/pkg/common/discovery/discovery.go @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -package utils +// Package discovery exposes helpers for detecting cluster shape (provider, multi-tenancy, served +// APIs) at operator startup. +package discovery import ( "context" diff --git a/pkg/controller/utils/discovery_test.go b/pkg/common/discovery/discovery_test.go similarity index 99% rename from pkg/controller/utils/discovery_test.go rename to pkg/common/discovery/discovery_test.go index 0f102a4d12..f3378921ee 100644 --- a/pkg/controller/utils/discovery_test.go +++ b/pkg/common/discovery/discovery_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package utils +package discovery import ( "context" diff --git a/pkg/controller/installation/core_controller.go b/pkg/controller/installation/core_controller.go index d1b7a7d0e8..9003d78f3e 100644 --- a/pkg/controller/installation/core_controller.go +++ b/pkg/controller/installation/core_controller.go @@ -61,7 +61,7 @@ import ( operatorv1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/active" "github.com/tigera/operator/pkg/common" - "github.com/tigera/operator/pkg/common/apidiscovery" + "github.com/tigera/operator/pkg/common/discovery" "github.com/tigera/operator/pkg/components" "github.com/tigera/operator/pkg/controller/certificatemanager" "github.com/tigera/operator/pkg/controller/ippool" @@ -404,7 +404,7 @@ type ReconcileInstallation struct { migrationWatchReady *utils.ReadyFlag v3CRDs bool kubernetesVersion *common.VersionInfo - apiDiscovery *apidiscovery.Discovery + apiDiscovery *discovery.APIDiscovery // newComponentHandler returns a new component handler. Useful stub for unit testing. newComponentHandler func(log logr.Logger, client client.Client, scheme *runtime.Scheme, cr metav1.Object) utils.ComponentHandler @@ -1004,7 +1004,7 @@ func (r *ReconcileInstallation) Reconcile(ctx context.Context, request reconcile if !r.enterpriseCRDsExist && instance.Spec.Variant.IsEnterprise() { // Perform an API discovery to determine if the necessary APIs exist. If they do, we can reboot into enterprise mode. // if they do not, we need to notify the user that the requested configuration is invalid. - b, err := utils.RequiresTigeraSecure(r.clientset) + b, err := discovery.RequiresTigeraSecure(r.clientset) if b { log.Info("Rebooting to enable TigeraSecure controllers") os.Exit(0) diff --git a/pkg/controller/installation/core_controller_test.go b/pkg/controller/installation/core_controller_test.go index 6465153b52..f2f872d95c 100644 --- a/pkg/controller/installation/core_controller_test.go +++ b/pkg/controller/installation/core_controller_test.go @@ -52,7 +52,7 @@ import ( operator "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/apis" "github.com/tigera/operator/pkg/common" - "github.com/tigera/operator/pkg/common/apidiscovery" + "github.com/tigera/operator/pkg/common/discovery" "github.com/tigera/operator/pkg/components" "github.com/tigera/operator/pkg/controller/certificatemanager" "github.com/tigera/operator/pkg/controller/status" @@ -2576,12 +2576,12 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { return ctrlrfake.DefaultFakeClientBuilder(scheme).WithObjects(initial...).Build() } - discoveryFor := func(mapVersion string) *apidiscovery.Discovery { + discoveryFor := func(mapVersion string) *discovery.APIDiscovery { m := map[schema.GroupKind]string{} if mapVersion != "" { m[admission.PolicyGroupKind] = mapVersion } - return apidiscovery.NewStatic(m) + return discovery.NewStaticAPIDiscovery(m) } BeforeEach(func() { diff --git a/pkg/controller/options/options.go b/pkg/controller/options/options.go index 26b0aa0c80..14acd47230 100644 --- a/pkg/controller/options/options.go +++ b/pkg/controller/options/options.go @@ -19,7 +19,7 @@ import ( v1 "github.com/tigera/operator/api/v1" "github.com/tigera/operator/pkg/common" - "github.com/tigera/operator/pkg/common/apidiscovery" + "github.com/tigera/operator/pkg/common/discovery" "k8s.io/client-go/kubernetes" ) @@ -54,5 +54,5 @@ type ControllerOptions struct { // APIDiscovery is a snapshot of which Kubernetes API versions the cluster serves for the kinds // the operator cares about. Populated once at startup so controllers can branch on API // availability without issuing further discovery requests at reconcile time. - APIDiscovery *apidiscovery.Discovery + APIDiscovery *discovery.APIDiscovery } diff --git a/pkg/controller/utils/utils.go b/pkg/controller/utils/utils.go index 0325af56f8..d64cd25e9a 100644 --- a/pkg/controller/utils/utils.go +++ b/pkg/controller/utils/utils.go @@ -47,6 +47,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/source" @@ -68,6 +69,8 @@ const ( unsupportedIgnoreAnnotation = "unsupported.operator.tigera.io/ignore" ) +var log = logf.Log.WithName("utils") + var ( DefaultInstanceKey = client.ObjectKey{Name: "default"} DefaultEnterpriseInstanceKey = client.ObjectKey{Name: "tigera-secure"} From 4273702a26911371dfd98d892119377e2a8c558e Mon Sep 17 00:00:00 2001 From: Casey Davenport Date: Wed, 20 May 2026 15:58:09 -0700 Subject: [PATCH 7/8] Remove unused log --- pkg/common/discovery/discovery.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/common/discovery/discovery.go b/pkg/common/discovery/discovery.go index fc2e75f4ec..384c80c7ed 100644 --- a/pkg/common/discovery/discovery.go +++ b/pkg/common/discovery/discovery.go @@ -25,13 +25,10 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" - logf "sigs.k8s.io/controller-runtime/pkg/log" operatorv1 "github.com/tigera/operator/api/v1" ) -var log = logf.Log.WithName("discovery") - const gkeNodeLabelPrefix = "cloud.google.com/gke-" // RequiresTigeraSecure determines if the configuration requires we start the tigera secure From d1fbf44fea09acdd475302b32253f43f5f35b33d Mon Sep 17 00:00:00 2001 From: Casey Davenport Date: Fri, 22 May 2026 14:15:24 -0700 Subject: [PATCH 8/8] Update cmd/main.go --- cmd/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/main.go b/cmd/main.go index f9c8d81f37..c0affd2153 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -514,7 +514,6 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe MultiTenant: multiTenant, ElasticExternal: discovery.UseExternalElastic(bootConfig), UseV3CRDs: v3CRDs, - APIDiscovery: apiDiscovery, }