Repository: kubernetes/kubernetes (language: go)

Issue:
Using server-side-apply for an Ingress failed when updating IngressBackend from port number to port name
### What happened?

I created an Ingress resource with a port number manually, and then tried to use an Operator with server-side apply to update the resource from a port number to a port name. This failed with the error message "cannot set both port name & port number".

### What did you expect to happen?

I expected the port number to be cleared as the operator would take over the ownership of the ServiceBackendPort. 

### How can we reproduce it (as minimally and precisely as possible)?

To simulate the behavior in the operator in a most simple example I use `kubectl`:

ingress-v1.yaml:

```
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
  name: myingress
spec:
  defaultBackend:
    service:
      name: myservice
      port:
        number: 8443
```

ingress-v2.yaml:

```
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
  name: myingress
spec:
  defaultBackend:
    service:
      name: myservice
      port:
        name: http
```

```
kubectl apply -f  ingress-v1.yaml --server-side --field-manager=app-1 --validate=false
kubectl apply -f  ingress-v2.yaml --server-side --field-manager=app-2 --validate=false  --force-conflicts 
```

This will print: 

> The Ingress "myingress" is invalid: spec.defaultBackend: Invalid value: "": cannot set both port name & port number


### Anything else we need to know?

It seems to me that the merge type for the struct ServiceBackendPort should to be set to "atomic" [according to this doc](https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy), so that the Operator updating the port  should take over the whole struct, and two applications shouldn't update single fields in it.

https://github.com/kubernetes/kubernetes/blob/0cc57258c3f8545c8250f0e7a1307fd01b0d283d/staging/src/k8s.io/api/networking/v1/types.go#L523-L544

### Kubernetes version

```
$ kubectl version
Client Version: v1.30.2
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
Server Version: v1.28.9+416ecaf
```

### Cloud provider

Red Hat OpenShift on AWS


### OS version

OS version of the client:

NAME="Fedora Linux"
VERSION="40 (Workstation Edition)"

Linux fedora.fritz.box 6.9.6-200.fc40.x86_64 #1 SMP PREEMPT_DYNAMIC Fri Jun 21 15:48:21 UTC 2024 x86_64 GNU/Linux

### Install tools

-

### Container runtime (CRI) and version (if applicable)

-

### Related plugins (CNI, CSI, ...) and versions (if applicable)

-


Discussion / hints:
/sig network
So, on one hand you made a problem when you used two different owners to manage one aspect of the object.  On the other hand, I think you're right that it SHOULD work.  Probably. 
 `+structType` is not a super common tag, and ingress way predates it, so no surprise it isn't used.

I ACK the issue and reproduced it.  

```
$ git diff
diff --git a/staging/src/k8s.io/api/networking/v1/types.go b/staging/src/k8s.io/api/networking/v1/types.go
index 4ae0be1a366..5db47530fe9 100644
--- a/staging/src/k8s.io/api/networking/v1/types.go
+++ b/staging/src/k8s.io/api/networking/v1/types.go
@@ -531,6 +531,7 @@ type IngressServiceBackend struct {
 }
 
 // ServiceBackendPort is the service port being referenced.
+// +structType=atomic
 type ServiceBackendPort struct {
        // name is the name of the port on the Service.
        // This is a mutually exclusive setting with "Number".
```

I ran update-codegen and built a KinD cluster from HEAD.

```
$ k apply -f /tmp/1.yaml --server-side --field-manager=app-1 --validate=false -o yaml --show-managed-fields
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  creationTimestamp: "2024-07-18T17:28:03Z"
  generation: 1
  managedFields:
  - apiVersion: networking.k8s.io/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:spec:
        f:defaultBackend:
          f:service:
            f:name: {}
            f:port: {}
    manager: app-1
    operation: Apply
    time: "2024-07-18T17:28:03Z"
  name: myingress
  namespace: default
  resourceVersion: "483"
  uid: eb2741c9-69cc-4626-a22f-fe1caca9607f
spec:
  defaultBackend:
    service:
      name: myservice
      port:
        number: 8443
status:
  loadBalancer: {}

$ k apply -f /tmp/2.yaml --server-side --field-manager=app-2 --validate=false -o yaml --show-managed-fields --force-conflicts
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  creationTimestamp: "2024-07-18T17:28:03Z"
  generation: 2
  managedFields:
  - apiVersion: networking.k8s.io/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:spec:
        f:defaultBackend:
          f:service:
            f:name: {}
    manager: app-1
    operation: Apply
    time: "2024-07-18T17:28:03Z"
  - apiVersion: networking.k8s.io/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:spec:
        f:defaultBackend:
          f:service:
            f:name: {}
            f:port: {}
    manager: app-2
    operation: Apply
    time: "2024-07-18T17:28:28Z"
  name: myingress
  namespace: default
  resourceVersion: "517"
  uid: eb2741c9-69cc-4626-a22f-fe1caca9607f
spec:
  defaultBackend:
    service:
      name: myservice
      port:
        name: http
status:
  loadBalancer: {}
```
@pohly this may be something you want to look at for DRA API - I didn't even realize this was a thing until today :)

Fix patch:
diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json
index c49e5f4971b67..29929108f4f0a 100644
--- a/api/openapi-spec/swagger.json
+++ b/api/openapi-spec/swagger.json
@@ -14052,7 +14052,8 @@
           "type": "integer"
         }
       },
-      "type": "object"
+      "type": "object",
+      "x-kubernetes-map-type": "atomic"
     },
     "io.k8s.api.networking.v1beta1.IPAddress": {
       "description": "IPAddress represents a single IP of a single IP Family. The object is designed to be used by APIs that operate on IP addresses. The object is used by the Service core API for allocation of IP addresses. An IP address can be represented in different formats, to guarantee the uniqueness of the IP, the name of the object is the IP address in canonical format, four decimal digits separated by dots suppressing leading zeros for IPv4 and the representation defined by RFC 5952 for IPv6. Valid: 192.168.1.5 or 2001:db8::1 or 2001:db8:aaaa:bbbb:cccc:dddd:eeee:1 Invalid: 10.01.2.3 or 2001:db8:0:0:0::1",
diff --git a/api/openapi-spec/v3/apis__networking.k8s.io__v1_openapi.json b/api/openapi-spec/v3/apis__networking.k8s.io__v1_openapi.json
index b33cc9dae024f..84c0d47f355e2 100644
--- a/api/openapi-spec/v3/apis__networking.k8s.io__v1_openapi.json
+++ b/api/openapi-spec/v3/apis__networking.k8s.io__v1_openapi.json
@@ -799,7 +799,8 @@
             "type": "integer"
           }
         },
-        "type": "object"
+        "type": "object",
+        "x-kubernetes-map-type": "atomic"
       },
       "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": {
         "description": "APIResource specifies the name of a resource and whether it is namespaced.",
diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go
index 3b8f37cce77bf..aad8566bac817 100644
--- a/pkg/generated/openapi/zz_generated.openapi.go
+++ b/pkg/generated/openapi/zz_generated.openapi.go
@@ -40804,6 +40804,11 @@ func schema_k8sio_api_networking_v1_ServiceBackendPort(ref common.ReferenceCallb
 					},
 				},
 			},
+			VendorExtensible: spec.VendorExtensible{
+				Extensions: spec.Extensions{
+					"x-kubernetes-map-type": "atomic",
+				},
+			},
 		},
 	}
 }
diff --git a/staging/src/k8s.io/api/networking/v1/generated.proto b/staging/src/k8s.io/api/networking/v1/generated.proto
index 7188aa6d9fb53..c72fdc8f3795e 100644
--- a/staging/src/k8s.io/api/networking/v1/generated.proto
+++ b/staging/src/k8s.io/api/networking/v1/generated.proto
@@ -541,6 +541,7 @@ message NetworkPolicySpec {
 }
 
 // ServiceBackendPort is the service port being referenced.
+// +structType=atomic
 message ServiceBackendPort {
   // name is the name of the port on the Service.
   // This is a mutually exclusive setting with "Number".
diff --git a/staging/src/k8s.io/api/networking/v1/types.go b/staging/src/k8s.io/api/networking/v1/types.go
index 4ae0be1a3667b..5db47530fe902 100644
--- a/staging/src/k8s.io/api/networking/v1/types.go
+++ b/staging/src/k8s.io/api/networking/v1/types.go
@@ -531,6 +531,7 @@ type IngressServiceBackend struct {
 }
 
 // ServiceBackendPort is the service port being referenced.
+// +structType=atomic
 type ServiceBackendPort struct {
 	// name is the name of the port on the Service.
 	// This is a mutually exclusive setting with "Number".
diff --git a/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go b/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go
index 0b04331415e72..98e8dcef938b6 100644
--- a/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go
+++ b/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go
@@ -10977,6 +10977,7 @@ var schemaYAML = typed.YAMLObject(`types:
     - name: number
       type:
         scalar: numeric
+    elementRelationship: atomic
 - name: io.k8s.api.networking.v1alpha1.IPAddress
   map:
     fields:


Test patch: