Commit c5ff26da authored by Geoff Simmons's avatar Geoff Simmons

Varnish/Ingress may implement and merge Ingresses across namespaces.

An Ingress definition with ingress.class for Varnish can identify
the Varnish Service that implements its rules:

* If it has the ingress.varnish-cache.org/varnish-svc annotation,
  then the values names the Varnish Service, which may be named
  using namespace/name notation. If the namespace is left out,
  then the same namespace as the Ingress is assumed.

* If the Ingress does not have the annotation, then:

  * If there is only one Varnish-as-Ingress Service in the cluster
    (with label app:varnish-ingress), then that Service implements
    the rules.

  * Otherwise if there is only one Varnish Service in the same
    namespace as the Ingress, then that Service implements the
    rules.

  * Otherwise error

A Varnish Service can define Services from different namespaces as
its backends, if it implements Ingresses from those namespaces which
define those Services as Ingress backends. (Ingresses only define
backends from the same namespace.)

Ingresses can be merged under these conditions:

* No host is specified in more than one Ingress.

* There is no more than one default backends in all of the Ingresses
  to be merged.

If either of these rules are violated, then error.

This resolves the question of merging Ingresses (to be documented in
upcoming commits).

Ref #26
Ref #13
parent 642209bc
This diff is collapsed.
/*
* Copyright (c) 2019 UPLEX Nils Goroll Systemoptimierung
* All rights reserved
*
* Author: Geoffrey Simmons <geoffrey.simmons@uplex.de>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package controller
import (
"testing"
extensions "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var ing1 = &extensions.Ingress{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "ing1",
},
Spec: extensions.IngressSpec{
Backend: &extensions.IngressBackend{
ServiceName: "default-svc2",
},
Rules: []extensions.IngressRule{
extensions.IngressRule{Host: "host1"},
},
},
}
var ing2 = &extensions.Ingress{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "ing2",
},
Spec: extensions.IngressSpec{
Backend: &extensions.IngressBackend{
ServiceName: "default-svc2",
},
Rules: []extensions.IngressRule{
extensions.IngressRule{Host: "host2"},
},
},
}
var ing3 = &extensions.Ingress{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "ing3",
},
Spec: extensions.IngressSpec{
Rules: []extensions.IngressRule{
extensions.IngressRule{Host: "host1"},
extensions.IngressRule{Host: "host2"},
},
},
}
func TestIngressMergeError(t *testing.T) {
ings := []*extensions.Ingress{ing1, ing2}
if err := ingMergeError(ings); err == nil {
t.Errorf("ingMergeError(): no error reported for more than " +
"one default backend")
} else if testing.Verbose() {
t.Logf("ingMergeError() returned as expected: %v", err)
}
ings = []*extensions.Ingress{ing2, ing3}
if err := ingMergeError(ings); err == nil {
t.Errorf("ingMergeError(): no error reported for overlapping " +
"Hosts")
} else if testing.Verbose() {
t.Logf("ingMergeError() returned as expected: %v", err)
}
}
......@@ -59,7 +59,8 @@ var (
func (worker *NamespaceWorker) getServiceEndpoints(
svc *api_v1.Service) (ep *api_v1.Endpoints, err error) {
eps, err := worker.endp.List(labels.Everything())
nsLister := worker.listers.endp.Endpoints(svc.Namespace)
eps, err := nsLister.List(labels.Everything())
if err != nil {
return
}
......
......@@ -67,6 +67,7 @@ type NamespaceWorker struct {
vController *varnish.VarnishController
queue workqueue.RateLimitingInterface
stopChan chan struct{}
listers *Listers
ing ext_listers.IngressNamespaceLister
svc core_v1_listers.ServiceNamespaceLister
endp core_v1_listers.EndpointsNamespaceLister
......@@ -341,6 +342,7 @@ func (qs *NamespaceQueues) next() {
vController: qs.vController,
queue: q,
stopChan: make(chan struct{}),
listers: qs.listers,
ing: qs.listers.ing.Ingresses(ns),
svc: qs.listers.svc.Services(ns),
endp: qs.listers.endp.Endpoints(ns),
......
......@@ -105,14 +105,27 @@ func (vadmErrs VarnishAdmErrors) Error() string {
return sb.String()
}
// Meta encapsulates meta-data for the resource types that enter into
// a Varnish configuration: Ingress, VarnishConfig and BackendConfig.
//
// Key: namespace/name
// UID: UID field from the resource meta-data
// Ver: ResourceVersion field from the resource meta-data
type Meta struct {
Key string
UID string
Ver string
}
type vclSpec struct {
spec vcl.Spec
key string
uid string
ings map[string]Meta
vcfg Meta
bcfg map[string]Meta
}
func (spec vclSpec) configName() string {
name := fmt.Sprintf("%s%s_%s_%0x", ingressPrefix, spec.key, spec.uid,
name := fmt.Sprintf("%s%0x", ingressPrefix,
spec.spec.Canonical().DeepHash())
return nonAlNum.ReplaceAllLiteralString(name, "_")
}
......@@ -563,57 +576,50 @@ func (vc *VarnishController) updateBeGauges() {
beEndpsGauge.Set(float64(nBeEndps))
}
// Update a Varnish Service to implement an Ingress.
// Update a Varnish Service to implement an configuration.
//
// svcKey: namespace/name key for the Service
// ingKey: namespace/name key for the Ingress
// uid: UID field from the Ingress
// spec: VCL spec corresponding to the Ingress definition
func (vc *VarnishController) Update(
svcKey, ingKey, uid string, spec vcl.Spec) error {
// spec: VCL spec corresponding to the configuration
// ingsMeta: Ingress meta-data
// vcfgMeta: VarnishConfig meta-data
// bcfgMeta: BackendConfig meta-data
func (vc *VarnishController) Update(svcKey string, spec vcl.Spec,
ingsMeta map[string]Meta, vcfgMeta Meta,
bcfgMeta map[string]Meta) error {
svc, exists := vc.svcs[svcKey]
if !exists {
svc = &varnishSvc{instances: make([]*varnishInst, 0)}
vc.svcs[svcKey] = svc
svcsGauge.Inc()
vc.log.Infof("Added Varnish service definition %s for Ingress "+
"%s uid=%s", svcKey, ingKey, uid)
vc.log.Infof("Added Varnish service definition %s", svcKey)
}
svc.cfgLoaded = false
if svc.spec == nil {
svc.spec = &vclSpec{}
}
svc.spec.key = ingKey
svc.spec.uid = uid
svc.spec.spec = spec
svc.spec.ings = ingsMeta
svc.spec.vcfg = vcfgMeta
svc.spec.bcfg = bcfgMeta
vc.updateBeGauges()
if len(svc.instances) == 0 {
return fmt.Errorf("Ingress %s uid=%s: Currently no known "+
"endpoints for Varnish service %s", ingKey, uid, svcKey)
return fmt.Errorf("Currently no known endpoints for Varnish "+
"service %s", svcKey)
}
return vc.updateVarnishSvc(svcKey)
}
// DeleteIngress is called for the Delete event on an Ingress, and
// syncs its effect for a Varnish Service.
//
// svcKey: namespace/name key for the Varnish Service
// ingKey: namespace/name key for the Ingress
//
// We currently only support one Ingress definition at a time for a
// Varnish Service, so deleting the Ingress means that we set Varnish
// instances to the not ready state.
func (vc *VarnishController) DeleteIngress(svcKey, ingKey string) error {
// SetNotReady may be called on the Delete event on an Ingress, if no
// Ingresses remain that are to be implemented by a Varnish Service.
// The Service is set to the not ready state, by relabelling VCL so
// that readiness checks are not answered with status 200.
func (vc *VarnishController) SetNotReady(svcKey string) error {
svc, ok := vc.svcs[svcKey]
if !ok {
return fmt.Errorf("Delete Ingress %s: no known Varnish service",
ingKey)
}
if svc.spec != nil && svc.spec.key != ingKey {
return fmt.Errorf("Delete Ingress %s: Ingress %s is assigned "+
"to Varnish service %s", ingKey, svc.spec.key, svcKey)
return fmt.Errorf("Set Varnish Service not ready: %s unknown",
svcKey)
}
svc.spec = nil
......@@ -636,24 +642,57 @@ func (vc *VarnishController) DeleteIngress(svcKey, ingKey string) error {
return errs
}
// HasIngress returns true iff an Ingress definition is already loaded
// for a Varnish Service (so a new sync attempt is not necessary).
// HasConfig returns true iff a configuration is already loaded for a
// Varnish Service (so a new sync attempt is not necessary).
//
// svcKey: namespace/name key for the Varnish Service
// ingKey: namespace/name key for the Ingress
// uid: UID field from the Ingress
// spec: VCL specification derived from the Ingress
func (vc *VarnishController) HasIngress(svcKey, ingKey, uid string,
spec vcl.Spec) bool {
// spec: VCL specification derived from the configuration
// ingsMeta: Ingress meta-data
// vcfgMeta: VarnishConfig meta-data
// bcfgMeta: BackendConfig meta-data
func (vc *VarnishController) HasConfig(svcKey string, spec vcl.Spec,
ingsMeta map[string]Meta, vcfgMeta Meta,
bcfgMeta map[string]Meta) bool {
svc, ok := vc.svcs[svcKey]
if !ok {
return false
}
return svc.cfgLoaded &&
svc.spec.key == ingKey &&
svc.spec.uid == uid &&
reflect.DeepEqual(svc.spec.spec.Canonical(), spec.Canonical())
if !svc.cfgLoaded {
return false
}
if len(ingsMeta) != len(svc.spec.ings) {
return false
}
if len(bcfgMeta) != len(svc.spec.bcfg) {
return false
}
if vcfgMeta.Key != svc.spec.vcfg.Key ||
vcfgMeta.UID != svc.spec.vcfg.UID ||
vcfgMeta.Ver != svc.spec.vcfg.Ver {
return false
}
for k, v := range ingsMeta {
specIng, exists := svc.spec.ings[k]
if !exists {
return false
}
if specIng.Key != v.Key || specIng.UID != v.UID ||
specIng.Ver != v.Ver {
return false
}
}
for k, v := range bcfgMeta {
specBcfg, exists := svc.spec.bcfg[k]
if !exists {
return false
}
if specBcfg.Key != v.Key || specBcfg.UID != v.UID ||
specBcfg.Ver != v.Ver {
return false
}
}
return reflect.DeepEqual(svc.spec.spec.Canonical(), spec.Canonical())
}
// SetAdmSecret stores the Secret data identified by the
......
......@@ -30,7 +30,10 @@ package varnish
import (
"fmt"
"strings"
"testing"
"code.uplex.de/uplex-varnish/k8s-ingress/pkg/varnish/vcl"
)
func TestVarnishAdmError(t *testing.T) {
......@@ -62,3 +65,253 @@ func TestVarnishAdmError(t *testing.T) {
t.Errorf("VarnishAdmErrors.Error() want=%s got=%s", want, err)
}
}
// Test data for HasConfig()
var teaSvc = vcl.Service{
Name: "tea-svc",
Addresses: []vcl.Address{
{
IP: "192.0.2.1",
Port: 80,
},
{
IP: "192.0.2.2",
Port: 80,
},
{
IP: "192.0.2.3",
Port: 80,
},
},
}
var coffeeSvc = vcl.Service{
Name: "coffee-svc",
Addresses: []vcl.Address{
{
IP: "192.0.2.4",
Port: 80,
},
{
IP: "192.0.2.5",
Port: 80,
},
},
}
var cafeSpec = vcl.Spec{
DefaultService: vcl.Service{},
Rules: []vcl.Rule{{
Host: "cafe.example.com",
PathMap: map[string]vcl.Service{
"/tea": teaSvc,
"/coffee": coffeeSvc,
},
}},
AllServices: map[string]vcl.Service{
"tea-svc": teaSvc,
"coffee-svc": coffeeSvc,
},
}
var teaSvcShuf = vcl.Service{
Name: "tea-svc",
Addresses: []vcl.Address{
{
IP: "192.0.2.3",
Port: 80,
},
{
IP: "192.0.2.1",
Port: 80,
},
{
IP: "192.0.2.2",
Port: 80,
},
},
}
var coffeeSvcShuf = vcl.Service{
Name: "coffee-svc",
Addresses: []vcl.Address{
{
IP: "192.0.2.5",
Port: 80,
},
{
IP: "192.0.2.4",
Port: 80,
},
},
}
var cafeSpecShuf = vcl.Spec{
DefaultService: vcl.Service{},
Rules: []vcl.Rule{{
Host: "cafe.example.com",
PathMap: map[string]vcl.Service{
"/coffee": coffeeSvcShuf,
"/tea": teaSvcShuf,
},
}},
AllServices: map[string]vcl.Service{
"coffee-svc": coffeeSvcShuf,
"tea-svc": teaSvcShuf,
},
}
var ingsMeta = map[string]Meta{
"default/cafe": Meta{
Key: "default/cafe",
UID: "123e4567-e89b-12d3-a456-426655440000",
Ver: "123456",
},
"ns/name": Meta{
Key: "ns/name",
UID: "00112233-4455-6677-8899-aabbccddeeff",
Ver: "654321",
},
"kube-system/ingress": Meta{
Key: "kube-system/ingress",
UID: "6ba7b812-9dad-11d1-80b4-00c04fd430c8",
Ver: "987654",
},
}
var bcfgsMeta = map[string]Meta{
"coffee-svc": Meta{
Key: "default/coffee-svc-cfg",
UID: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
Ver: "010101",
},
"tea-svc": Meta{
Key: "ns/tea-svc-cfg",
UID: "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
Ver: "909090",
},
}
var vcfgMeta = Meta{
Key: "default/varnish-cfg",
UID: "6ba7b814-9dad-11d1-80b4-00c04fd430c8",
Ver: "37337",
}
func TestHasConfig(t *testing.T) {
spec := vclSpec{
spec: cafeSpec,
ings: ingsMeta,
vcfg: vcfgMeta,
bcfg: bcfgsMeta,
}
vSvc := varnishSvc{
spec: &spec,
cfgLoaded: true,
}
vc := VarnishController{
svcs: map[string]*varnishSvc{"default/cafe-ingress": &vSvc},
}
svcKey := "default/cafe-ingress"
if !vc.HasConfig(svcKey, cafeSpecShuf, ingsMeta, vcfgMeta, bcfgsMeta) {
t.Errorf("HasConfig() got:false want:true")
}
if vc.HasConfig("ns/name", cafeSpecShuf, ingsMeta, vcfgMeta,
bcfgsMeta) {
t.Errorf("HasConfig(unknown Service) got:true want:false")
}
vSvc.cfgLoaded = false
if vc.HasConfig(svcKey, cafeSpecShuf, ingsMeta, vcfgMeta, bcfgsMeta) {
t.Errorf("HasConfig(cfgLoaded=false) got:true want:false")
}
vSvc.cfgLoaded = true
otherVcfg := vcfgMeta
otherVcfg.Ver = "37338"
if vc.HasConfig(svcKey, cafeSpecShuf, ingsMeta, otherVcfg, bcfgsMeta) {
t.Errorf("HasConfig(changed VarnishConfig) got:true want:false")
}
otherIngs := make(map[string]Meta)
for k, v := range ingsMeta {
otherIngs[k] = v
}
otherIngs["key"] = Meta{}
if vc.HasConfig(svcKey, cafeSpecShuf, otherIngs, vcfgMeta, bcfgsMeta) {
t.Errorf("HasConfig(more Ingresses) got:true want:false")
}
delete(otherIngs, "key")
otherIngs["default/cafe"] = Meta{
Key: "default/cafe",
UID: "123e4567-e89b-12d3-a456-426655440000",
Ver: "123457",
}
if vc.HasConfig(svcKey, cafeSpecShuf, otherIngs, vcfgMeta, bcfgsMeta) {
t.Errorf("HasConfig(changed Ingresses) got:true want:false")
}
delete(otherIngs, "default/cafe")
if vc.HasConfig(svcKey, cafeSpecShuf, otherIngs, vcfgMeta, bcfgsMeta) {
t.Errorf("HasConfig(fewer Ingresses) got:true want:false")
}
otherBcfgs := make(map[string]Meta)
for k, v := range bcfgsMeta {
otherBcfgs[k] = v
}
otherBcfgs["key"] = Meta{}
if vc.HasConfig(svcKey, cafeSpecShuf, ingsMeta, vcfgMeta, otherBcfgs) {
t.Errorf("HasConfig(more BackendConfigs) got:true want:false")
}
delete(otherBcfgs, "key")
otherBcfgs["coffee-svc"] = Meta{
Key: "default/coffee-svc-cfg",
UID: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
Ver: "010102",
}
if vc.HasConfig(svcKey, cafeSpecShuf, ingsMeta, vcfgMeta, otherBcfgs) {
t.Errorf("HasConfig(changed BackendConfigs) got:true want:false")
}
delete(otherBcfgs, "coffee-svc")
if vc.HasConfig(svcKey, cafeSpecShuf, ingsMeta, vcfgMeta, otherBcfgs) {
t.Errorf("HasConfig(fewer BackendConfigs) got:true want:false")
}
}
func TestConfigName(t *testing.T) {
spec := vclSpec{spec: cafeSpec}
name1 := spec.configName()
if !strings.HasPrefix(name1, ingressPrefix) {
t.Errorf("configName(): name %s does not have prefix %s",
name1, ingressPrefix)
}
spec = vclSpec{spec: cafeSpecShuf}
name2 := spec.configName()
if !strings.HasPrefix(name2, ingressPrefix) {
t.Errorf("configName(): name %s does not have prefix %s",
name2, ingressPrefix)
}
if name1 != name2 {
t.Errorf("configName(): equivalent specs have different names:"+
"'%s' '%s'", name1, name2)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment