Change:
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks! If the matter is security related, please disclose it privately via --What happened: It appears that validating admission requests for server-side apply are populating the for requests to create Namespace objects. Non server-side apply requests don't do this, and a missing namespace field is the only way to distinguish cluster-scoped resources from namespace-scoped resources. This appears to be a breaking behavioral change. See What you expected to happen: Requests to create namespaces would have a consistent signature between server-side apply and non-server-side apply contexts. How to reproduce it (as minimally and precisely as possible): See Anything else we need to know?: Environment: Kubernetes version (use ): Cloud provider or hardware configuration: kind OS (e.g: ): Kernel (e.g. ): Install tools: Network plugin and version (if this is a network-related bug): Others:
/sig sig-api-machinery
The label(s) cannot be applied, because the repository doesn't have them <detailsIn response to : /sig sig-api-machinery Instructions for interacting with me using PR comments are available . If you have questions or suggestions related to my behavior, please file an issue against the repository. </details
/sig api-machinery
/wg api-expression
/assign
/assign
Requests made to namespaced URLs (URLs which contain a segment) populate the attribute in AdmissionReview requests. The documentation of the AdmissionReview field is as follows: From Update/Patch/Delete requests to specific namespaces have consistently populate this attribute for modifications to Namespace objects. Server-side apply is a type of patch request, so even when it is creating an object, that attribute will be populated. The difference is caused by a difference in the URL used by POST and apply-based creates. Creation via POST requests are made against the endpoint, so no attribute was populated. Namespace computed from request path: Namespace admission attributes for POST-based creates: Creation via server-side apply is done via a PATCH request to , so the attribute is populated. Namespace computed from request path: Namespace admission for apply-based creates: This is currently working as intended. If we want to make the behavior consistent between the two, I'd consider populating the namespace attribute for POST-based creation of Namespace objects.
Thanks for the thorough analysis Jordan, the problem OP is trying to solve is that they want to know if the object is namespaced or not, and used the presence of the field to detect that. According to your analysis, that seems wrong. The typical way to know if an object is namespaced is to use the discovery information, would that be the right way to solve that?
They could use discovery, or can continue to use the presence of the attribute, special-casing requests (namespaces are the only cluster-scoped resource which can have the attribute populated in AdmissionReview requests).
the documentation should be clearer, and I'd consider updating the POST-based create behavior to include this information to be consistent with update/patch/delete requests
do you want to update the doc and/or the post-based create behavior?
Sure! I am happy to take care of both.
where do you think it makes sense to add the namespace to the post-based behavior? We could add it to , but that might interfere with authorization, right? Would it be better to add the namespace directly to the ?
The info from the request body is not available when parsing requestinfo, only data in the URL. That's sort of what I expected, around the point we do this to pull the name from the incoming object:
Makes sense, I will give it a go, thank you!
that will also require sweeping admission plugins that check the namespace attribute. examples:
I think that we don't need to change anymore now. Do you know if there are other places in the docs where I should check?
yeah, I think we're good now. thanks for helping identify and clean this up

Discussion:
"//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", <ins> "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", </ins> "//staging/src/k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota:go_default_library", <ins> "//staging/src/k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1:go_default_library", </ins> "//staging/src/k8s.io/client-go/informers:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library", "//vendor/github.com/hashicorp/golang-lru:go_default_library",

"k8s.io/apiserver/pkg/admission" genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer" resourcequotaapi "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota" <ins> v1 "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1" </ins> "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/validation" quota "k8s.io/apiserver/pkg/quota/v1" "k8s.io/apiserver/pkg/quota/v1/generic"

// PluginName is a string with the name of the plugin const PluginName = "ResourceQuota" <ins> var namespaceGVK = v1.SchemeGroupVersion.WithKind("Namespace").GroupKind() </ins> // Register registers a plugin func Register(plugins *admission.Plugins) { plugins.Register(PluginName,

if attr.GetSubresource() != "" { return nil } <del> // ignore all operations that are not namespaced if attr.GetNamespace() == "" { </del> <ins> // ignore all operations that are not namespaced or creation of namespaces if attr.GetNamespace() == "" || isNamespaceCreation(attr) { </ins> return nil } return a.evaluator.Evaluate(attr) } <ins> func isNamespaceCreation(attr admission.Attributes) bool { return attr.GetOperation() == admission.Create && attr.GetKind().GroupKind() == namespaceGVK } </ins>

package resourcequota import ( <ins> "context" "errors" </ins> "testing" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" <ins> "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" v1 "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1" </ins> ) func TestPrettyPrint(t *testing.T) {

} } } <ins> type fakeEvaluator struct{} func (fakeEvaluator) Evaluate(a admission.Attributes) error { return errors.New("should not be called") } func TestExcludedOperations(t *testing.T) { a := &QuotaAdmission{ evaluator: fakeEvaluator{}, } testCases := []struct { desc string attr admission.Attributes }{ { "subresource", admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "namespace", "name", schema.GroupVersionResource{}, "subresource", admission.Create, nil, false, nil), }, { "non-namespaced resource", admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "namespace", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil), }, { "namespace creation", admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Namespace"), "namespace", "namespace", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil), }, } for _, test := range testCases { if err := a.Validate(context.TODO(), test.attr, nil); err != nil { t.Errorf("Test case: %q. Expected no error but got: %v", test.desc, err) } } } </ins>

utiltrace "k8s.io/utils/trace" ) <ins> var namespaceGVK = schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"} </ins> func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Interface, includeName bool) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { // For performance tracking purposes.

ctx, cancel := context.WithTimeout(req.Context(), timeout) defer cancel() <del> ctx = request.WithNamespace(ctx, namespace) </del> outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, scope) if err != nil { scope.err(err, w, req)

} trace.Step("Conversion done") <ins> // On create, get name from new object if unset if len(name) == 0 { _, name, _ = scope.Namer.ObjectName(obj) } if len(namespace) == 0 && *gvk == namespaceGVK { namespace = name } ctx = request.WithNamespace(ctx, namespace) </ins> ae := request.AuditEventFrom(ctx) admit = admission.WithAudit(admit, ae) audit.LogRequestObject(ae, obj, scope.Resource, scope.Subresource, scope.Serializer) userInfo, _ := request.UserFrom(ctx) <del> // On create, get name from new object if unset if len(name) == 0 { _, name, _ = scope.Namer.ObjectName(obj) } </del> trace.Step("About to store object in database") admissionAttributes := admission.NewAttributesRecord(obj, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Create, options, dryrun.IsDryRun(options.DryRun), userInfo) requestFunc := func() (runtime.Object, error) {

} } func (h *holder) expect(gvr schema.GroupVersionResource, gvk, optionsGVK schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject, options bool) { <del> // Special-case namespaces, since the object name shows up in request attributes for update/delete requests if len(namespace) == 0 && gvk.Group == "" && gvk.Version == "v1" && gvk.Kind == "Namespace" && operation != v1beta1.Create { </del> <ins> // Special-case namespaces, since the object name shows up in request attributes if len(namespace) == 0 && gvk.Group == "" && gvk.Version == "v1" && gvk.Kind == "Namespace" { </ins> namespace = name }