From dd1def9d332e186ecf04ce30ed739fdd5a0f5234 Mon Sep 17 00:00:00 2001 From: Daniil Basin Date: Tue, 7 Apr 2026 09:12:51 +0500 Subject: [PATCH] Use strict minimal structure to parse last applied configuration JSON Signed-off-by: Daniil Basin --- changelogs/unreleased/9653-BassinD | 1 + pkg/restore/actions/service_action.go | 21 +++++++-------- pkg/restore/actions/service_action_test.go | 30 ++++++++++++++++++++++ 3 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 changelogs/unreleased/9653-BassinD diff --git a/changelogs/unreleased/9653-BassinD b/changelogs/unreleased/9653-BassinD new file mode 100644 index 000000000..d4eefe02a --- /dev/null +++ b/changelogs/unreleased/9653-BassinD @@ -0,0 +1 @@ +Fix service restore with null healthCheckNodePort in last-applied-configuration label diff --git a/pkg/restore/actions/service_action.go b/pkg/restore/actions/service_action.go index 9de75228e..34ba85e93 100644 --- a/pkg/restore/actions/service_action.go +++ b/pkg/restore/actions/service_action.go @@ -94,21 +94,18 @@ func deleteHealthCheckNodePort(service *corev1api.Service) error { // Search HealthCheckNodePort from server's last-applied-configuration // annotation(HealthCheckNodePort is specified by `kubectl apply` command) - lastAppliedConfig, ok := service.Annotations[annotationLastAppliedConfig] - if ok { - appliedServiceUnstructured := new(map[string]any) - if err := json.Unmarshal([]byte(lastAppliedConfig), appliedServiceUnstructured); err != nil { + if lastAppliedConfig, ok := service.Annotations[annotationLastAppliedConfig]; ok { + var appliedConfig struct { + Spec struct { + HealthCheckNodePort *int32 `json:"healthCheckNodePort"` + } `json:"spec"` + } + + if err := json.Unmarshal([]byte(lastAppliedConfig), &appliedConfig); err != nil { return errors.WithStack(err) } - healthCheckNodePort, exist, err := unstructured.NestedFloat64(*appliedServiceUnstructured, "spec", "healthCheckNodePort") - if err != nil { - return errors.WithStack(err) - } - - // Found healthCheckNodePort in lastAppliedConfig annotation, - // and the value is not 0. No need to delete, return. - if exist && healthCheckNodePort != 0 { + if appliedConfig.Spec.HealthCheckNodePort != nil && *appliedConfig.Spec.HealthCheckNodePort != 0 { return nil } } diff --git a/pkg/restore/actions/service_action_test.go b/pkg/restore/actions/service_action_test.go index 42c0d290d..f9a01d5d4 100644 --- a/pkg/restore/actions/service_action_test.go +++ b/pkg/restore/actions/service_action_test.go @@ -644,6 +644,36 @@ func TestServiceActionExecute(t *testing.T) { }, }, }, + { + name: "If PreserveNodePorts is false and HealthCheckNodePort is null in last-applied-configuration, it should not crash and the port should be cleared.", + obj: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Annotations: map[string]string{ + "kubectl.kubernetes.io/last-applied-configuration": `{"spec":{"healthCheckNodePort":null}}`, + }, + }, + Spec: corev1api.ServiceSpec{ + HealthCheckNodePort: 8080, + ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeLocal, + Type: corev1api.ServiceTypeLoadBalancer, + }, + }, + restore: builder.ForRestore(api.DefaultNamespace, "").PreserveNodePorts(false).Result(), + expectedRes: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Annotations: map[string]string{ + "kubectl.kubernetes.io/last-applied-configuration": `{"spec":{"healthCheckNodePort":null}}`, + }, + }, + Spec: corev1api.ServiceSpec{ + HealthCheckNodePort: 0, + ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeLocal, + Type: corev1api.ServiceTypeLoadBalancer, + }, + }, + }, } for _, test := range tests {