Skip to content

Commit 991e7a8

Browse files
authored
Merge pull request kubernetes#125646 from HirazawaUi/apply-null
Prune explicit nulls from client-side apply create
2 parents df20694 + c29a196 commit 991e7a8

File tree

5 files changed

+94
-4
lines changed

5 files changed

+94
-4
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: my-dep-null
5+
spec:
6+
selector:
7+
matchLabels:
8+
l1: l1
9+
template:
10+
metadata:
11+
labels:
12+
l1: l1
13+
spec:
14+
containers:
15+
- name: nginx
16+
image: registry.k8s.io/nginx:1.7.9
17+
resources:
18+
requests:
19+
memory: "64Mi"
20+
cpu: null
21+
terminationMessagePolicy: null
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
kind: ResourceQuota
2+
apiVersion: v1
3+
metadata:
4+
name: my-rq
5+
spec:
6+
hard:
7+
limits.cpu: null
8+
limits.memory: null

staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,10 @@ func mergeMap(original, patch map[string]interface{}, schema LookupPatchMeta, me
13611361
// original. Otherwise, check if we want to preserve it or skip it.
13621362
// Preserving the null value is useful when we want to send an explicit
13631363
// delete to the API server.
1364+
// In some cases, this may lead to inconsistent behavior with create.
1365+
// ref: https://github.com/kubernetes/kubernetes/issues/123304
1366+
// To avoid breaking compatibility,
1367+
// we made corresponding changes on the client side to ensure that the create and patch behaviors are idempotent.
13641368
if patchV == nil {
13651369
delete(original, k)
13661370
if mergeOptions.IgnoreUnmatchedNulls {

staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import (
4343
"k8s.io/client-go/util/csaupgrade"
4444
"k8s.io/component-base/version"
4545
"k8s.io/klog/v2"
46-
"k8s.io/kubectl/pkg/cmd/delete"
46+
cmddelete "k8s.io/kubectl/pkg/cmd/delete"
4747
cmdutil "k8s.io/kubectl/pkg/cmd/util"
4848
"k8s.io/kubectl/pkg/scheme"
4949
"k8s.io/kubectl/pkg/util"
@@ -61,7 +61,7 @@ type ApplyFlags struct {
6161
RecordFlags *genericclioptions.RecordFlags
6262
PrintFlags *genericclioptions.PrintFlags
6363

64-
DeleteFlags *delete.DeleteFlags
64+
DeleteFlags *cmddelete.DeleteFlags
6565

6666
FieldManager string
6767
Selector string
@@ -84,7 +84,7 @@ type ApplyOptions struct {
8484
PrintFlags *genericclioptions.PrintFlags
8585
ToPrinter func(string) (printers.ResourcePrinter, error)
8686

87-
DeleteOptions *delete.DeleteOptions
87+
DeleteOptions *cmddelete.DeleteOptions
8888

8989
ServerSideApply bool
9090
ForceConflicts bool
@@ -182,7 +182,7 @@ var ApplySetToolVersion = version.Get().GitVersion
182182
func NewApplyFlags(streams genericiooptions.IOStreams) *ApplyFlags {
183183
return &ApplyFlags{
184184
RecordFlags: genericclioptions.NewRecordFlags(),
185-
DeleteFlags: delete.NewDeleteFlags("The files that contain the configurations to apply."),
185+
DeleteFlags: cmddelete.NewDeleteFlags("The files that contain the configurations to apply."),
186186
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
187187

188188
Overwrite: true,
@@ -681,6 +681,12 @@ See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts`
681681
return cmdutil.AddSourceToErr("creating", info.Source, err)
682682
}
683683

684+
// prune nulls when client-side apply does a create to match what will happen when client-side applying an update.
685+
// do this after CreateApplyAnnotation so the annotation matches what will be persisted on an update apply of the same manifest.
686+
if u, ok := info.Object.(runtime.Unstructured); ok {
687+
pruneNullsFromMap(u.UnstructuredContent())
688+
}
689+
684690
if o.DryRunStrategy != cmdutil.DryRunClient {
685691
// Then create the resource and skip the three-way merge
686692
obj, err := helper.Create(info.Namespace, true, info.Object)
@@ -759,6 +765,29 @@ See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts`
759765
return nil
760766
}
761767

768+
func pruneNullsFromMap(data map[string]interface{}) {
769+
for k, v := range data {
770+
if v == nil {
771+
delete(data, k)
772+
} else {
773+
pruneNulls(v)
774+
}
775+
}
776+
}
777+
func pruneNullsFromSlice(data []interface{}) {
778+
for _, v := range data {
779+
pruneNulls(v)
780+
}
781+
}
782+
func pruneNulls(v interface{}) {
783+
switch v := v.(type) {
784+
case map[string]interface{}:
785+
pruneNullsFromMap(v)
786+
case []interface{}:
787+
pruneNullsFromSlice(v)
788+
}
789+
}
790+
762791
// Saves the last-applied-configuration annotation in a separate SSA field manager
763792
// to prevent it from being dropped by users who have transitioned to SSA.
764793
//

test/cmd/apply.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,34 @@ run_kubectl_apply_tests() {
9696
# cleanup
9797
kubectl delete pods selector-test-pod
9898

99+
# Create a deployment
100+
kubectl apply -f hack/testdata/null-propagation/deployment-null.yml "${kube_flags[@]:?}"
101+
# resources.limits.cpu should be nil.
102+
kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].resources.requests.cpu}" ''
103+
kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].resources.requests.memory}" '64Mi'
104+
# The default value of the terminationMessagePolicy field is `File`, so the result will not be changed.
105+
kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].terminationMessagePolicy}" 'File'
106+
107+
# kubectl apply on create should do what kubectl apply on update will accomplish.
108+
kubectl apply -f hack/testdata/null-propagation/deployment-null.yml "${kube_flags[@]}"
109+
kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].resources.requests.cpu}" ''
110+
kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].resources.requests.memory}" '64Mi'
111+
kube::test::get_object_jsonpath_assert "deployment/my-dep-null" "{.spec.template.spec.containers[0].terminationMessagePolicy}" 'File'
112+
113+
# hard.limits.cpu should be nil.
114+
kubectl apply -f hack/testdata/null-propagation/resourcesquota-null.yml "${kube_flags[@]}"
115+
kube::test::get_object_jsonpath_assert "resourcequota/my-rq" "{.spec.hard['limits\.cpu']}" ''
116+
kube::test::get_object_jsonpath_assert "resourcequota/my-rq" "{.spec.hard['limits\.memory']}" ''
117+
118+
# kubectl apply on create should do what kubectl apply on update will accomplish.
119+
kubectl apply -f hack/testdata/null-propagation/resourcesquota-null.yml "${kube_flags[@]}"
120+
kube::test::get_object_jsonpath_assert "resourcequota/my-rq" "{.spec.hard['limits\.cpu']}" ''
121+
kube::test::get_object_jsonpath_assert "resourcequota/my-rq" "{.spec.hard['limits\.memory']}" ''
122+
123+
# cleanup
124+
kubectl delete deployment my-dep-null
125+
kubectl delete resourcequota my-rq
126+
99127
## kubectl apply --dry-run=server
100128
# Pre-Condition: no POD exists
101129
kube::test::get_object_assert pods "{{range.items}}{{${id_field:?}}}:{{end}}" ''

0 commit comments

Comments
 (0)