func (c *Cloud) ensureLoadBalancer(namespacedName types.NamespacedName, loadBalancerName string, listeners []*elb.Listener, subnetIDs []string, securityGroupIDs []string, internalELB, proxyProtocol bool) (*elb.LoadBalancerDescription, error) { loadBalancer, err := c.describeLoadBalancer(loadBalancerName) if err != nil { return nil, err } dirty := false if loadBalancer == nil { createRequest := &elb.CreateLoadBalancerInput{} createRequest.LoadBalancerName = aws.String(loadBalancerName) createRequest.Listeners = listeners if internalELB { createRequest.Scheme = aws.String("internal") } // We are supposed to specify one subnet per AZ. // TODO: What happens if we have more than one subnet per AZ? createRequest.Subnets = stringPointerArray(subnetIDs) createRequest.SecurityGroups = stringPointerArray(securityGroupIDs) createRequest.Tags = []*elb.Tag{ {Key: aws.String(TagNameKubernetesCluster), Value: aws.String(c.getClusterName())}, {Key: aws.String(TagNameKubernetesService), Value: aws.String(namespacedName.String())}, } glog.Infof("Creating load balancer for %v with name: ", namespacedName, loadBalancerName) _, err := c.elb.CreateLoadBalancer(createRequest) if err != nil { return nil, err } if proxyProtocol { err = c.createProxyProtocolPolicy(loadBalancerName) if err != nil { return nil, err } for _, listener := range listeners { glog.V(2).Infof("Adjusting AWS loadbalancer proxy protocol on node port %d. Setting to true", *listener.InstancePort) err := c.setBackendPolicies(loadBalancerName, *listener.InstancePort, []*string{aws.String(ProxyProtocolPolicyName)}) if err != nil { return nil, err } } } dirty = true } else { // TODO: Sync internal vs non-internal { // Sync subnets expected := sets.NewString(subnetIDs...) actual := stringSetFromPointers(loadBalancer.Subnets) additions := expected.Difference(actual) removals := actual.Difference(expected) if removals.Len() != 0 { request := &elb.DetachLoadBalancerFromSubnetsInput{} request.LoadBalancerName = aws.String(loadBalancerName) request.Subnets = stringSetToPointers(removals) glog.V(2).Info("Detaching load balancer from removed subnets") _, err := c.elb.DetachLoadBalancerFromSubnets(request) if err != nil { return nil, fmt.Errorf("error detaching AWS loadbalancer from subnets: %v", err) } dirty = true } if additions.Len() != 0 { request := &elb.AttachLoadBalancerToSubnetsInput{} request.LoadBalancerName = aws.String(loadBalancerName) request.Subnets = stringSetToPointers(additions) glog.V(2).Info("Attaching load balancer to added subnets") _, err := c.elb.AttachLoadBalancerToSubnets(request) if err != nil { return nil, fmt.Errorf("error attaching AWS loadbalancer to subnets: %v", err) } dirty = true } } { // Sync security groups expected := sets.NewString(securityGroupIDs...) actual := stringSetFromPointers(loadBalancer.SecurityGroups) if !expected.Equal(actual) { // This call just replaces the security groups, unlike e.g. subnets (!) request := &elb.ApplySecurityGroupsToLoadBalancerInput{} request.LoadBalancerName = aws.String(loadBalancerName) request.SecurityGroups = stringPointerArray(securityGroupIDs) glog.V(2).Info("Applying updated security groups to load balancer") _, err := c.elb.ApplySecurityGroupsToLoadBalancer(request) if err != nil { return nil, fmt.Errorf("error applying AWS loadbalancer security groups: %v", err) } dirty = true } } { // Sync listeners listenerDescriptions := loadBalancer.ListenerDescriptions foundSet := make(map[int]bool) removals := []*int64{} for _, listenerDescription := range listenerDescriptions { actual := listenerDescription.Listener if actual == nil { glog.Warning("Ignoring empty listener in AWS loadbalancer: ", loadBalancerName) continue } found := -1 for i, expected := range listeners { if orEmpty(actual.Protocol) != orEmpty(expected.Protocol) { continue } if orEmpty(actual.InstanceProtocol) != orEmpty(expected.InstanceProtocol) { continue } if orZero(actual.InstancePort) != orZero(expected.InstancePort) { continue } if orZero(actual.LoadBalancerPort) != orZero(expected.LoadBalancerPort) { continue } if orEmpty(actual.SSLCertificateId) != orEmpty(expected.SSLCertificateId) { continue } found = i } if found != -1 { foundSet[found] = true } else { removals = append(removals, actual.LoadBalancerPort) } } additions := []*elb.Listener{} for i := range listeners { if foundSet[i] { continue } additions = append(additions, listeners[i]) } if len(removals) != 0 { request := &elb.DeleteLoadBalancerListenersInput{} request.LoadBalancerName = aws.String(loadBalancerName) request.LoadBalancerPorts = removals glog.V(2).Info("Deleting removed load balancer listeners") _, err := c.elb.DeleteLoadBalancerListeners(request) if err != nil { return nil, fmt.Errorf("error deleting AWS loadbalancer listeners: %v", err) } dirty = true } if len(additions) != 0 { request := &elb.CreateLoadBalancerListenersInput{} request.LoadBalancerName = aws.String(loadBalancerName) request.Listeners = additions glog.V(2).Info("Creating added load balancer listeners") _, err := c.elb.CreateLoadBalancerListeners(request) if err != nil { return nil, fmt.Errorf("error creating AWS loadbalancer listeners: %v", err) } dirty = true } } { // Sync proxy protocol state for new and existing listeners proxyPolicies := make([]*string, 0) if proxyProtocol { // Ensure the backend policy exists // NOTE The documentation for the AWS API indicates we could get an HTTP 400 // back if a policy of the same name already exists. However, the aws-sdk does not // seem to return an error to us in these cases. Therefore this will issue an API // request every time. err := c.createProxyProtocolPolicy(loadBalancerName) if err != nil { return nil, err } proxyPolicies = append(proxyPolicies, aws.String(ProxyProtocolPolicyName)) } foundBackends := make(map[int64]bool) proxyProtocolBackends := make(map[int64]bool) for _, backendListener := range loadBalancer.BackendServerDescriptions { foundBackends[*backendListener.InstancePort] = false proxyProtocolBackends[*backendListener.InstancePort] = proxyProtocolEnabled(backendListener) } for _, listener := range listeners { setPolicy := false instancePort := *listener.InstancePort if currentState, ok := proxyProtocolBackends[instancePort]; !ok { // This is a new ELB backend so we only need to worry about // potentientally adding a policy and not removing an // existing one setPolicy = proxyProtocol } else { foundBackends[instancePort] = true // This is an existing ELB backend so we need to determine // if the state changed setPolicy = (currentState != proxyProtocol) } if setPolicy { glog.V(2).Infof("Adjusting AWS loadbalancer proxy protocol on node port %d. Setting to %t", instancePort, proxyProtocol) err := c.setBackendPolicies(loadBalancerName, instancePort, proxyPolicies) if err != nil { return nil, err } dirty = true } } // We now need to figure out if any backend policies need removed // because these old policies will stick around even if there is no // corresponding listener anymore for instancePort, found := range foundBackends { if !found { glog.V(2).Infof("Adjusting AWS loadbalancer proxy protocol on node port %d. Setting to false", instancePort) err := c.setBackendPolicies(loadBalancerName, instancePort, []*string{}) if err != nil { return nil, err } dirty = true } } } } if dirty { loadBalancer, err = c.describeLoadBalancer(loadBalancerName) if err != nil { glog.Warning("Unable to retrieve load balancer after creation/update") return nil, err } } return loadBalancer, nil }
// Returns an error if processing the delta failed, along with a time.Duration // indicating whether processing should be retried; zero means no-retry; otherwise // we should retry in that Duration. func (s *ServiceController) processDelta(delta *cache.Delta) (error, time.Duration) { deltaService, ok := delta.Object.(*api.Service) var namespacedName types.NamespacedName var cachedService *cachedService if !ok { // If the DeltaFIFO saw a key in our cache that it didn't know about, it // can send a deletion with an unknown state. Grab the service from our // cache for deleting. key, ok := delta.Object.(cache.DeletedFinalStateUnknown) if !ok { return fmt.Errorf("Delta contained object that wasn't a service or a deleted key: %+v", delta), doNotRetry } cachedService, ok = s.cache.get(key.Key) if !ok { return fmt.Errorf("Service %s not in cache even though the watcher thought it was. Ignoring the deletion.", key), doNotRetry } deltaService = cachedService.lastState delta.Object = deltaService namespacedName = types.NamespacedName{Namespace: deltaService.Namespace, Name: deltaService.Name} } else { namespacedName.Namespace = deltaService.Namespace namespacedName.Name = deltaService.Name cachedService = s.cache.getOrCreate(namespacedName.String()) } glog.V(2).Infof("Got new %s delta for service: %v", delta.Type, namespacedName) // Ensure that no other goroutine will interfere with our processing of the // service. cachedService.mu.Lock() defer cachedService.mu.Unlock() // Get the most recent state of the service from the API directly rather than // trusting the body of the delta. This avoids update re-ordering problems. // TODO: Handle sync delta types differently rather than doing a get on every // service every time we sync? service, err := s.kubeClient.Core().Services(namespacedName.Namespace).Get(namespacedName.Name) if err != nil && !errors.IsNotFound(err) { glog.Warningf("Failed to get most recent state of service %v from API (will retry): %v", namespacedName, err) return err, cachedService.nextRetryDelay() } else if errors.IsNotFound(err) { glog.V(2).Infof("Service %v not found, ensuring load balancer is deleted", namespacedName) s.eventRecorder.Event(deltaService, api.EventTypeNormal, "DeletingLoadBalancer", "Deleting load balancer") err := s.balancer.EnsureLoadBalancerDeleted(deltaService) if err != nil { message := "Error deleting load balancer (will retry): " + err.Error() s.eventRecorder.Event(deltaService, api.EventTypeWarning, "DeletingLoadBalancerFailed", message) return err, cachedService.nextRetryDelay() } s.eventRecorder.Event(deltaService, api.EventTypeNormal, "DeletedLoadBalancer", "Deleted load balancer") s.cache.delete(namespacedName.String()) cachedService.resetRetryDelay() return nil, doNotRetry } // Update the cached service (used above for populating synthetic deletes) cachedService.lastState = service err, retry := s.createLoadBalancerIfNeeded(namespacedName, service, cachedService.appliedState) if err != nil { message := "Error creating load balancer" if retry { message += " (will retry): " } else { message += " (will not retry): " } message += err.Error() s.eventRecorder.Event(service, api.EventTypeWarning, "CreatingLoadBalancerFailed", message) return err, cachedService.nextRetryDelay() } // Always update the cache upon success. // NOTE: Since we update the cached service if and only if we successfully // processed it, a cached service being nil implies that it hasn't yet // been successfully processed. cachedService.appliedState = service s.cache.set(namespacedName.String(), cachedService) cachedService.resetRetryDelay() return nil, doNotRetry }