Change:
Is this a BUG REPORT or FEATURE REQUEST?: /kind bug What happened: Re-applying a NodePort service that has two ports fails and crashes the API server: What you expected to happen: Do not crash, service is configured correctly. How to reproduce it (as minimally and precisely as possible): Anything else we need to know?: Environment: Kubernetes version (use ):Cloud provider or hardware configuration: on VBox provider in macOS OS (e.g. from /etc/os-release): macOS/Minikube Kernel (e.g. ): Install tools: Others:
Seems that in func updateNodePorts, it ignored protocol when Detecting duplicate nodeport, I would have a fix on it :)
/sig network
FYI: I think this would be fixed in
/cc Maybe this is the time to have another review on and merge it :)
/assign
Fine, That makes sense

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