diff --git a/cmd/main.go b/cmd/main.go index 7b1117b355..c0affd2153 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/discovery" "github.com/tigera/operator/pkg/components" "github.com/tigera/operator/pkg/controller/metrics" "github.com/tigera/operator/pkg/controller/migration/datastoremigration" @@ -335,6 +336,10 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe os.Exit(1) } + // 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 := 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. if bootstrapCRDs || manageCRDs { @@ -345,7 +350,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, apiDiscovery.ServedVersion(admission.APIGroup, admission.KindPolicy), setupLog); err != nil { setupLog.Error(err, "Failed to ensure MutatingAdmissionPolicies are created") os.Exit(1) } @@ -425,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) @@ -433,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) @@ -441,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) @@ -507,8 +512,9 @@ 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, } // Before we start any controllers, make sure our options are valid. diff --git a/go.mod b/go.mod index dbce0cd331..24400013a4 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 1c1f3549d8..bc64c80be8 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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= -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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= 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= diff --git a/pkg/common/discovery/api.go b/pkg/common/discovery/api.go new file mode 100644 index 0000000000..35eea22185 --- /dev/null +++ b/pkg/common/discovery/api.go @@ -0,0 +1,68 @@ +// 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 discovery + +import ( + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// 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"}, +} + +// 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 APIDiscovery struct { + 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. + 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 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}] +} + +// 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 &APIDiscovery{versions: cp} +} diff --git a/pkg/common/discovery/api_test.go b/pkg/common/discovery/api_test.go new file mode 100644 index 0000000000..36d460e1b4 --- /dev/null +++ b/pkg/common/discovery/api_test.go @@ -0,0 +1,99 @@ +// 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 discovery + +import ( + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// 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 *fakeAPIMapper) 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 TestAPIDiscoveryRecordsPreferredVersion(t *testing.T) { + served := map[schema.GroupKind][]string{} + for _, gk := range trackedGroupKinds { + served[gk] = []string{"v1", "v1beta1"} + } + mapper := &fakeAPIMapper{served: served} + + d := DiscoverAPIs(mapper) + + 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 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) + } + } +} + +func TestAPIDiscoveryNoCallsAfterConstruction(t *testing.T) { + served := map[schema.GroupKind][]string{} + for _, gk := range trackedGroupKinds { + served[gk] = []string{"v1"} + } + mapper := &fakeAPIMapper{served: served} + d := DiscoverAPIs(mapper) + calls := mapper.calls + + for i := 0; i < 100; i++ { + for _, gk := range trackedGroupKinds { + _ = d.ServedVersion(gk.Group, gk.Kind) + } + _ = 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 TestNilAPIDiscoverySafe(t *testing.T) { + var d *APIDiscovery + if got := d.ServedVersion("g", "K"); 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..384c80c7ed 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" @@ -23,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 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/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 a018c16364..30c5f2d329 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" @@ -62,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/discovery" "github.com/tigera/operator/pkg/components" "github.com/tigera/operator/pkg/controller/certificatemanager" "github.com/tigera/operator/pkg/controller/ippool" @@ -345,6 +345,7 @@ func newReconciler(mgr manager.Manager, opts options.ControllerOptions) (*Reconc newComponentHandler: utils.NewComponentHandler, v3CRDs: opts.UseV3CRDs, kubernetesVersion: opts.KubernetesVersion, + apiDiscovery: opts.APIDiscovery, } r.status.Run(opts.ShutdownContext) r.typhaAutoscaler.start(opts.ShutdownContext) @@ -403,6 +404,7 @@ type ReconcileInstallation struct { migrationWatchReady *utils.ReadyFlag v3CRDs bool kubernetesVersion *common.VersionInfo + 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 @@ -1002,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) @@ -2270,47 +2272,44 @@ 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) + + // 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). + 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) + desired := admission.GetMutatingAdmissionPolicies(install.Spec.Variant, r.v3CRDs, mapAPIVersion) - // 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, mapAPIVersion) + 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..f2f872d95c 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" @@ -36,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" @@ -50,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/discovery" "github.com/tigera/operator/pkg/components" "github.com/tigera/operator/pkg/controller/certificatemanager" "github.com/tigera/operator/pkg/controller/status" @@ -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,18 @@ var _ = Describe("updateMutatingAdmissionPolicies", func() { installation *operator.Installation ) + clientFor := func(initial ...client.Object) client.Client { + return ctrlrfake.DefaultFakeClientBuilder(scheme).WithObjects(initial...).Build() + } + + discoveryFor := func(mapVersion string) *discovery.APIDiscovery { + m := map[schema.GroupKind]string{} + if mapVersion != "" { + m[admission.PolicyGroupKind] = mapVersion + } + return discovery.NewStaticAPIDiscovery(m) + } + BeforeEach(func() { log = logr.Discard() ctx, cancel = context.WithCancel(context.Background()) @@ -2577,10 +2591,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 +2610,29 @@ 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(), + 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 }, } - 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 +2641,115 @@ 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(), + 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 }, } - 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(), + scheme: scheme, + status: mockStatus, + manageCRDs: true, + v3CRDs: true, + apiDiscovery: discoveryFor(""), 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(), + 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 }, } - 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(), + 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 }, } - 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(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 }, } - 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 +2760,49 @@ 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(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 }, } - 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(), + 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 }, @@ -2817,8 +2810,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/controller/options/options.go b/pkg/controller/options/options.go index e7e10e1112..14acd47230 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/discovery" "k8s.io/client-go/kubernetes" ) @@ -49,4 +50,9 @@ type ControllerOptions struct { // Whether or not to use crd.projectcalico.org/v1 or projectcalico.org/v3 for Calico CRDs. UseV3CRDs bool + + // 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 *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"} diff --git a/pkg/imports/admission/admission.go b/pkg/imports/admission/admission.go index 5731149b9b..52369050ec 100644 --- a/pkg/imports/admission/admission.go +++ b/pkg/imports/admission/admission.go @@ -23,8 +23,11 @@ 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" + 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,8 +40,24 @@ 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" + + // 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 @@ -47,10 +66,11 @@ var ( ) // 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 +103,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 +124,20 @@ 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. -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 } - objs := GetMutatingAdmissionPolicies(opv1.ProductVariant(variant), v3) + if apiVersion == "" { + log.Info("MutatingAdmissionPolicy API not available on cluster, skipping bootstrap") + return nil + } + + 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 +150,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 +166,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 +177,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..2a747b4e26 100644 --- a/pkg/imports/admission/admission_test.go +++ b/pkg/imports/admission/admission_test.go @@ -18,59 +18,83 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1" 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)) + Describe("GetMutatingAdmissionPolicies", func() { + It("returns Calico v1beta1 MAPs when v3=true", func() { + objs := GetMutatingAdmissionPolicies(opv1.Calico, true, VersionV1Beta1) + Expect(objs).To(HaveLen(4)) - // 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++ + 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)) } - // 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) - }) + Expect(mapCount).To(Equal(2)) + Expect(mapbCount).To(Equal(2)) + }) - 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)) + It("returns Calico v1 MAPs when discovered version is v1", func() { + objs := GetMutatingAdmissionPolicies(opv1.Calico, true, VersionV1) + Expect(objs).To(HaveLen(4)) - var mapCount, mapbCount int - for _, obj := range objs { - switch obj.(type) { - case *admissionv1beta1.MutatingAdmissionPolicy: - mapCount++ - case *admissionv1beta1.MutatingAdmissionPolicyBinding: - mapbCount++ + 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(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 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("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") - } + 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()) + } + }) }) })