Change:
<!-- This form is for bug reports and feature requests ONLY! If you're looking for help check and the . --Is this a BUG REPORT or FEATURE REQUEST?: /kind bug /sig network /cc /assign What happened: a ClusterIP type service the service. Change it's type to NodePort and add a Port. Note: the new port has same port number but protocol with the existing Port. the updated service. The TCP Port was eaten. What you expected to happen: The updated service has both TCP and UDP ports. How to reproduce it (as minimally and precisely as possible): See the step above. Anything else we need to know?: If I create a NodePort service with the above two ports, it works. I write a test case to verify this case. I got a panic. The root cause is as below. We should take another look at this logic. BTW: changed some related code but it doesn't change the logic here. So I believe this bug is not introduced by . I also tested this on v1.5.1, it has the same issue. Environment: Kubernetes version (use ): HEAD Cloud provider or hardware configuration**: OS (e.g. from /etc/os-release): Kernel (e.g. ): Install tools: Others:
I will be OOO the next week. Since this bug is not so urgent, I will fix it ASAP when I'm back. Folks who are interested in this can take it :) BTW, I can join in the discussion from my phone.
I'm not sure this is a bug. In the first configuration yaml, you're defining the ports array twice. I'm the second, you're only defining it once. The ports array should only be defined once.
Did you mean the below yaml by ? Please note that the two ports have same port number but different protocols. This is allowed.
Yes, that's the one I meant. But note you have "ports:" twice.
Ah, thanks for pointing it out! This might be the cause. But I remembered I also tested the case without the second line, it had the same issue. I can't verify this now. Will test it when I'm back to my keyboard.
I try the case without the second line, the issue also exists. So I believe there is a bug here. Seems like there might be two issues here: a port is lost as is described in this issue. we shouldn't allow to configure two lines in the manifest. i.e. we should add validation for this case.
/remove-priority awaiting-more-evidence
can you please provide (or edit) example YAMLs then to allow for attempts at duplication?
Update: reproducing without the extra line. a ClusterIP type service the service (). Change it's type to NodePort and add a Port which has the same port number but different protocol with the existing Port. the updated service. We can see that the UDP Port was not created. I have dug into the code and will send a patch to fix it.
PR sent. Reviews welcome! Thanks!
There are/were bugs in the patch implementation wrt ports, because they need compound merge keys.
Yeah I had some discussios with in . But we also do have a bug in our service updating codes. See:

Discussion:
proxyTransport   http.RoundTripper } <ins> // ServiceNodePort includes protocol and port number of a service NodePort. type ServiceNodePort struct { // The IP protocol for this port. Supports "TCP" and "UDP". Protocol api.Protocol // The port on each node on which this service is exposed. // Default is to auto-allocate a port if the ServiceType of this Service requires one. NodePort int32 } </ins> // NewStorage returns a new REST. func NewStorage(registry Registry, endpoints endpoint.Registry, serviceIPs ipallocator.Interface, serviceNodePorts portallocator.Interface, proxyTransport http.RoundTripper) *ServiceRest {

// This is O(N), but we expect haystack to be small; // so small that we expect a linear search to be faster <del> func contains(haystack []int, needle int) bool { </del> <ins> func containsNumber(haystack []int, needle int) bool { </ins> for _, v := range haystack { if v == needle { return true

return false } <ins> // This is O(N), but we expect serviceNodePorts to be small; // so small that we expect a linear search to be faster func containsNodePort(serviceNodePorts []ServiceNodePort, serviceNodePort ServiceNodePort) bool { for _, snp := range serviceNodePorts { if snp == serviceNodePort { return true } } return false } </ins> func CollectServiceNodePorts(service *api.Service) []int { servicePorts := []int{} for i := range service.Spec.Ports {

} func (rs *REST) updateNodePorts(oldService, newService *api.Service, nodePortOp *portallocator.PortAllocationOperation) error { <del> oldNodePorts := CollectServiceNodePorts(oldService) </del> <ins> oldNodePortsNumbers := CollectServiceNodePorts(oldService) newNodePorts := []ServiceNodePort{} portAllocated := map[int]bool{} </ins> <del> newNodePorts := []int{} </del> for i := range newService.Spec.Ports { servicePort := &newService.Spec.Ports[i] <del> nodePort := int(servicePort.NodePort) if nodePort != 0 { if !contains(oldNodePorts, nodePort) { err := nodePortOp.Allocate(nodePort) </del> <ins> nodePort := ServiceNodePort{Protocol: servicePort.Protocol, NodePort: servicePort.NodePort} if nodePort.NodePort != 0 { if !containsNumber(oldNodePortsNumbers, int(nodePort.NodePort)) && !portAllocated[int(nodePort.NodePort)] { err := nodePortOp.Allocate(int(nodePort.NodePort)) </ins> if err != nil { <del> el := field.ErrorList{field.Invalid(field.NewPath("spec", "ports").Index(i).Child("nodePort"), nodePort, err.Error())} </del> <ins> el := field.ErrorList{field.Invalid(field.NewPath("spec", "ports").Index(i).Child("nodePort"), nodePort.NodePort, err.Error())} </ins> return errors.NewInvalid(api.Kind("Service"), newService.Name, el) } <ins> portAllocated[int(nodePort.NodePort)] = true </ins> } } else { <del> nodePort, err := nodePortOp.AllocateNext() </del> <ins> nodePortNumber, err := nodePortOp.AllocateNext() </ins> if err != nil { // TODO: what error should be returned here?  It's not a // field-level validation failure (the field is valid), and it's // not really an internal error. return errors.NewInternalError(fmt.Errorf("failed to allocate a nodePort: %v", err)) } <del> servicePort.NodePort = int32(nodePort) </del> <ins> servicePort.NodePort = int32(nodePortNumber) nodePort.NodePort = servicePort.NodePort </ins> } <del> // Detect duplicate node ports; this should have been caught by validation, so we panic if contains(newNodePorts, nodePort) { panic("duplicate node port") </del> <ins> if containsNodePort(newNodePorts, nodePort) { return fmt.Errorf("duplicate nodePort: %v", nodePort) </ins> } newNodePorts = append(newNodePorts, nodePort) } <ins> newNodePortsNumbers := CollectServiceNodePorts(newService) </ins> // The comparison loops are O(N^2), but we don't expect N to be huge // (there's a hard-limit at 2^16, because they're ports; and even 4 ports would be a lot) <del> for _, oldNodePort := range oldNodePorts { if contains(newNodePorts, oldNodePort) { </del> <ins> for _, oldNodePortNumber := range oldNodePortsNumbers { if containsNumber(newNodePortsNumbers, oldNodePortNumber) { </ins> continue } <del> nodePortOp.ReleaseDeferred(oldNodePort) </del> <ins> nodePortOp.ReleaseDeferred(int(oldNodePortNumber)) </ins> } return nil

}, expectSpecifiedNodePorts: []int{}, }, <ins> { name: "Add new ServicePort with a different protocol without changing port numbers", oldService: &api.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ Selector:        map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, Type:            api.ServiceTypeNodePort, Ports: []api.ServicePort{ { Name:       "port-tcp", Port:       53, TargetPort: intstr.FromInt(6502), Protocol:   api.ProtocolTCP, NodePort:   30053, }, }, }, }, newService: &api.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ Selector:        map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, Type:            api.ServiceTypeNodePort, Ports: []api.ServicePort{ { Name:       "port-tcp", Port:       53, TargetPort: intstr.FromInt(6502), Protocol:   api.ProtocolTCP, NodePort:   30053, }, { Name:       "port-udp", Port:       53, TargetPort: intstr.FromInt(6502), Protocol:   api.ProtocolUDP, NodePort:   30053, }, }, }, }, expectSpecifiedNodePorts: []int{30053, 30053}, }, { name: "Change service type from ClusterIP to NodePort with same NodePort number but different protocols", oldService: &api.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ Selector:        map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, Type:            api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port:       53, Protocol:   api.ProtocolTCP, TargetPort: intstr.FromInt(6502), }}, }, }, newService: &api.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ Selector:        map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, Type:            api.ServiceTypeNodePort, Ports: []api.ServicePort{ { Name:       "port-tcp", Port:       53, TargetPort: intstr.FromInt(6502), Protocol:   api.ProtocolTCP, NodePort:   30053, }, { Name:       "port-udp", Port:       53, TargetPort: intstr.FromInt(6502), Protocol:   api.ProtocolUDP, NodePort:   30053, }, }, }, }, expectSpecifiedNodePorts: []int{30053, 30053}, }, </ins> } for _, test := range testCases {

} }) <ins> It("should be able to update NodePorts with two same port numbers but different protocols", func() { serviceName := "nodeport-update-service" ns := f.Namespace.Name jig := framework.NewServiceTestJig(cs, serviceName) By("creating a TCP service " + serviceName + " with type=ClusterIP in namespace " + ns) tcpService := jig.CreateTCPServiceOrFail(ns, nil) defer func() { framework.Logf("Cleaning up the updating NodePorts test service") err := cs.Core().Services(ns).Delete(serviceName, nil) Expect(err).NotTo(HaveOccurred()) }() jig.SanityCheckService(tcpService, v1.ServiceTypeClusterIP) svcPort := int(tcpService.Spec.Ports[0].Port) framework.Logf("service port TCP: %d", svcPort) // Change the services to NodePort and add a UDP port. By("changing the TCP service to type=NodePort and add a UDP port") newService := jig.UpdateServiceOrFail(ns, tcpService.Name, func(s *v1.Service) { s.Spec.Type = v1.ServiceTypeNodePort s.Spec.Ports = []v1.ServicePort{ { Name:     "tcp-port", Port:     80, Protocol: v1.ProtocolTCP, }, { Name:     "udp-port", Port:     80, Protocol: v1.ProtocolUDP, }, } }) jig.SanityCheckService(newService, v1.ServiceTypeNodePort) if len(newService.Spec.Ports) != 2 { framework.Failf("new service should have two Ports") } for _, port := range newService.Spec.Ports { if port.NodePort == 0 { framework.Failf("new service failed to allocate NodePort for Port %s", port.Name) } framework.Logf("new service allocates NodePort %d for Port %s", port.NodePort, port.Name) } }) </ins> It("should be able to change the type from ExternalName to ClusterIP", func() { serviceName := "externalname-service" ns := f.Namespace.Name