Skip to content

kubectl diff --server-side does not detect deletions #1796

@gnunn1

Description

@gnunn1

What happened:

The command kubectl diff --server-side fails to detect fields that are removed from the manifest even though kubectl apply reconciles the removal correctly. Using kubectl diff without the --server-side switch works fine since it leverages the last-applied-configuration annotation.

What you expected to happen:

The kubectl diff --server-side command should align with kubectl apply --server-side which AFAIK uses the structured-merge-diff library to do its work. Otherwise the diff is generating output that is IMHO errorneous and goes against user expectations that diff should output what the effects of an apply will be.

Ideally diff --server-side should use the structured-merge-diff library rather then relying on an alternate way to generate the diff.

How to reproduce it (as minimally and precisely as possible):

Save this deployment as a file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: basic-app
  labels:
    test: value
  annotations:
    test: value
    test2: value
spec:
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: basic-app
  template:
    metadata:
      labels:
        app: basic-app
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: node-role.kubernetes.io/worker
                    operator: In
                    values:
                      - ''
                  - key: kubernetes.io/os
                    operator: In
                    values:
                      - 'linux'
      containers:
      - name: basic-app
        image: busybox:latest # A simple image for demonstration
        command: ["sh", "-c", "echo $APP_MESSAGE && echo 'Application running on port:' $APP_PORT && sleep 3600"]
        env:
        - name: TEST_VAR
          value: value
        - name: TEST_VAR2
          value: value2

And then apply it to the cluster:

kubectl apply -f deployment.yaml

Remove the affinity section from the manifest (but note it is the same issue regardless of what you remove) and then diff it with server side:

$ kubectl diff -f deployment.yaml --server-side
diff -u -N /tmp/LIVE-486847088/apps.v1.Deployment.diff-bug.basic-app /tmp/MERGED-2682306472/apps.v1.Deployment.diff-bug.basic-app
--- /tmp/LIVE-486847088/apps.v1.Deployment.diff-bug.basic-app   2025-11-05 13:18:43.473757610 -0800
+++ /tmp/MERGED-2682306472/apps.v1.Deployment.diff-bug.basic-app        2025-11-05 13:18:43.474757618 -0800
@@ -4,11 +4,11 @@
   annotations:
     deployment.kubernetes.io/revision: "1"
     kubectl.kubernetes.io/last-applied-configuration: |
-      {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"test":"value","test2":"value"},"labels":{"test":"value"},"name":"basic-app","namespace":"diff-bug"},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"basic-app"}},"template":{"metadata":{"labels":{"app":"basic-app"}},"spec":{"affinity":{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"node-role.kubernetes.io/worker","operator":"In","values":[""]},{"key":"kubernetes.io/os","operator":"In","values":["linux"]}]}]}}},"containers":[{"command":["sh","-c","echo $APP_MESSAGE \u0026\u0026 echo 'Application running on port:' $APP_PORT \u0026\u0026 sleep 3600"],"env":[{"name":"TEST_VAR","value":"value"},{"name":"TEST_VAR2","value":"value2"}],"image":"busybox:latest","name":"basic-app"}]}}}}
+      {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"test":"value","test2":"value"},"labels":{"test":"value"},"name":"basic-app","namespace":"diff-bug"},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"basic-app"}},"template":{"metadata":{"labels":{"app":"basic-app"}},"spec":{"containers":[{"command":["sh","-c","echo $APP_MESSAGE \u0026\u0026 echo 'Application running on port:' $APP_PORT \u0026\u0026 sleep 3600"],"env":[{"name":"TEST_VAR","value":"value"},{"name":"TEST_VAR2","value":"value2"}],"image":"busybox:latest","name":"basic-app"}]}}}}
     test: value
     test2: value
   creationTimestamp: "2025-11-05T21:18:15Z"
-  generation: 1
+  generation: 2
   labels:
     test: value
   name: basic-app

The diff fails to show the removed affinity. Diff'ing this using client-side diff works fine since it leverages the last-applied-configuration. Also running kubectl apply -f deployment.yaml updates the manifest as expected and removes the affinity section using either client-side or server-side apply.

Anything else we need to know?:

Tools like Argo CD rely on the way Kubernetes handles the diff and this dissonance between diff and apply causes these tools to fail to report differences as expected to their users.

Environment:

  • Kubernetes client and server versions (use kubectl version):
Client Version: v1.34.1
Kustomize Version: v5.7.1
Server Version: v1.33.5
  • Cloud provider or hardware configuration:

on-prem

  • OS (e.g: cat /etc/os-release):
NAME="Arch Linux"
PRETTY_NAME="Arch Linux"
ID=arch
BUILD_ID=rolling
ANSI_COLOR="38;2;23;147;209"
HOME_URL="https://archlinux.org/"
DOCUMENTATION_URL="https://wiki.archlinux.org/"
SUPPORT_URL="https://bbs.archlinux.org/"
BUG_REPORT_URL="https://gitlab.archlinux.org/groups/archlinux/-/issues"
PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/"
LOGO=archlinux-logo

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/bugCategorizes issue or PR as related to a bug.needs-triageIndicates an issue or PR lacks a `triage/foo` label and requires one.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions