diff --git a/docs.md b/docs.md index d36df09f..ca117f01 100644 --- a/docs.md +++ b/docs.md @@ -250,6 +250,9 @@ This admission webhook prevents the disabling or deletion of a NodeDriver if the ClusterName must be equal to the namespace, and must refer to an existing `management.cattle.io/v3.Cluster` object. In addition, users cannot update the field after creation. +#### BackingNamespace validation +The `BackingNamespace` field cannot be changed once set. Projects without the `BackingNamespace` field can have it added. + #### Protects system project The system project cannot be deleted. @@ -277,8 +280,17 @@ If `field.cattle.io/no-creator-rbac` annotation is set, `field.cattle.io/creator #### On create +Populates the `BackingNamespace` field by concatenating `Project.ClusterName` and `Project.Name`. + +If the project is using a generated name (ie `GenerateName` is not empty), the generation happens within the mutating webhook. +The reason for this is that the `BackingNamespace` is made up of the `Project.Name`, and name generation happens after mutating and before validating webhooks. + Adds the authz.management.cattle.io/creator-role-bindings annotation. +#### On update + +If the `BackingNamespace` field is empty, it's populated with the project name. + ## ProjectRoleTemplateBinding ### Validation Checks @@ -297,7 +309,6 @@ Users cannot create ProjectRoleTemplateBindings that violate the following const - The `ProjectName` field must be: - Provided as a non-empty value - Specified using the format of `clusterName:projectName`; `clusterName` is the `metadata.name` of a cluster, and `projectName` is the `metadata.name` of a project - - The `projectName` part of the field must match the namespace of the ProjectRoleTemplateBinding - Refer to a valid project and cluster (both must exist and project.Spec.ClusterName must equal the cluster) - Either a user subject (through `UserName` or `UserPrincipalName`), or a group subject (through `GroupName` or `GroupPrincipalName`), or a service account subject (through `ServiceAccount`) must be specified. Exactly one diff --git a/go.mod b/go.mod index 876dd5be..32684b2b 100644 --- a/go.mod +++ b/go.mod @@ -41,21 +41,21 @@ require ( github.com/gorilla/mux v1.8.1 github.com/rancher/dynamiclistener v0.6.1-rc.1 github.com/rancher/lasso v0.0.0-20240924233157-8f384efc8813 - github.com/rancher/rancher/pkg/apis v0.0.0-20240903164338-21e4787cd0b3 - github.com/rancher/rke v1.6.0 + github.com/rancher/rancher/pkg/apis v0.0.0-20241030141955-e2d0b42c9125 + github.com/rancher/rke v1.7.0-rc.5 github.com/rancher/wrangler/v3 v3.1.0-rc.1 github.com/robfig/cron v1.2.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 go.uber.org/mock v0.5.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 - golang.org/x/text v0.17.0 + golang.org/x/text v0.19.0 golang.org/x/tools v0.24.0 k8s.io/api v0.31.1 k8s.io/apimachinery v0.31.1 k8s.io/apiserver v0.31.1 k8s.io/client-go v12.0.0+incompatible - k8s.io/kubernetes v1.31.0 + k8s.io/kubernetes v1.31.1 k8s.io/pod-security-admission v0.31.1 k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 sigs.k8s.io/controller-runtime v0.19.0 @@ -109,11 +109,11 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rancher/aks-operator v1.9.1 // indirect - github.com/rancher/eks-operator v1.9.1 // indirect - github.com/rancher/fleet/pkg/apis v0.10.0 // indirect - github.com/rancher/gke-operator v1.9.1 // indirect - github.com/rancher/norman v0.0.0-20240822182819-60ccfabc4ac5 // indirect + github.com/rancher/aks-operator v1.10.0-rc.2 // indirect + github.com/rancher/eks-operator v1.10.0-rc.2 // indirect + github.com/rancher/fleet/pkg/apis v0.11.0-beta.2 // indirect + github.com/rancher/gke-operator v1.10.0-rc.2 // indirect + github.com/rancher/norman v0.0.0-20241001183610-78a520c160ab // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect @@ -122,29 +122,29 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect go.etcd.io/etcd/client/v3 v3.5.15 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.26.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/oauth2 v0.22.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/time v0.7.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect @@ -165,7 +165,6 @@ require ( k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect k8s.io/kubelet v0.31.1 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect - sigs.k8s.io/cli-utils v0.37.2 // indirect sigs.k8s.io/cluster-api v1.8.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect diff --git a/go.sum b/go.sum index 75422f7a..aa61bb84 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -127,10 +127,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0= -github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -146,22 +146,22 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rancher/aks-operator v1.9.1 h1:ARxafxU3c51V91BeCB1w94n3yjrKjjzQrqd/tWrd1go= -github.com/rancher/aks-operator v1.9.1/go.mod h1:RbGSV11kCpSjmSiX+WDiExBRQxnF/oRUdlmeAg9PqNA= +github.com/rancher/aks-operator v1.10.0-rc.2 h1:Efbrf9bZZ/fu9yjTSwj7htOtG/Y5xlY3yaHB41OIv8o= +github.com/rancher/aks-operator v1.10.0-rc.2/go.mod h1:n7CBXwN5mpJZT7/3PYg6cWBAVCqjayhaUiRtTCH1FMQ= github.com/rancher/dynamiclistener v0.6.1-rc.1 h1:EGmTpPzSI5LHj35Wg3NHFmKSbZdzNuh5zptws1jo/yo= github.com/rancher/dynamiclistener v0.6.1-rc.1/go.mod h1:7VNEQhAwzbYJ08S1MYb6B4vili6K7CcrG4cNZXq1j+s= -github.com/rancher/eks-operator v1.9.1 h1:o0K2jcEdlrERRuIyC/CVZkRW++EcCa6GbVidBEfFh0w= -github.com/rancher/eks-operator v1.9.1/go.mod h1:vMQSu6MQqLkuilXcv+KKlL15sFBg24TZQUifVgoDmIc= -github.com/rancher/fleet/pkg/apis v0.10.0 h1:0f8OEghEDJNzvUAR2fpg2dw8EnAgfWvkhnwsYFS9G+w= -github.com/rancher/fleet/pkg/apis v0.10.0/go.mod h1:mjirthAmgpz0xo+qywUiaJDFpjnmX3xrc2E0/qmk3yc= -github.com/rancher/gke-operator v1.9.1 h1:COsXcgo10QEXMzju9zuGFBA+muvHfIiaTXpRz9dwQ2I= -github.com/rancher/gke-operator v1.9.1/go.mod h1:D/U7p7NpQtZcdHFWM+uBOmtk9/tVdlZv7BgR+TIiMAI= +github.com/rancher/eks-operator v1.10.0-rc.2 h1:StjPwsrZ6GY5G47xhIYC9y6rP5r9roMbpAemazRTuR0= +github.com/rancher/eks-operator v1.10.0-rc.2/go.mod h1:coW31jIfImAHdGsepc7yCXSuixdclQkJn3y26E9tsss= +github.com/rancher/fleet/pkg/apis v0.11.0-beta.2 h1:NKES0uiPFHBBi7ag2ZbfVbMILTqZt2GdpaxAT1p6xHw= +github.com/rancher/fleet/pkg/apis v0.11.0-beta.2/go.mod h1:BnNQzIwjbGEnDBtpN2yDFYr32KhejzarPYBUiTR71tU= +github.com/rancher/gke-operator v1.10.0-rc.2 h1:zo6nqyJPACJbRGEpy/u1T1mpknCieNKm9Nin7shQ2XI= +github.com/rancher/gke-operator v1.10.0-rc.2/go.mod h1:k3oIJMCilpaLHeHPRy90S3pfZ05vbe+b+g1ISiHQbLo= github.com/rancher/lasso v0.0.0-20240924233157-8f384efc8813 h1:V/LY8pUHZG9Kc+xEDWDOryOnCU6/Q+Lsr9QQEQnshpU= github.com/rancher/lasso v0.0.0-20240924233157-8f384efc8813/go.mod h1:IxgTBO55lziYhTEETyVKiT8/B5Rg92qYiRmcIIYoPgI= -github.com/rancher/norman v0.0.0-20240822182819-60ccfabc4ac5 h1:Z34NXcW0ymdpVBfd1R0vvqTXBh1gCOdPNFtB+RUahQw= -github.com/rancher/norman v0.0.0-20240822182819-60ccfabc4ac5/go.mod h1:dyjfXBsNiroPWOdUZe7diUOUSLf6HQ/r2kEpwH/8zas= -github.com/rancher/rancher/pkg/apis v0.0.0-20240903164338-21e4787cd0b3 h1:KCO+g13mukOPpaYFUMmr3oMULltsFbp6H8rp4NV02jI= -github.com/rancher/rancher/pkg/apis v0.0.0-20240903164338-21e4787cd0b3/go.mod h1:V1RX7d/ziNUUD0RRz/HDf3xaCZdJPdbXRQLArExtg+U= +github.com/rancher/norman v0.0.0-20241001183610-78a520c160ab h1:ihK6See3y/JilqZlc0CG7NXPN+ue5nY9U7xUZUA8M7I= +github.com/rancher/norman v0.0.0-20241001183610-78a520c160ab/go.mod h1:qX/OG/4wY27xSAcSdRilUBxBumV6Ey2CWpAeaKnBQDs= +github.com/rancher/rancher/pkg/apis v0.0.0-20241030141955-e2d0b42c9125 h1:OouJhq7h3eM1UOoWyS0jlEbEVH+Z4og0h1DzBfgkHtA= +github.com/rancher/rancher/pkg/apis v0.0.0-20241030141955-e2d0b42c9125/go.mod h1:dOPalv772EkfFGnXcA+ejkxO9Blj2M+qthK4lF5N7ac= github.com/rancher/rke v1.6.2 h1:ttGk77t5oe7bsiS7s7SOFmAl3PALYI5M2SQQenjKevk= github.com/rancher/rke v1.6.2/go.mod h1:5xRbf3L8PxqJRhABjYRfaBqbpVqAnqyH3maUNQEuwvk= github.com/rancher/wrangler/v3 v3.1.0-rc.1 h1:tEZZjDj4lOyQeqEcrl6d5Xr+QAS04vOInS43QqJz1LQ= @@ -218,20 +218,20 @@ go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -245,8 +245,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -257,10 +257,10 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -270,16 +270,16 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -295,14 +295,14 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f h1:jTm13A2itBi3La6yTGqn8bVSrc3ZZ1r8ENHlIXBfnRA= +google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f/go.mod h1:CLGoBuH1VHxAUXVPP8FfPwPEVJB6lz3URE5mY2SuayE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -365,8 +365,6 @@ k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfG k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/cli-utils v0.37.2 h1:GOfKw5RV2HDQZDJlru5KkfLO1tbxqMoyn1IYUxqBpNg= -sigs.k8s.io/cli-utils v0.37.2/go.mod h1:V+IZZr4UoGj7gMJXklWBg6t5xbdThFBcpj4MrZuCYco= sigs.k8s.io/cluster-api v1.8.3 h1:N6i25rF5QMadwVg2UPfuO6CzmNXjqnF2r1MAO+kcsro= sigs.k8s.io/cluster-api v1.8.3/go.mod h1:pXv5LqLxuIbhGIXykyNKiJh+KrLweSBajVHHitPLyoY= sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= diff --git a/pkg/resources/management.cattle.io/v3/globalrolebinding/mutator_test.go b/pkg/resources/management.cattle.io/v3/globalrolebinding/mutator_test.go index 8e27d916..e78503e9 100644 --- a/pkg/resources/management.cattle.io/v3/globalrolebinding/mutator_test.go +++ b/pkg/resources/management.cattle.io/v3/globalrolebinding/mutator_test.go @@ -358,7 +358,7 @@ func Test_MutatorAdmit(t *testing.T) { gotObj := &apisv3.GlobalRoleBinding{} err = json.Unmarshal(patchedJS, gotObj) - require.NoError(t, err, "failed to unmarshall patched Object") + require.NoError(t, err, "failed to unmarshal patched Object") require.True(t, equality.Semantic.DeepEqual(test.wantGRB(), gotObj), "patched object and desired object are not equivalent wanted=%#v got=%#v", test.wantGRB(), gotObj) } else { diff --git a/pkg/resources/management.cattle.io/v3/project/Project.md b/pkg/resources/management.cattle.io/v3/project/Project.md index 8bb250a9..fc4e6189 100644 --- a/pkg/resources/management.cattle.io/v3/project/Project.md +++ b/pkg/resources/management.cattle.io/v3/project/Project.md @@ -4,6 +4,9 @@ ClusterName must be equal to the namespace, and must refer to an existing `management.cattle.io/v3.Cluster` object. In addition, users cannot update the field after creation. +### BackingNamespace validation +The `BackingNamespace` field cannot be changed once set. Projects without the `BackingNamespace` field can have it added. + ### Protects system project The system project cannot be deleted. @@ -31,4 +34,13 @@ If `field.cattle.io/no-creator-rbac` annotation is set, `field.cattle.io/creator ### On create +Populates the `BackingNamespace` field by concatenating `Project.ClusterName` and `Project.Name`. + +If the project is using a generated name (ie `GenerateName` is not empty), the generation happens within the mutating webhook. +The reason for this is that the `BackingNamespace` is made up of the `Project.Name`, and name generation happens after mutating and before validating webhooks. + Adds the authz.management.cattle.io/creator-role-bindings annotation. + +### On update + +If the `BackingNamespace` field is empty, it's populated with the project name. diff --git a/pkg/resources/management.cattle.io/v3/project/mutator.go b/pkg/resources/management.cattle.io/v3/project/mutator.go index b8e087d6..af90e7ea 100644 --- a/pkg/resources/management.cattle.io/v3/project/mutator.go +++ b/pkg/resources/management.cattle.io/v3/project/mutator.go @@ -3,16 +3,22 @@ package project import ( "encoding/json" "fmt" + "strings" v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" "github.com/rancher/webhook/pkg/admission" ctrlv3 "github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io/v3" objectsv3 "github.com/rancher/webhook/pkg/generated/objects/management.cattle.io/v3" "github.com/rancher/webhook/pkg/patch" + corev1controller "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1" + "github.com/rancher/wrangler/v3/pkg/name" "github.com/sirupsen/logrus" admissionv1 "k8s.io/api/admission/v1" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/storage/names" "k8s.io/utils/trace" ) @@ -31,13 +37,17 @@ var gvr = schema.GroupVersionResource{ // Mutator implements admission.MutatingAdmissionWebhook. type Mutator struct { roleTemplateCache ctrlv3.RoleTemplateCache + namespaceClient corev1controller.NamespaceController + projectClient ctrlv3.ProjectClient } // NewMutator returns a new mutator which mutates projects -func NewMutator(roleTemplateCache ctrlv3.RoleTemplateCache) *Mutator { +func NewMutator(nsClient corev1controller.NamespaceController, roleTemplateCache ctrlv3.RoleTemplateCache, projectClient ctrlv3.ProjectClient) *Mutator { roleTemplateCache.AddIndexer(mutatorCreatorRoleTemplateIndex, creatorRoleTemplateIndexer) return &Mutator{ roleTemplateCache: roleTemplateCache, + namespaceClient: nsClient, + projectClient: projectClient, } } @@ -58,6 +68,7 @@ func (m *Mutator) GVR() schema.GroupVersionResource { func (m *Mutator) Operations() []admissionregistrationv1.OperationType { return []admissionregistrationv1.OperationType{ admissionregistrationv1.Create, + admissionregistrationv1.Update, } } @@ -85,13 +96,81 @@ func (m *Mutator) Admit(request *admission.Request) (*admissionv1.AdmissionRespo } switch request.Operation { case admissionv1.Create: - return m.admitCreate(project, request) + project, err = m.createProjectNamespace(project) + if err != nil { + return nil, err + } + project, err = m.addCreatorRoleBindings(project) + if err != nil { + return nil, err + } + case admissionv1.Update: + project = m.updateProjectNamespace(project) default: return nil, fmt.Errorf("operation type %q not handled", request.Operation) } + response := &admissionv1.AdmissionResponse{} + if err := patch.CreatePatch(request.Object.Raw, project, response); err != nil { + return nil, fmt.Errorf("failed to create patch: %w", err) + } + response.Allowed = true + return response, nil +} + +func (m *Mutator) createProjectNamespace(project *v3.Project) (*v3.Project, error) { + newProject := project.DeepCopy() + backingNamespace := "" + var err error + // When the project name is empty, that means we want to generate a name for it + // Name generation happens after mutating webhooks, so in order to have access to the name early + // for the backing namespace, we need to generate it ourselves + if project.Name == "" { + // If err is nil, (meaning "project exists", see below) we need to repeat the generation process to find a project name and backing namespace that isn't taken + for err == nil { + newName := names.SimpleNameGenerator.GenerateName(project.GenerateName) + _, err = m.projectClient.Get(newProject.Spec.ClusterName, newName, v1.GetOptions{}) + if err == nil { + // A project with this name already exists. Generate a new name. + continue + } else if !apierrors.IsNotFound(err) { + return nil, err + } + + backingNamespace = name.SafeConcatName(newProject.Spec.ClusterName, strings.ToLower(newName)) + _, err = m.namespaceClient.Get(backingNamespace, v1.GetOptions{}) + + // If the backing namespace already exists, generate a new project name + if err != nil && !apierrors.IsNotFound(err) { + return nil, err + } + } + } else { + backingNamespace = name.SafeConcatName(newProject.Spec.ClusterName, strings.ToLower(newProject.Name)) + _, err = m.namespaceClient.Get(backingNamespace, v1.GetOptions{}) + if err == nil { + return nil, fmt.Errorf("failed to create project: namespace %s already exists", backingNamespace) + } else if !apierrors.IsNotFound(err) { + return nil, err + } + } + + newProject.Status.BackingNamespace = backingNamespace + return newProject, nil } -func (m *Mutator) admitCreate(project *v3.Project, request *admission.Request) (*admissionv1.AdmissionResponse, error) { +// updateProjectNamespace fills in BackingNamespace with the project name if it wasn't already set. +// This was the naming convention for project namespaces prior to using the BackingNamespace field. +// Filling it here is just to maintain backwards compatibility. +func (m *Mutator) updateProjectNamespace(project *v3.Project) *v3.Project { + if project.Status.BackingNamespace != "" { + return project + } + newProject := project.DeepCopy() + newProject.Status.BackingNamespace = newProject.Name + return newProject +} + +func (m *Mutator) addCreatorRoleBindings(project *v3.Project) (*v3.Project, error) { logrus.Debugf("[project-mutation] adding creator-role-bindings to project: %v", project.Name) newProject := project.DeepCopy() @@ -103,12 +182,7 @@ func (m *Mutator) admitCreate(project *v3.Project, request *admission.Request) ( return nil, fmt.Errorf("failed to add annotation to project %s: %w", project.Name, err) } newProject.Annotations[roleTemplatesRequired] = annotations - response := &admissionv1.AdmissionResponse{} - if err := patch.CreatePatch(request.Object.Raw, newProject, response); err != nil { - return nil, fmt.Errorf("failed to create patch: %w", err) - } - response.Allowed = true - return response, nil + return newProject, nil } func (m *Mutator) getCreatorRoleTemplateAnnotations() (string, error) { diff --git a/pkg/resources/management.cattle.io/v3/project/mutator_test.go b/pkg/resources/management.cattle.io/v3/project/mutator_test.go index 24261448..e28dfa66 100644 --- a/pkg/resources/management.cattle.io/v3/project/mutator_test.go +++ b/pkg/resources/management.cattle.io/v3/project/mutator_test.go @@ -5,12 +5,16 @@ import ( "fmt" "testing" + jsonpatch "github.com/evanphx/json-patch" v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" "github.com/rancher/wrangler/v3/pkg/generic/fake" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" ) const ( @@ -18,23 +22,37 @@ const ( expectedIndexKey = "creatorDefaultUnlocked" ) +var ( + defaultProject = v3.Project{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testproject", + }, + Spec: v3.ProjectSpec{ + ClusterName: "testcluster", + }, + } + emptyProject = func() *v3.Project { + return &v3.Project{} + } +) + func TestAdmit(t *testing.T) { t.Parallel() tests := []struct { - name string - operation admissionv1.Operation - dryRun bool - oldProject *v3.Project - newProject *v3.Project - indexer func() ([]*v3.RoleTemplate, error) - wantPatch []map[string]interface{} - wantErr bool + name string + operation admissionv1.Operation + dryRun bool + oldProject func() *v3.Project + newProject func() *v3.Project + indexer func() ([]*v3.RoleTemplate, error) + wantProject func() *v3.Project + wantErr bool }{ { name: "dry run returns allowed", operation: admissionv1.Update, dryRun: true, - newProject: &v3.Project{}, + newProject: emptyProject, }, { name: "failure to decode project returns error", @@ -44,85 +62,100 @@ func TestAdmit(t *testing.T) { { name: "delete operation is invalid", operation: admissionv1.Delete, - newProject: &v3.Project{}, - oldProject: &v3.Project{}, + newProject: emptyProject, + oldProject: emptyProject, wantErr: true, }, { - name: "update operation is invalid", - operation: admissionv1.Update, - newProject: &v3.Project{}, - oldProject: &v3.Project{}, - wantErr: true, + name: "update operation is valid and adds backingNamespace", + operation: admissionv1.Update, + newProject: func() *v3.Project { + return &v3.Project{ + ObjectMeta: metav1.ObjectMeta{ + Name: "p-abc123", + }, + } + }, + oldProject: func() *v3.Project { + return &v3.Project{ + ObjectMeta: metav1.ObjectMeta{ + Name: "p-abc123", + }, + } + }, + wantProject: func() *v3.Project { + return &v3.Project{ + ObjectMeta: metav1.ObjectMeta{ + Name: "p-abc123", + }, + Status: v3.ProjectStatus{ + BackingNamespace: "p-abc123", + }, + } + }, }, { name: "connect operation is invalid", operation: admissionv1.Connect, - newProject: &v3.Project{}, - oldProject: &v3.Project{}, + newProject: emptyProject, + oldProject: emptyProject, wantErr: true, }, { name: "indexer error", operation: admissionv1.Create, - newProject: &v3.Project{}, + newProject: emptyProject, indexer: func() ([]*v3.RoleTemplate, error) { return nil, fmt.Errorf("indexer error") }, wantErr: true, }, { name: "indexer returns empty", operation: admissionv1.Create, - newProject: &v3.Project{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testproject", - }, + newProject: func() *v3.Project { + return defaultProject.DeepCopy() }, indexer: func() ([]*v3.RoleTemplate, error) { return nil, nil }, - wantPatch: []map[string]interface{}{ - { - "op": "add", - "path": "/metadata/annotations", - "value": map[string]string{ - "authz.management.cattle.io/creator-role-bindings": "{}", - }, - }, + wantProject: func() *v3.Project { + p := defaultProject.DeepCopy() + p.Annotations = map[string]string{ + "authz.management.cattle.io/creator-role-bindings": "{}", + } + p.Status.BackingNamespace = "testcluster-testproject" + return p }, }, { name: "created project gets annotation added", operation: admissionv1.Create, - newProject: &v3.Project{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testproject", - }, + newProject: func() *v3.Project { + return defaultProject.DeepCopy() }, - wantPatch: []map[string]interface{}{ - { - "op": "add", - "path": "/metadata/annotations", - "value": map[string]string{ - "authz.management.cattle.io/creator-role-bindings": "{\"required\":[\"project-owner\"]}", - }, - }, + wantProject: func() *v3.Project { + p := defaultProject.DeepCopy() + p.Annotations = map[string]string{ + "authz.management.cattle.io/creator-role-bindings": "{\"required\":[\"project-owner\"]}", + } + p.Status.BackingNamespace = "testcluster-testproject" + return p }, }, { name: "override user-set annotations", operation: admissionv1.Create, - newProject: &v3.Project{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testproject", - Annotations: map[string]string{ - "authz.management.cattle.io/creator-role-bindings": "my own setting", - }, - }, + newProject: func() *v3.Project { + p := defaultProject.DeepCopy() + p.Annotations = map[string]string{ + "authz.management.cattle.io/creator-role-bindings": "my own setting", + } + return p }, - wantPatch: []map[string]interface{}{ - { - "op": "replace", - "path": "/metadata/annotations/authz.management.cattle.io~1creator-role-bindings", - "value": "{\"required\":[\"project-owner\"]}", - }, + wantProject: func() *v3.Project { + p := defaultProject.DeepCopy() + p.Annotations = map[string]string{ + "authz.management.cattle.io/creator-role-bindings": "{\"required\":[\"project-owner\"]}", + } + p.Status.BackingNamespace = "testcluster-testproject" + return p }, }, } @@ -145,8 +178,12 @@ func TestAdmit(t *testing.T) { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() - req, err := createProjectRequest(test.oldProject, test.newProject, test.operation, test.dryRun) - assert.NoError(t, err) + + ctrl := gomock.NewController(t) + nsMock := fake.NewMockNonNamespacedControllerInterface[*corev1.Namespace, *corev1.NamespaceList](ctrl) + nsMock.EXPECT().Get(gomock.Any(), metav1.GetOptions{}).Return(nil, apierrors.NewNotFound(schema.GroupResource{}, "")).AnyTimes() + projectMock := fake.NewMockClientInterface[*v3.Project, *v3.ProjectList](ctrl) + projectMock.EXPECT().Get(gomock.Any(), gomock.Any(), metav1.GetOptions{}).Return(nil, apierrors.NewNotFound(schema.GroupResource{}, "")).AnyTimes() roleTemplateCache := fake.NewMockNonNamespacedCacheInterface[*v3.RoleTemplate](gomock.NewController(t)) roleTemplateCache.EXPECT().AddIndexer(expectedIndexerName, gomock.Any()) indexer := defaultIndexer @@ -155,19 +192,41 @@ func TestAdmit(t *testing.T) { } returnedRTs, returnedErr := indexer() roleTemplateCache.EXPECT().GetByIndex(expectedIndexerName, expectedIndexKey).Return(returnedRTs, returnedErr).AnyTimes() - m := NewMutator(roleTemplateCache) + + var oldProject, newProject *v3.Project + if test.oldProject != nil { + oldProject = test.oldProject() + } + if test.newProject != nil { + newProject = test.newProject() + } + req, err := createProjectRequest(oldProject, newProject, test.operation, test.dryRun) + assert.NoError(t, err) + m := NewMutator(nsMock, roleTemplateCache, projectMock) + resp, err := m.Admit(req) + if test.wantErr { assert.Error(t, err) return } + assert.NoError(t, err, "Admit failed") assert.Equal(t, true, resp.Allowed) - var wantPatch []byte - if test.wantPatch != nil { - wantPatch, err = json.Marshal(test.wantPatch) - assert.NoError(t, err) + if test.wantProject != nil { + patchObj, err := jsonpatch.DecodePatch(resp.Patch) + assert.NoError(t, err, "failed to decode patch from response") + + patchedJS, err := patchObj.Apply(req.Object.Raw) + assert.NoError(t, err, "failed to apply patch to Object") + + gotObj := &v3.Project{} + err = json.Unmarshal(patchedJS, gotObj) + assert.NoError(t, err, "failed to unmarshal patched Object") + + assert.Equal(t, test.wantProject(), gotObj) + } else { + assert.Nil(t, resp.Patch, "unexpected patch request received") } - assert.Equal(t, string(wantPatch), string(resp.Patch)) }) } } diff --git a/pkg/resources/management.cattle.io/v3/project/validator.go b/pkg/resources/management.cattle.io/v3/project/validator.go index 2c960c93..a9c6b919 100644 --- a/pkg/resources/management.cattle.io/v3/project/validator.go +++ b/pkg/resources/management.cattle.io/v3/project/validator.go @@ -23,11 +23,12 @@ import ( ) const ( - systemProjectLabel = "authz.management.cattle.io/system-project" - projectQuotaField = "resourceQuota" - clusterNameField = "clusterName" - namespaceQuotaField = "namespaceDefaultResourceQuota" - containerLimitField = "containerDefaultResourceLimit" + systemProjectLabel = "authz.management.cattle.io/system-project" + projectQuotaField = "resourceQuota" + clusterNameField = "clusterName" + backingNamespaceField = "backingNamespace" + namespaceQuotaField = "namespaceDefaultResourceQuota" + containerLimitField = "containerDefaultResourceLimit" ) var projectSpecFieldPath = field.NewPath("project").Child("spec") @@ -129,8 +130,13 @@ func (a *admitter) admitCreate(project *v3.Project) (*admissionv1.AdmissionRespo } func (a *admitter) admitUpdate(oldProject, newProject *v3.Project) (*admissionv1.AdmissionResponse, error) { + var fieldErr *field.Error if oldProject.Spec.ClusterName != newProject.Spec.ClusterName { - fieldErr := field.Invalid(projectSpecFieldPath.Child(clusterNameField), newProject.Spec.ClusterName, "field is immutable") + fieldErr = field.Invalid(projectSpecFieldPath.Child(clusterNameField), newProject.Spec.ClusterName, "field is immutable") + } else if oldProject.Status.BackingNamespace != "" && oldProject.Status.BackingNamespace != newProject.Status.BackingNamespace { + fieldErr = field.Invalid(projectSpecFieldPath.Child(backingNamespaceField), newProject.Status.BackingNamespace, "field is immutable") + } + if fieldErr != nil { return admission.ResponseBadRequest(fieldErr.Error()), nil } diff --git a/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/ProjectRoleTemplateBinding.md b/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/ProjectRoleTemplateBinding.md index dc57d547..baa22592 100644 --- a/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/ProjectRoleTemplateBinding.md +++ b/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/ProjectRoleTemplateBinding.md @@ -14,7 +14,6 @@ Users cannot create ProjectRoleTemplateBindings that violate the following const - The `ProjectName` field must be: - Provided as a non-empty value - Specified using the format of `clusterName:projectName`; `clusterName` is the `metadata.name` of a cluster, and `projectName` is the `metadata.name` of a project - - The `projectName` part of the field must match the namespace of the ProjectRoleTemplateBinding - Refer to a valid project and cluster (both must exist and project.Spec.ClusterName must equal the cluster) - Either a user subject (through `UserName` or `UserPrincipalName`), or a group subject (through `GroupName` or `GroupPrincipalName`), or a service account subject (through `ServiceAccount`) must be specified. Exactly one diff --git a/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/validator.go b/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/validator.go index 1c80e77d..745b74da 100644 --- a/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/validator.go +++ b/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/validator.go @@ -210,9 +210,6 @@ func (a *admitter) validateCreateFields(newPRTB *apisv3.ProjectRoleTemplateBindi if clusterName == "" || projectName == "" { return field.Invalid(fieldPath.Child("projectName"), newPRTB.ProjectName, "projectName must be of the form cluster.metadata.name:project.metadata.name, and both must refer to an existing object") } - if projectName != newPRTB.Namespace { - return field.Forbidden(fieldPath, "namespace and the projectName part of projectName must match") - } cluster, err := a.clusterCache.Get(clusterName) clusterNotFoundErr := field.Invalid(fieldPath.Child("projectName"), newPRTB.ProjectName, fmt.Sprintf("specified cluster %s not found", clusterName)) if err != nil { diff --git a/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/validator_test.go b/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/validator_test.go index b3c88798..1d523424 100644 --- a/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/validator_test.go +++ b/pkg/resources/management.cattle.io/v3/projectroletemplatebinding/validator_test.go @@ -998,22 +998,6 @@ func (p *ProjectRoleTemplateBindingSuite) TestValidationOnCreate() { }, allowed: false, }, - { - name: "namespace and the project id part of the project name differ", - args: args{ - username: adminUser, - oldPRTB: func() *apisv3.ProjectRoleTemplateBinding { - return nil - }, - newPRTB: func() *apisv3.ProjectRoleTemplateBinding { - basePRTB := newBasePRTB() - basePRTB.ObjectMeta.Namespace = "default" - basePRTB.ProjectName = fmt.Sprintf("%s:%s", clusterID, "p-cgtq4") - return basePRTB - }, - }, - allowed: false, - }, { name: "missing cluster name", args: args{ diff --git a/pkg/server/handlers.go b/pkg/server/handlers.go index a6f2299a..28272b6a 100644 --- a/pkg/server/handlers.go +++ b/pkg/server/handlers.go @@ -97,7 +97,7 @@ func Mutation(clients *clients.Clients) ([]admission.MutatingAdmissionHandler, e if clients.MultiClusterManagement { secrets := secret.NewMutator(clients.RBAC.Role(), clients.RBAC.RoleBinding()) - projects := project.NewMutator(clients.Management.RoleTemplate().Cache()) + projects := project.NewMutator(clients.Core.Namespace(), clients.Management.RoleTemplate().Cache(), clients.Management.Project()) grbs := globalrolebinding.NewMutator(clients.Management.GlobalRole().Cache()) mutators = append(mutators, secrets, projects, grbs) }