2016-02-22 00:13:08 +00:00
|
|
|
/*
|
|
|
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
|
|
|
|
|
|
|
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 (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"reflect"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/golang/glog"
|
|
|
|
|
|
|
|
"k8s.io/kubernetes/pkg/api"
|
|
|
|
"k8s.io/kubernetes/pkg/apis/extensions"
|
|
|
|
"k8s.io/kubernetes/pkg/client/cache"
|
|
|
|
"k8s.io/kubernetes/pkg/client/record"
|
|
|
|
client "k8s.io/kubernetes/pkg/client/unversioned"
|
|
|
|
"k8s.io/kubernetes/pkg/controller/framework"
|
|
|
|
"k8s.io/kubernetes/pkg/labels"
|
|
|
|
"k8s.io/kubernetes/pkg/runtime"
|
|
|
|
"k8s.io/kubernetes/pkg/watch"
|
|
|
|
|
|
|
|
"k8s.io/contrib/ingress/controllers/nginx-third-party/nginx"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// Name of the default config map that contains the configuration for nginx.
|
|
|
|
// Takes the form namespace/name.
|
|
|
|
// If the annotation does not exists the controller will create a new annotation with the default
|
|
|
|
// configuration.
|
|
|
|
lbConfigName = "lbconfig"
|
|
|
|
|
|
|
|
// If you have pure tcp services or https services that need L3 routing, you
|
|
|
|
// must specify them by name. Note that you are responsible for:
|
|
|
|
// 1. Making sure there is no collision between the service ports of these services.
|
|
|
|
// - You can have multiple <mysql svc name>:3306 specifications in this map, and as
|
|
|
|
// long as the service ports of your mysql service don't clash, you'll get
|
|
|
|
// loadbalancing for each one.
|
|
|
|
// 2. Exposing the service ports as node ports on a pod.
|
|
|
|
// 3. Adding firewall rules so these ports can ingress traffic.
|
|
|
|
|
|
|
|
// Comma separated list of tcp/https
|
|
|
|
// namespace/serviceName:portToExport pairings. This assumes you've opened up the right
|
|
|
|
// hostPorts for each service that serves ingress traffic. Te value of portToExport indicates the
|
|
|
|
// port to listen inside nginx, not the port of the service.
|
|
|
|
lbTcpServices = "tcpservices"
|
|
|
|
|
|
|
|
k8sAnnotationPrefix = "nginx-ingress.kubernetes.io"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
keyFunc = framework.DeletionHandlingMetaNamespaceKeyFunc
|
|
|
|
)
|
|
|
|
|
|
|
|
// loadBalancerController watches the kubernetes api and adds/removes services
|
|
|
|
// from the loadbalancer
|
|
|
|
type loadBalancerController struct {
|
|
|
|
client *client.Client
|
|
|
|
ingController *framework.Controller
|
|
|
|
configController *framework.Controller
|
|
|
|
ingLister StoreToIngressLister
|
|
|
|
configLister StoreToConfigMapLister
|
|
|
|
recorder record.EventRecorder
|
|
|
|
ingQueue *taskQueue
|
|
|
|
configQueue *taskQueue
|
|
|
|
stopCh chan struct{}
|
|
|
|
ngx *nginx.NginxManager
|
|
|
|
lbInfo *lbInfo
|
|
|
|
// stopLock is used to enforce only a single call to Stop is active.
|
|
|
|
// Needed because we allow stopping through an http endpoint and
|
|
|
|
// allowing concurrent stoppers leads to stack traces.
|
|
|
|
stopLock sync.Mutex
|
|
|
|
shutdown bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type annotations map[string]string
|
|
|
|
|
|
|
|
func (a annotations) getNginxConfig() (string, bool) {
|
|
|
|
val, ok := a[fmt.Sprintf("%v/%v", k8sAnnotationPrefix, lbConfigName)]
|
|
|
|
return val, ok
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a annotations) getTcpServices() (string, bool) {
|
|
|
|
val, ok := a[fmt.Sprintf("%v/%v", k8sAnnotationPrefix, lbTcpServices)]
|
|
|
|
return val, ok
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewLoadBalancerController creates a controller for nginx loadbalancer
|
|
|
|
func NewLoadBalancerController(kubeClient *client.Client, resyncPeriod time.Duration, defaultSvc, customErrorSvc nginx.Service, namespace string, lbInfo *lbInfo) (*loadBalancerController, error) {
|
|
|
|
eventBroadcaster := record.NewBroadcaster()
|
|
|
|
eventBroadcaster.StartLogging(glog.Infof)
|
|
|
|
eventBroadcaster.StartRecordingToSink(kubeClient.Events(""))
|
|
|
|
|
|
|
|
lbc := loadBalancerController{
|
|
|
|
client: kubeClient,
|
|
|
|
stopCh: make(chan struct{}),
|
|
|
|
recorder: eventBroadcaster.NewRecorder(
|
|
|
|
api.EventSource{Component: "nginx-lb-controller"}),
|
|
|
|
lbInfo: lbInfo,
|
|
|
|
}
|
|
|
|
lbc.ingQueue = NewTaskQueue(lbc.syncIngress)
|
|
|
|
lbc.configQueue = NewTaskQueue(lbc.syncConfig)
|
|
|
|
|
|
|
|
lbc.ngx = nginx.NewManager(kubeClient, defaultSvc, customErrorSvc)
|
|
|
|
|
|
|
|
// Ingress watch handlers
|
|
|
|
pathHandlers := framework.ResourceEventHandlerFuncs{
|
|
|
|
AddFunc: func(obj interface{}) {
|
|
|
|
addIng := obj.(*extensions.Ingress)
|
|
|
|
lbc.recorder.Eventf(addIng, api.EventTypeNormal, "ADD", fmt.Sprintf("Adding ingress %s/%s", addIng.Namespace, addIng.Name))
|
|
|
|
lbc.ingQueue.enqueue(obj)
|
|
|
|
},
|
|
|
|
DeleteFunc: lbc.ingQueue.enqueue,
|
|
|
|
UpdateFunc: func(old, cur interface{}) {
|
|
|
|
if !reflect.DeepEqual(old, cur) {
|
|
|
|
glog.V(2).Infof("Ingress %v changed, syncing", cur.(*extensions.Ingress).Name)
|
|
|
|
}
|
|
|
|
lbc.ingQueue.enqueue(cur)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
lbc.ingLister.Store, lbc.ingController = framework.NewInformer(
|
|
|
|
&cache.ListWatch{
|
|
|
|
ListFunc: ingressListFunc(lbc.client, namespace),
|
|
|
|
WatchFunc: ingressWatchFunc(lbc.client, namespace),
|
|
|
|
},
|
|
|
|
&extensions.Ingress{}, resyncPeriod, pathHandlers)
|
|
|
|
|
|
|
|
// Config watch handlers
|
|
|
|
configHandlers := framework.ResourceEventHandlerFuncs{
|
|
|
|
AddFunc: func(obj interface{}) {
|
|
|
|
lbc.configQueue.enqueue(obj)
|
|
|
|
},
|
|
|
|
DeleteFunc: lbc.configQueue.enqueue,
|
|
|
|
UpdateFunc: func(old, cur interface{}) {
|
|
|
|
if !reflect.DeepEqual(old, cur) {
|
|
|
|
glog.V(2).Infof("nginx rc changed, syncing")
|
|
|
|
lbc.configQueue.enqueue(cur)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
lbc.configLister.Store, lbc.configController = framework.NewInformer(
|
|
|
|
&cache.ListWatch{
|
|
|
|
ListFunc: func(api.ListOptions) (runtime.Object, error) {
|
2016-02-27 15:17:54 +00:00
|
|
|
switch lbInfo.DeployType.(type) {
|
|
|
|
case *api.ReplicationController:
|
|
|
|
rc, err := kubeClient.ReplicationControllers(lbInfo.PodNamespace).Get(lbInfo.ObjectName)
|
|
|
|
return &api.ReplicationControllerList{
|
|
|
|
Items: []api.ReplicationController{*rc},
|
|
|
|
}, err
|
|
|
|
case *extensions.DaemonSet:
|
|
|
|
ds, err := kubeClient.Extensions().DaemonSets(lbInfo.PodNamespace).Get(lbInfo.ObjectName)
|
|
|
|
return &extensions.DaemonSetList{
|
|
|
|
Items: []extensions.DaemonSet{*ds},
|
|
|
|
}, err
|
|
|
|
default:
|
|
|
|
return nil, errInvalidKind
|
|
|
|
}
|
2016-02-22 00:13:08 +00:00
|
|
|
},
|
|
|
|
WatchFunc: func(options api.ListOptions) (watch.Interface, error) {
|
2016-02-27 15:17:54 +00:00
|
|
|
switch lbInfo.DeployType.(type) {
|
|
|
|
case *api.ReplicationController:
|
|
|
|
options.LabelSelector = labels.SelectorFromSet(labels.Set{"name": lbInfo.ObjectName})
|
|
|
|
return kubeClient.ReplicationControllers(lbInfo.PodNamespace).Watch(options)
|
|
|
|
case *extensions.DaemonSet:
|
|
|
|
options.LabelSelector = labels.SelectorFromSet(labels.Set{"name": lbInfo.ObjectName})
|
|
|
|
return kubeClient.Extensions().DaemonSets(lbInfo.PodNamespace).Watch(options)
|
|
|
|
default:
|
|
|
|
return nil, errInvalidKind
|
|
|
|
}
|
2016-02-22 00:13:08 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
&api.ReplicationController{}, resyncPeriod, configHandlers)
|
|
|
|
|
|
|
|
return &lbc, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func ingressListFunc(c *client.Client, ns string) func(api.ListOptions) (runtime.Object, error) {
|
|
|
|
return func(opts api.ListOptions) (runtime.Object, error) {
|
|
|
|
return c.Extensions().Ingress(ns).List(opts)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func ingressWatchFunc(c *client.Client, ns string) func(options api.ListOptions) (watch.Interface, error) {
|
|
|
|
return func(options api.ListOptions) (watch.Interface, error) {
|
|
|
|
return c.Extensions().Ingress(ns).Watch(options)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// syncIngress manages Ingress create/updates/deletes.
|
|
|
|
func (lbc *loadBalancerController) syncIngress(key string) {
|
|
|
|
glog.V(2).Infof("Syncing Ingress %v", key)
|
|
|
|
|
|
|
|
obj, ingExists, err := lbc.ingLister.Store.GetByKey(key)
|
|
|
|
if err != nil {
|
|
|
|
lbc.ingQueue.requeue(key, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !ingExists {
|
|
|
|
glog.Errorf("Ingress not found: %v", key)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// this means some Ingress rule changed. There is no need to reload nginx but
|
|
|
|
// we need to update the rules to use invoking "POST /update-ingress" with the
|
|
|
|
// list of Ingress rules
|
|
|
|
ingList := lbc.ingLister.Store.List()
|
|
|
|
if err := lbc.ngx.SyncIngress(ingList); err != nil {
|
|
|
|
lbc.ingQueue.requeue(key, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ing := *obj.(*extensions.Ingress)
|
|
|
|
if err := lbc.updateIngressStatus(ing); err != nil {
|
|
|
|
lbc.recorder.Eventf(&ing, api.EventTypeWarning, "Status", err.Error())
|
|
|
|
lbc.ingQueue.requeue(key, err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// syncConfig manages changes in nginx configuration.
|
|
|
|
func (lbc *loadBalancerController) syncConfig(key string) {
|
2016-02-27 15:17:54 +00:00
|
|
|
glog.Infof("Syncing nginx configuration")
|
|
|
|
if !lbc.ingController.HasSynced() {
|
|
|
|
glog.Infof("deferring sync till endpoints controller has synced")
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
}
|
|
|
|
|
|
|
|
// we only need to sync nginx
|
|
|
|
if key != fmt.Sprintf("%v/%v", lbc.lbInfo.PodNamespace, lbc.lbInfo.ObjectName) {
|
|
|
|
glog.Warningf("skipping sync because the event is not related to a change in configuration")
|
2016-02-22 00:13:08 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
obj, configExists, err := lbc.configLister.Store.GetByKey(key)
|
|
|
|
if err != nil {
|
|
|
|
lbc.configQueue.requeue(key, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !configExists {
|
2016-02-27 15:17:54 +00:00
|
|
|
glog.Errorf("Configuration not found: %v", key)
|
2016-02-22 00:13:08 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
glog.V(2).Infof("Syncing config %v", key)
|
|
|
|
|
2016-02-27 15:17:54 +00:00
|
|
|
var kindAnnotations map[string]string
|
|
|
|
switch obj.(type) {
|
|
|
|
case *api.ReplicationController:
|
|
|
|
rc := *obj.(*api.ReplicationController)
|
|
|
|
kindAnnotations = rc.Annotations
|
|
|
|
case *extensions.DaemonSet:
|
|
|
|
rc := *obj.(*extensions.DaemonSet)
|
|
|
|
kindAnnotations = rc.Annotations
|
|
|
|
}
|
|
|
|
|
|
|
|
ngxCfgAnn, _ := annotations(kindAnnotations).getNginxConfig()
|
|
|
|
tcpSvcAnn, _ := annotations(kindAnnotations).getTcpServices()
|
2016-02-22 00:13:08 +00:00
|
|
|
|
|
|
|
ngxConfig, err := lbc.ngx.ReadConfig(ngxCfgAnn)
|
|
|
|
if err != nil {
|
|
|
|
glog.Warningf("%v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: tcp services can change (new item in the annotation list)
|
|
|
|
// TODO: skip get everytime
|
|
|
|
tcpServices := getTcpServices(lbc.client, tcpSvcAnn)
|
|
|
|
lbc.ngx.Reload(ngxConfig, tcpServices)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// updateIngressStatus updates the IP and annotations of a loadbalancer.
|
|
|
|
// The annotations are parsed by kubectl describe.
|
|
|
|
func (lbc *loadBalancerController) updateIngressStatus(ing extensions.Ingress) error {
|
|
|
|
ingClient := lbc.client.Extensions().Ingress(ing.Namespace)
|
|
|
|
|
|
|
|
ip := lbc.lbInfo.PodIP
|
|
|
|
currIng, err := ingClient.Get(ing.Name)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
currIng.Status = extensions.IngressStatus{
|
|
|
|
LoadBalancer: api.LoadBalancerStatus{
|
|
|
|
Ingress: []api.LoadBalancerIngress{
|
|
|
|
{IP: ip},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
glog.Infof("Updating loadbalancer %v/%v with IP %v", ing.Namespace, ing.Name, ip)
|
|
|
|
lbc.recorder.Eventf(currIng, api.EventTypeNormal, "CREATE", "ip: %v", ip)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lbc *loadBalancerController) registerHandlers() {
|
|
|
|
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if err := lbc.ngx.IsHealthy(); err != nil {
|
|
|
|
w.WriteHeader(500)
|
|
|
|
w.Write([]byte("nginx error"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.WriteHeader(200)
|
|
|
|
w.Write([]byte("ok"))
|
|
|
|
})
|
|
|
|
|
|
|
|
http.HandleFunc("/stop", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
lbc.Stop()
|
|
|
|
})
|
|
|
|
|
|
|
|
glog.Fatalf(fmt.Sprintf("%v", http.ListenAndServe(fmt.Sprintf(":%v", *healthzPort), nil)))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stop stops the loadbalancer controller.
|
|
|
|
func (lbc *loadBalancerController) Stop() {
|
|
|
|
// Stop is invoked from the http endpoint.
|
|
|
|
lbc.stopLock.Lock()
|
|
|
|
defer lbc.stopLock.Unlock()
|
|
|
|
|
|
|
|
// Only try draining the workqueue if we haven't already.
|
|
|
|
if !lbc.shutdown {
|
|
|
|
close(lbc.stopCh)
|
|
|
|
glog.Infof("Shutting down controller queues")
|
|
|
|
lbc.ingQueue.shutdown()
|
|
|
|
lbc.configQueue.shutdown()
|
|
|
|
lbc.shutdown = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run starts the loadbalancer controller.
|
|
|
|
func (lbc *loadBalancerController) Run() {
|
|
|
|
glog.Infof("Starting nginx loadbalancer controller")
|
|
|
|
go lbc.ngx.Start()
|
|
|
|
go lbc.registerHandlers()
|
|
|
|
|
|
|
|
go lbc.configController.Run(lbc.stopCh)
|
|
|
|
go lbc.configQueue.run(time.Second, lbc.stopCh)
|
|
|
|
|
|
|
|
// Initial nginx configuration.
|
2016-02-27 15:17:54 +00:00
|
|
|
lbc.syncConfig(lbc.lbInfo.PodNamespace + "/" + lbc.lbInfo.ObjectName)
|
2016-02-22 00:13:08 +00:00
|
|
|
|
|
|
|
time.Sleep(5 * time.Second)
|
|
|
|
|
|
|
|
go lbc.ingController.Run(lbc.stopCh)
|
|
|
|
go lbc.ingQueue.run(time.Second, lbc.stopCh)
|
|
|
|
|
|
|
|
<-lbc.stopCh
|
|
|
|
glog.Infof("Shutting down nginx loadbalancer controller")
|
|
|
|
}
|