Change:
As per , I'm trying to remove CPU limits. But the behaviour is different depending whether the ResourceQuota exists or not: I would expect to be idempotent, and for its behaviour to result in the same end configuration regardless of the initial configuration. a new namespace named this command: that the ResourceQuota is created and is set to it again that is removed each subsequent time it's run, as long as resourcequota/compute-resource exists resourcequota/compute-resource again that the ResourceQuota is created and is set to again, and so forth the exact same behaviour if is changed to No response <details</details<detailsOpenShift 4.13.12 </details<details</details<details</details<details</details<details</details
/sig node
Tried to replicate the issue and observed the same behaviour.
/remove-sig node /sig api-machinery /cc
What's the goal of applying here? What are you trying to achieve? I agree that this is not the right behavior though.
I'm trying to remove a previously set property. I cannot remove the ResourceQuota altogether because it contains other properties (which I left out of my example) that I do not wish to remove.
But 0 is the same as unset, isn't it?
No, if I set it to 0 then any deployment with set to higher than 0 will fail to create any pods because it will exceed the quota.
/triage accepted /cc
/assign
Hello, I'm having a similar issue. Here's my use case: We use Flux to deploy things on our cluster - in particular, we use the component and set chart values in . Some of the charts we deploy have default values we want to remove, so we want to set a value to Editing an HelmRelease with this kind of ajustments drops the null keys, so default values end up being setup again and generated manifests are invalid :( Let me know if you need anything else to keep this issue moving!
I think the correct behavior is to set the struct to the default value when or , because in the standard library does this as well, and I think we expect the result to be as similar as possible to . So our real issue here is that we didn't set the to its default value when patch.
Based on the above, I think this question is not related to the current issue.
Tracing it back to , maybe the current result is what we want. The explicit null values means that we want to remove it in patch. As mentioned , our behavior in create is also as expected. I'm not quite sure if we actually want to fix it, and how /cc I hope to get your advice as you are the implementer of
I think client-side on create should do what on update will accomplish, namely, explicit nulls in the manifest are treated as the removal of the attributes they are values of. Proof of concept opened at , I'd be happy for someone to pick that up, add test coverage, and work through any failing tests that change uncovers
Thank you, but I still have a question. If we solve the difference between create and patch behaviors in ApiServer on the client side, will third-party callers still have the same problem? Or should we not cover this scenario?
I would consider changing behavior of null in a create request server-side to be a compatibility break. values are defined for client-side apply to mean "remove the key associated with this value". I think fixing the client-side apply creation case to have the same semantic is the most narrowly scoped fix we can make while preserving compatible server-side behavior generally.
Okay, I got it, I'll follow up with

Discussion:
<ins> apiVersion: apps/v1 kind: Deployment metadata: name: my-dep-null spec: selector: matchLabels: l1: l1 template: metadata: labels: l1: l1 spec: containers: - name: nginx image: registry.k8s.io/nginx:1.7.9 resources: requests: memory: "64Mi" cpu: null terminationMessagePolicy: null </ins>

<ins> kind: ResourceQuota apiVersion: v1 metadata: name: my-rq spec: hard: limits.cpu: null limits.memory: null </ins>

// original. Otherwise, check if we want to preserve it or skip it. // Preserving the null value is useful when we want to send an explicit // delete to the API server. <ins> // In some cases, this may lead to inconsistent behavior with create. // ref: https://github.com/kubernetes/kubernetes/issues/123304 // To avoid breaking compatibility, // we made corresponding changes on the client side to ensure that the create and patch behaviors are idempotent. </ins> if patchV == nil { delete(original, k) if mergeOptions.IgnoreUnmatchedNulls {

"k8s.io/client-go/util/csaupgrade" "k8s.io/component-base/version" "k8s.io/klog/v2" <del> "k8s.io/kubectl/pkg/cmd/delete" </del> <ins> cmddelete "k8s.io/kubectl/pkg/cmd/delete" </ins> cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util"

RecordFlags *genericclioptions.RecordFlags PrintFlags  *genericclioptions.PrintFlags <del> DeleteFlags *delete.DeleteFlags </del> <ins> DeleteFlags *cmddelete.DeleteFlags </ins> FieldManager   string Selector       string

PrintFlags *genericclioptions.PrintFlags ToPrinter  func(string) (printers.ResourcePrinter, error) <del> DeleteOptions *delete.DeleteOptions </del> <ins> DeleteOptions *cmddelete.DeleteOptions </ins> ServerSideApply bool ForceConflicts  bool

func NewApplyFlags(streams genericiooptions.IOStreams) *ApplyFlags { return &ApplyFlags{ RecordFlags: genericclioptions.NewRecordFlags(), <del> DeleteFlags: delete.NewDeleteFlags("The files that contain the configurations to apply."), </del> <ins> DeleteFlags: cmddelete.NewDeleteFlags("The files that contain the configurations to apply."), </ins> PrintFlags:  genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), Overwrite:    true,

return cmdutil.AddSourceToErr("creating", info.Source, err) } <ins> // prune nulls when client-side apply does a create to match what will happen when client-side applying an update. // do this after CreateApplyAnnotation so the annotation matches what will be persisted on an update apply of the same manifest. if u, ok := info.Object.(runtime.Unstructured); ok { pruneNullsFromMap(u.UnstructuredContent()) } </ins> if o.DryRunStrategy != cmdutil.DryRunClient { // Then create the resource and skip the three-way merge obj, err := helper.Create(info.Namespace, true, info.Object)

return nil } <ins> func pruneNullsFromMap(data map[string]interface{}) { for k, v := range data { if v == nil { delete(data, k) } else { pruneNulls(v) } } } func pruneNullsFromSlice(data []interface{}) { for _, v := range data { pruneNulls(v) } } func pruneNulls(v interface{}) { switch v := v.(type) { case map[string]interface{}: pruneNullsFromMap(v) case []interface{}: pruneNullsFromSlice(v) } } </ins> // Saves the last-applied-configuration annotation in a separate SSA field manager // to prevent it from being dropped by users who have transitioned to SSA. //

# cleanup kubectl delete pods selector-test-pod <ins> # Create a deployment kubectl apply -f hack/testdata/null-propagation/deployment-null.yml "${kube_flags[@]:?}" # resources.limits.cpu should be nil. kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].resources.requests.cpu}" '' kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].resources.requests.memory}" '64Mi' # The default value of the terminationMessagePolicy field is `File`, so the result will not be changed. kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].terminationMessagePolicy}" 'File' # kubectl apply on create should do what kubectl apply on update will accomplish. kubectl apply -f hack/testdata/null-propagation/deployment-null.yml "${kube_flags[@]}" kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].resources.requests.cpu}" '' kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].resources.requests.memory}" '64Mi' kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].terminationMessagePolicy}" 'File' # hard.limits.cpu should be nil. kubectl apply -f hack/testdata/null-propagation/resourcesquota-null.yml "${kube_flags[@]}" kube::test::get_object_jsonpath_assert  "resourcequota/my-rq" "{.spec.hard['limits.cpu']}" '' kube::test::get_object_jsonpath_assert  "resourcequota/my-rq" "{.spec.hard['limits.memory']}" '' # kubectl apply on create should do what kubectl apply on update will accomplish. kubectl apply -f hack/testdata/null-propagation/resourcesquota-null.yml "${kube_flags[@]}" kube::test::get_object_jsonpath_assert "resourcequota/my-rq" "{.spec.hard['limits.cpu']}" '' kube::test::get_object_jsonpath_assert  "resourcequota/my-rq" "{.spec.hard['limits.memory']}" '' # cleanup kubectl delete deployment my-dep-null kubectl delete resourcequota my-rq </ins> ## kubectl apply --dry-run=server # Pre-Condition: no POD exists kube::test::get_object_assert pods "{{range.items}}{{${id_field:?}}}:{{end}}" ''