New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Ingress status management #2386
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Copyright © 2020 VMware | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package main | ||
|
||
import ( | ||
"sync" | ||
|
||
"github.com/projectcontour/contour/internal/k8s" | ||
"github.com/sirupsen/logrus" | ||
v1 "k8s.io/api/core/v1" | ||
) | ||
|
||
// loadBalancerStatusWriter manages the lifetime of IngressStatusUpdaters. | ||
// | ||
// The theory of operation of the loadBalancerStatusWriter is as follows: | ||
// 1. On startup the loadBalancerStatusWriter waits to be elected leader. | ||
// 2. Once elected leader, the loadBalancerStatusWriter waits to receive a | ||
// v1.LoadBalancerStatus value. | ||
// 3. Once a v1.LoadBalancerStatus value has been received, any existing informer | ||
// is stopped and a new informer started in its place. This ensures that all existing | ||
// Ingress objects will have OnAdd events fired to the new event handler. | ||
// 4. Each informer is connected to a k8s.IngressStatusUpdater which reacts to | ||
// OnAdd events for networking.k8s.io/ingress.v1beta1 objects. For each OnAdd | ||
// the object is patched with the v1.LoadBalancerStatus value obtained on creation. | ||
// OnUpdate and OnDelete events are ignored.If a new v1.LoadBalancerStatus value | ||
// is been received, operation restarts at step 3. | ||
// 5. If the worker is stopped, any existing informer is stopped before the worker stops. | ||
type loadBalancerStatusWriter struct { | ||
log logrus.FieldLogger | ||
clients *k8s.Clients | ||
isLeader chan struct{} | ||
lbStatus chan v1.LoadBalancerStatus | ||
} | ||
|
||
func (isw *loadBalancerStatusWriter) Start(stop <-chan struct{}) error { | ||
|
||
// Await leadership election. | ||
isw.log.Info("awaiting leadership election") | ||
select { | ||
case <-stop: | ||
// We were asked to stop before elected leader. | ||
return nil | ||
case <-isw.isLeader: | ||
isw.log.Info("elected leader") | ||
} | ||
|
||
var shutdown chan struct{} | ||
var ingressInformers sync.WaitGroup | ||
for { | ||
select { | ||
case <-stop: | ||
// Use the shutdown channel to stop existing informer and shut down | ||
if shutdown != nil { | ||
close(shutdown) | ||
} | ||
ingressInformers.Wait() | ||
return nil | ||
case lbs := <-isw.lbStatus: | ||
// Stop the existing informer. | ||
if shutdown != nil { | ||
close(shutdown) | ||
} | ||
ingressInformers.Wait() | ||
|
||
isw.log.Info("Received a new address for status.loadBalancer") | ||
|
||
// Create new informer for the new LoadBalancerStatus | ||
factory := isw.clients.NewInformerFactory() | ||
inf := factory.Networking().V1beta1().Ingresses().Informer() | ||
log := isw.log.WithField("context", "IngressStatusUpdater") | ||
inf.AddEventHandler(&k8s.IngressStatusUpdater{ | ||
Client: isw.clients.ClientSet(), | ||
Logger: log, | ||
Status: lbs, | ||
}) | ||
|
||
shutdown = make(chan struct{}) | ||
ingressInformers.Add(1) | ||
fn := startInformer(factory, log) | ||
go func() { | ||
defer ingressInformers.Done() | ||
if err := fn(shutdown); err != nil { | ||
return | ||
} | ||
}() | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
// +build none | ||
|
||
package main | ||
|
||
import ( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// Copyright © 2020 VMware | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package k8s | ||
|
||
import ( | ||
"github.com/sirupsen/logrus" | ||
v1 "k8s.io/api/core/v1" | ||
"k8s.io/api/networking/v1beta1" | ||
clientset "k8s.io/client-go/kubernetes" | ||
) | ||
|
||
// StatusLoadbalancerUpdater observes informer OnAdd events and | ||
// updates the ingress.status.loadBalancer field on all Ingress | ||
// objects that match the ingress class (if used). | ||
type IngressStatusUpdater struct { | ||
Client clientset.Interface | ||
Logger logrus.FieldLogger | ||
Status v1.LoadBalancerStatus | ||
} | ||
|
||
jpeach marked this conversation as resolved.
Show resolved
Hide resolved
|
||
func (s *IngressStatusUpdater) OnAdd(obj interface{}) { | ||
ing := obj.(*v1beta1.Ingress).DeepCopy() | ||
|
||
// TODO(dfc) check ingress class | ||
|
||
ing.Status.LoadBalancer = s.Status | ||
_, err := s.Client.NetworkingV1beta1().Ingresses(ing.GetNamespace()).UpdateStatus(ing) | ||
if err != nil { | ||
s.Logger. | ||
WithField("name", ing.GetName()). | ||
WithField("namespace", ing.GetNamespace()). | ||
WithError(err).Error("unable to update status") | ||
} | ||
} | ||
|
||
func (s *IngressStatusUpdater) OnUpdate(oldObj, newObj interface{}) { | ||
// Ignoring OnUpdate allows us to avoid the message generated | ||
// from the status update. | ||
|
||
// TODO(dfc) handle these cases: | ||
// - OnUpdate transitions from an ingress class which is out of scope | ||
// to one in scope. | ||
// - OnUpdate transitions from an ingress class in scope to one out | ||
// of scope. | ||
} | ||
|
||
func (s *IngressStatusUpdater) OnDelete(obj interface{}) { | ||
// we don't need to update the status on resources that | ||
// have been deleted. | ||
} | ||
|
||
// ServiceStatusLoadBalancerWatcher implements ResourceEventHandler and | ||
// watches for changes to the status.loadbalancer field | ||
type ServiceStatusLoadBalancerWatcher struct { | ||
ServiceName string | ||
LBStatus chan v1.LoadBalancerStatus | ||
jpeach marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
func (s *ServiceStatusLoadBalancerWatcher) OnAdd(obj interface{}) { | ||
svc, ok := obj.(*v1.Service) | ||
if !ok { | ||
// not a service | ||
return | ||
} | ||
if svc.Name != s.ServiceName { | ||
return | ||
} | ||
s.notify(svc.Status.LoadBalancer) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Is a method useful here? We could just s.LBStatus <- svc.Status.LoadBalancer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this could boil down to: if svc, ok := obj.(*v1.Service); ok && svc.Name == s.ServiceName {
s.LBStatus <- svc.Status.LoadBalancer
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed about the method, it's not really necessary for now, unless we end up wanting to do something more clever than just send down the channel. For the code though, I feel like having individual clauses is more verbose, but far easier to read. For lack of a better word, I'd call it more idiomatic. |
||
} | ||
|
||
func (s *ServiceStatusLoadBalancerWatcher) OnUpdate(oldObj, newObj interface{}) { | ||
svc, ok := newObj.(*v1.Service) | ||
if !ok { | ||
// not a service | ||
return | ||
} | ||
if svc.Name != s.ServiceName { | ||
return | ||
} | ||
s.notify(svc.Status.LoadBalancer) | ||
} | ||
|
||
func (s *ServiceStatusLoadBalancerWatcher) OnDelete(obj interface{}) { | ||
svc, ok := obj.(*v1.Service) | ||
if !ok { | ||
// not a service | ||
return | ||
} | ||
if svc.Name != s.ServiceName { | ||
return | ||
} | ||
s.notify(v1.LoadBalancerStatus{ | ||
Ingress: nil, | ||
}) | ||
} | ||
|
||
func (s *ServiceStatusLoadBalancerWatcher) notify(lbstatus v1.LoadBalancerStatus) { | ||
s.LBStatus <- lbstatus | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we reuse the informers created earlier? What if the informer would fail to create?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason to throw away the old informer is to ensure that the new informer does a full resync, so that all the existing Ingress objects that are in scope will get updated (via OnAdd calls).
I think that if the informer failed to create, there would be a panic somewhere, because this method doesn't return an error for us to check.